diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 5ae4aa8..cc9cb96 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2758,4 +2758,13 @@ target_link_libraries(step430_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step431_test tests/step431_test.cpp) +target_include_directories(step431_test PRIVATE src) +target_link_libraries(step431_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/WorkflowProgressDashboard.h b/editor/src/WorkflowProgressDashboard.h new file mode 100644 index 0000000..4f684ad --- /dev/null +++ b/editor/src/WorkflowProgressDashboard.h @@ -0,0 +1,112 @@ +#pragma once + +#include "WorkflowProgress.h" +#include "WorkflowCostTracker.h" + +#include +#include +#include + +struct ThroughputPoint { + std::string timestamp; + int completed = 0; +}; + +struct WorkerSlice { + std::string workerType; + int count = 0; + float percent = 0.0f; +}; + +struct BlockerSummary { + std::string type; + int count = 0; + std::string description; +}; + +struct ProgressDashboardModel { + float completionPercent = 0.0f; + int completedItems = 0; + int totalItems = 0; + float itemsPerMinute = 0.0f; + float estimatedRemainingMinutes = -1.0f; + std::vector throughputSeries; + std::vector workerBreakdown; + std::vector blockerSummary; + double actualCost = 0.0; + double estimatedCost = 0.0; + double estimatedRemainingCost = 0.0; +}; + +class WorkflowProgressDashboard { +public: + static ProgressDashboardModel build(const WorkflowState& wf, + const WorkflowProgress& progress, + const WorkflowCostTracker* costTracker = nullptr) { + ProgressDashboardModel m; + auto snapshot = progress.getSnapshot(); + m.completionPercent = snapshot.completionPercent; + m.completedItems = snapshot.completedItems; + m.totalItems = snapshot.totalItems; + m.itemsPerMinute = snapshot.itemsPerMinute; + m.estimatedRemainingMinutes = snapshot.estimatedRemainingMinutes; + m.throughputSeries = buildThroughputSeries(progress.getTimeline()); + m.workerBreakdown = buildWorkerBreakdown(wf.getStats().byWorkerType); + m.blockerSummary = buildBlockerSummary(snapshot.blockers); + + if (costTracker) { + auto cost = costTracker->summarize(); + m.actualCost = cost.actualCost; + m.estimatedCost = cost.estimatedCost; + int remaining = std::max(0, m.totalItems - m.completedItems); + double avgPerItem = 0.0; + if (m.completedItems > 0) avgPerItem = m.actualCost / (double)m.completedItems; + m.estimatedRemainingCost = avgPerItem * (double)remaining; + } + return m; + } + +private: + static std::vector buildThroughputSeries( + const std::vector& timeline) { + std::vector out; + int completed = 0; + for (const auto& e : timeline) { + if (e.type == "completed") ++completed; + out.push_back({e.timestamp, completed}); + } + return out; + } + + static std::vector buildWorkerBreakdown( + const std::map& byWorkerType) { + std::vector out; + int total = 0; + for (const auto& [_, count] : byWorkerType) total += count; + for (const auto& [worker, count] : byWorkerType) { + WorkerSlice s; + s.workerType = worker; + s.count = count; + s.percent = total > 0 ? (100.0f * (float)count / (float)total) : 0.0f; + out.push_back(s); + } + std::sort(out.begin(), out.end(), + [](const WorkerSlice& a, const WorkerSlice& b) { + return a.workerType < b.workerType; + }); + return out; + } + + static std::vector buildBlockerSummary( + const std::vector& blockers) { + std::vector out; + for (const auto& b : blockers) { + out.push_back({b.type, (int)b.itemIds.size(), b.description}); + } + std::sort(out.begin(), out.end(), + [](const BlockerSummary& a, const BlockerSummary& b) { + return a.type < b.type; + }); + return out; + } +}; diff --git a/editor/tests/step431_test.cpp b/editor/tests/step431_test.cpp new file mode 100644 index 0000000..8e87928 --- /dev/null +++ b/editor/tests/step431_test.cpp @@ -0,0 +1,203 @@ +// Step 431: Progress Dashboard Tests (12 tests) + +#include "WorkflowProgressDashboard.h" + +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static WorkItem makeItem(const std::string& id, const std::string& worker, const std::string& status) { + WorkItem wi; + wi.id = id; + wi.nodeId = id + "_node"; + wi.nodeName = id; + wi.nodeType = "Function"; + wi.workerType = worker; + wi.status = status; + wi.priority = "medium"; + return wi; +} + +static WorkflowState makeWorkflow() { + WorkflowState wf("dashboard"); + wf.queue.enqueue(makeItem("a", "deterministic", WI_COMPLETE)); + wf.queue.enqueue(makeItem("b", "slm", WI_IN_PROGRESS)); + wf.queue.enqueue(makeItem("c", "llm", WI_REVIEW)); + wf.queue.enqueue(makeItem("d", "human", WI_READY)); + return wf; +} + +static WorkflowProgress makeProgress() { + WorkflowProgress p(4); + p.recordEvent({"routed", "a", {{"workerType", "deterministic"}}, "2026-01-01T00:00:00Z"}); + p.recordEvent({"completed", "a", json::object(), "2026-01-01T00:01:00Z"}); + p.recordEvent({"routed", "b", {{"workerType", "slm"}}, "2026-01-01T00:02:00Z"}); + p.recordEvent({"completed", "b", json::object(), "2026-01-01T00:03:00Z"}); + p.recordEvent({"blocked", "", {{"blockers", json::array({ + {{"type", "needs-review"}, {"itemIds", json::array({"c"})}, {"description", "review pending"}} + })}}, "2026-01-01T00:04:00Z"}); + return p; +} + +static WorkflowCostTracker makeCosts(ModelProfileRegistry& reg) { + WorkflowCostTracker costs(®); + costs.recordEstimate("a", "deterministic", 100, 20); + costs.recordEstimate("b", "slm", 800, 120); + costs.recordActual("a", "deterministic", 80, 10); + costs.recordActual("b", "slm", 900, 140); + return costs; +} + +void test_dashboard_copies_completion_metrics() { + TEST(dashboard_copies_completion_metrics); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.totalItems == 4, "total items mismatch"); + CHECK(d.completedItems == 2, "completed mismatch"); + CHECK(d.completionPercent > 40.0f, "completion percent should be >40"); + PASS(); +} + +void test_dashboard_throughput_series_from_timeline() { + TEST(dashboard_throughput_series_from_timeline); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.throughputSeries.size() == p.getTimeline().size(), "throughput series size mismatch"); + PASS(); +} + +void test_throughput_series_counts_completed_only() { + TEST(throughput_series_counts_completed_only); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(!d.throughputSeries.empty(), "throughput series empty"); + CHECK(d.throughputSeries.back().completed == 2, "completed counter should end at 2"); + PASS(); +} + +void test_worker_breakdown_contains_all_workers() { + TEST(worker_breakdown_contains_all_workers); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + bool hasDet = false, hasSlm = false, hasLlm = false, hasHuman = false; + for (const auto& s : d.workerBreakdown) { + if (s.workerType == "deterministic") hasDet = true; + if (s.workerType == "slm") hasSlm = true; + if (s.workerType == "llm") hasLlm = true; + if (s.workerType == "human") hasHuman = true; + } + CHECK(hasDet && hasSlm && hasLlm && hasHuman, "missing worker slice"); + PASS(); +} + +void test_worker_breakdown_percent_sum_around_100() { + TEST(worker_breakdown_percent_sum_around_100); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + float sum = 0.0f; + for (const auto& s : d.workerBreakdown) sum += s.percent; + CHECK(std::fabs(sum - 100.0f) < 0.5f, "percent sum should be ~100"); + PASS(); +} + +void test_blocker_summary_reflects_snapshot() { + TEST(blocker_summary_reflects_snapshot); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(!d.blockerSummary.empty(), "blocker summary should not be empty"); + CHECK(d.blockerSummary[0].count == 1, "blocker count mismatch"); + PASS(); +} + +void test_cost_fields_populated_with_tracker() { + TEST(cost_fields_populated_with_tracker); + auto wf = makeWorkflow(); + auto p = makeProgress(); + ModelProfileRegistry reg; + auto costs = makeCosts(reg); + auto d = WorkflowProgressDashboard::build(wf, p, &costs); + CHECK(d.actualCost >= 0.0, "actual cost should be non-negative"); + CHECK(d.estimatedCost >= 0.0, "estimated cost should be non-negative"); + PASS(); +} + +void test_remaining_cost_estimate_positive_when_items_left() { + TEST(remaining_cost_estimate_positive_when_items_left); + auto wf = makeWorkflow(); + auto p = makeProgress(); + ModelProfileRegistry reg; + auto costs = makeCosts(reg); + auto d = WorkflowProgressDashboard::build(wf, p, &costs); + CHECK(d.estimatedRemainingCost >= 0.0, "remaining estimate should be non-negative"); + PASS(); +} + +void test_costs_zero_without_tracker() { + TEST(costs_zero_without_tracker); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.actualCost == 0.0, "actual cost should be zero without tracker"); + CHECK(d.estimatedCost == 0.0, "estimated cost should be zero without tracker"); + PASS(); +} + +void test_empty_timeline_yields_empty_throughput() { + TEST(empty_timeline_yields_empty_throughput); + auto wf = makeWorkflow(); + WorkflowProgress p(4); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.throughputSeries.empty(), "empty timeline should produce empty throughput"); + PASS(); +} + +void test_eta_passthrough_from_progress_snapshot() { + TEST(eta_passthrough_from_progress_snapshot); + auto wf = makeWorkflow(); + auto p = makeProgress(); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.estimatedRemainingMinutes >= 0.0f, "eta should be available in this scenario"); + PASS(); +} + +void test_dashboard_handles_zero_total_items() { + TEST(dashboard_handles_zero_total_items); + WorkflowState wf("empty"); + WorkflowProgress p(0); + auto d = WorkflowProgressDashboard::build(wf, p, nullptr); + CHECK(d.totalItems == 0, "total should be zero"); + CHECK(d.completionPercent == 0.0f, "completion should be zero"); + PASS(); +} + +int main() { + std::cout << "Step 431: Progress Dashboard Tests\n"; + + test_dashboard_copies_completion_metrics(); // 1 + test_dashboard_throughput_series_from_timeline(); // 2 + test_throughput_series_counts_completed_only(); // 3 + test_worker_breakdown_contains_all_workers(); // 4 + test_worker_breakdown_percent_sum_around_100(); // 5 + test_blocker_summary_reflects_snapshot(); // 6 + test_cost_fields_populated_with_tracker(); // 7 + test_remaining_cost_estimate_positive_when_items_left(); // 8 + test_costs_zero_without_tracker(); // 9 + test_empty_timeline_yields_empty_throughput(); // 10 + test_eta_passthrough_from_progress_snapshot(); // 11 + test_dashboard_handles_zero_total_items(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 564a7ef..622cd44 100644 --- a/progress.md +++ b/progress.md @@ -4900,6 +4900,50 @@ coding, critical-path highlighting, hover details, and click navigation IDs. - `editor/src/MCPServer.h` (`1940` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) +### Step 431: Progress Dashboard +**Status:** PASS (12/12 tests) + +Added a progress-dashboard model adapter that composes workflow stats, +progress/timeline metrics, blocker summaries, worker distribution, and cost +tracking into a panel-ready snapshot. + +**Files created:** +- `editor/src/WorkflowProgressDashboard.h` — dashboard support: + - completion metrics passthrough (percent, throughput, ETA) + - throughput series derivation from timeline events + - worker-breakdown slices with percentage normalization + - blocker-summary projection for action surfaces + - optional cost integration (`WorkflowCostTracker`) with remaining-cost estimate +- `editor/tests/step431_test.cpp` — 12 tests covering: + 1. completion metric passthrough + 2. throughput-series length from timeline + 3. completed-count progression in throughput series + 4. worker-slice presence + 5. worker-percent normalization + 6. blocker-summary projection + 7. populated cost fields with tracker + 8. remaining-cost estimate path + 9. zero-cost behavior without tracker + 10. empty-timeline edge case + 11. ETA passthrough + 12. zero-total workflow edge case + +**Files modified:** +- `editor/CMakeLists.txt` — `step431_test` target + +**Verification run:** +- `step431_test` — PASS (12/12) new step coverage +- `step430_test` — PASS (12/12) regression coverage +- `step429_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/WorkflowProgressDashboard.h` within header-size limit (`112` <= `600`) +- `editor/tests/step431_test.cpp` within test-file size guidance (`202` lines) +- Legacy oversized headers persist: + - `editor/src/ast/Serialization.h` (`1427` > `600`) + - `editor/src/MCPServer.h` (`1940` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)