From 39555929d507928b4ba63fbac6120422886ccd12 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 15 Feb 2026 16:28:59 -0700 Subject: [PATCH] =?UTF-8?q?Step=20322:=20WorkflowState=20=E2=80=94=20Proje?= =?UTF-8?q?ct-Level=20Workflow=20Tracking=20(12/12=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level workflow state with auto-detected phases, statistics, audit trail, skeleton population, and full JSON serialization. Co-Authored-By: Claude Opus 4.6 --- editor/CMakeLists.txt | 5 + editor/src/WorkflowState.h | 259 +++++++++++++++++++++++++++++ editor/tests/step322_test.cpp | 296 ++++++++++++++++++++++++++++++++++ progress.md | 20 +++ 4 files changed, 580 insertions(+) create mode 100644 editor/src/WorkflowState.h create mode 100644 editor/tests/step322_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4aec9dd..f8fde2c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1926,4 +1926,9 @@ add_executable(step321_test tests/step321_test.cpp) target_include_directories(step321_test PRIVATE src) target_link_libraries(step321_test PRIVATE nlohmann_json::nlohmann_json) +# Step 322: WorkflowState — Project-Level Workflow Tracking +add_executable(step322_test tests/step322_test.cpp) +target_include_directories(step322_test PRIVATE src) +target_link_libraries(step322_test PRIVATE nlohmann_json::nlohmann_json) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/WorkflowState.h b/editor/src/WorkflowState.h new file mode 100644 index 0000000..cb2672b --- /dev/null +++ b/editor/src/WorkflowState.h @@ -0,0 +1,259 @@ +#pragma once +// Step 322: WorkflowState — Project-Level Workflow Tracking +// +// Top-level state managing the full workflow lifecycle for a project: from +// skeleton creation through routing, execution, review, and completion. + +#include "TaskQueue.h" +#include "SkeletonAST.h" +#include +#include +#include + +// --- WorkflowPhase --- + +enum class WorkflowPhase { + Modeling, // architect is creating/annotating skeletons + Routing, // routing engine is assigning worker types + Executing, // workers are producing results + Reviewing, // human review in progress + Complete // all work items complete +}; + +inline std::string workflowPhaseToString(WorkflowPhase p) { + switch (p) { + case WorkflowPhase::Modeling: return "modeling"; + case WorkflowPhase::Routing: return "routing"; + case WorkflowPhase::Executing: return "executing"; + case WorkflowPhase::Reviewing: return "reviewing"; + case WorkflowPhase::Complete: return "complete"; + } + return "unknown"; +} + +inline WorkflowPhase workflowPhaseFromString(const std::string& s) { + if (s == "modeling") return WorkflowPhase::Modeling; + if (s == "routing") return WorkflowPhase::Routing; + if (s == "executing") return WorkflowPhase::Executing; + if (s == "reviewing") return WorkflowPhase::Reviewing; + if (s == "complete") return WorkflowPhase::Complete; + return WorkflowPhase::Modeling; +} + +// --- StatusChange: audit trail entry --- + +struct StatusChange { + std::string itemId; + std::string fromStatus; + std::string toStatus; + std::string timestamp; + std::string actor; + std::string note; + + json toJson() const { + return json{ + {"itemId", itemId}, {"fromStatus", fromStatus}, + {"toStatus", toStatus}, {"timestamp", timestamp}, + {"actor", actor}, {"note", note} + }; + } + + static StatusChange fromJson(const json& j) { + StatusChange sc; + if (j.contains("itemId")) sc.itemId = j["itemId"].get(); + if (j.contains("fromStatus")) sc.fromStatus = j["fromStatus"].get(); + if (j.contains("toStatus")) sc.toStatus = j["toStatus"].get(); + if (j.contains("timestamp")) sc.timestamp = j["timestamp"].get(); + if (j.contains("actor")) sc.actor = j["actor"].get(); + if (j.contains("note")) sc.note = j["note"].get(); + return sc; + } +}; + +// --- WorkflowStats --- + +struct WorkflowStats { + int total = 0; + int pending = 0; + int ready = 0; + int assigned = 0; + int inProgress = 0; + int review = 0; + int complete = 0; + int rejected = 0; + std::map byWorkerType; + std::map byPriority; + float completionPercent = 0.0f; + + json toJson() const { + json j; + j["total"] = total; + j["pending"] = pending; + j["ready"] = ready; + j["assigned"] = assigned; + j["inProgress"] = inProgress; + j["review"] = review; + j["complete"] = complete; + j["rejected"] = rejected; + j["byWorkerType"] = byWorkerType; + j["byPriority"] = byPriority; + j["completionPercent"] = completionPercent; + return j; + } +}; + +// --- WorkflowState --- + +class WorkflowState { +public: + TaskQueue queue; + std::string projectName; + std::string createdAt; + + WorkflowState() : createdAt(workItemTimestamp()) {} + + explicit WorkflowState(const std::string& name) + : projectName(name), createdAt(workItemTimestamp()) {} + + // Walk skeleton AST, create WorkItems, enqueue all. Returns count. + int populateFromSkeleton(const ASTNode* module, const std::string& bufferId) { + auto tasks = skeletonToTaskList(module); + int count = 0; + for (const auto& task : tasks) { + WorkItem wi = createWorkItem(task, bufferId); + recordChange(wi.id, "", WI_PENDING, "system", "created from skeleton"); + queue.enqueue(wi); + ++count; + } + return count; + } + + // Get workflow statistics + WorkflowStats getStats() const { + WorkflowStats stats; + stats.total = queue.size(); + + auto count = [&](const std::string& status) { + return static_cast(queue.getByStatus(status).size()); + }; + stats.pending = count(WI_PENDING); + stats.ready = count(WI_READY); + stats.assigned = count(WI_ASSIGNED); + stats.inProgress = count(WI_IN_PROGRESS); + stats.review = count(WI_REVIEW); + stats.complete = count(WI_COMPLETE); + stats.rejected = count(WI_REJECTED); + + // Aggregate by worker type and priority + for (const auto& status : {WI_PENDING, WI_READY, WI_ASSIGNED, WI_IN_PROGRESS, + WI_REVIEW, WI_COMPLETE, WI_REJECTED}) { + for (const auto& item : queue.getByStatus(status)) { + if (!item.workerType.empty()) stats.byWorkerType[item.workerType]++; + if (!item.priority.empty()) stats.byPriority[item.priority]++; + } + } + + if (stats.total > 0) { + stats.completionPercent = 100.0f * static_cast(stats.complete) + / static_cast(stats.total); + } + return stats; + } + + // Auto-compute phase from item statuses + WorkflowPhase getPhase() const { + if (queue.size() == 0) return WorkflowPhase::Modeling; + + auto stats = getStats(); + + // All complete → Complete + if (stats.complete == stats.total) return WorkflowPhase::Complete; + + // Any in review → Reviewing + if (stats.review > 0) return WorkflowPhase::Reviewing; + + // Any assigned or in-progress → Executing + if (stats.assigned > 0 || stats.inProgress > 0) return WorkflowPhase::Executing; + + // Any ready (routed) → Routing + if (stats.ready > 0) return WorkflowPhase::Routing; + + // All pending → Modeling + return WorkflowPhase::Modeling; + } + + // Audit trail for a specific item + std::vector getHistory(const std::string& itemId) const { + std::vector result; + for (const auto& sc : history_) { + if (sc.itemId == itemId) result.push_back(sc); + } + return result; + } + + // Record a status change in the audit trail + void recordChange(const std::string& itemId, const std::string& from, + const std::string& to, const std::string& actor, + const std::string& note = "") { + StatusChange sc; + sc.itemId = itemId; + sc.fromStatus = from; + sc.toStatus = to; + sc.timestamp = workItemTimestamp(); + sc.actor = actor; + sc.note = note; + history_.push_back(sc); + } + + // Full state serialization + json toJson() const { + json j; + j["projectName"] = projectName; + j["createdAt"] = createdAt; + j["phase"] = workflowPhaseToString(getPhase()); + + // Serialize all items from the queue + json items = json::array(); + for (const auto& status : {WI_PENDING, WI_READY, WI_ASSIGNED, WI_IN_PROGRESS, + WI_REVIEW, WI_COMPLETE, WI_REJECTED}) { + for (const auto& item : queue.getByStatus(status)) { + items.push_back(workItemToJson(item)); + } + } + j["items"] = items; + + // Serialize history + json hist = json::array(); + for (const auto& sc : history_) { + hist.push_back(sc.toJson()); + } + j["history"] = hist; + + j["stats"] = getStats().toJson(); + return j; + } + + // Deserialize from JSON + static WorkflowState fromJson(const json& j) { + WorkflowState ws; + if (j.contains("projectName")) ws.projectName = j["projectName"].get(); + if (j.contains("createdAt")) ws.createdAt = j["createdAt"].get(); + + if (j.contains("items")) { + for (const auto& itemJ : j["items"]) { + WorkItem wi = workItemFromJson(itemJ); + ws.queue.enqueue(wi); + } + } + + if (j.contains("history")) { + for (const auto& scJ : j["history"]) { + ws.history_.push_back(StatusChange::fromJson(scJ)); + } + } + return ws; + } + +private: + std::vector history_; +}; diff --git a/editor/tests/step322_test.cpp b/editor/tests/step322_test.cpp new file mode 100644 index 0000000..144da8b --- /dev/null +++ b/editor/tests/step322_test.cpp @@ -0,0 +1,296 @@ +// Step 322: WorkflowState — Project-Level Workflow Tracking (12 tests) +// Tests populateFromSkeleton, phase auto-detection, stats, history, +// JSON roundtrip, empty workflow, phase transitions. + +#include "WorkflowState.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 {} + +// Helper: build a skeleton module with N functions +static Module* makeSkeletonModule(const std::string& name, int funcCount) { + auto* mod = createSkeletonModule(name, "python"); + for (int i = 0; i < funcCount; ++i) { + addSkeletonFunction(mod, "func_" + std::to_string(i), {"x"}, "int"); + } + return mod; +} + +// 1. populateFromSkeleton creates correct item count +void test_populate_from_skeleton() { + TEST(populate_from_skeleton); + auto* mod = makeSkeletonModule("proj1", 5); + WorkflowState ws("proj1"); + int count = ws.populateFromSkeleton(mod, "buf-1"); + CHECK(count == 5, "5 items created, got " + std::to_string(count)); + CHECK(ws.queue.size() == 5, "queue has 5 items"); + delete mod; + PASS(); +} + +// 2. Phase auto-detection: empty → Modeling +void test_phase_empty() { + TEST(phase_empty); + WorkflowState ws("empty"); + CHECK(ws.getPhase() == WorkflowPhase::Modeling, "empty = Modeling"); + PASS(); +} + +// 3. Phase: all pending → Modeling +void test_phase_all_pending() { + TEST(phase_all_pending); + WorkflowState ws("proj"); + // Enqueue items with dependencies so they stay pending + WorkItem a; + a.id = "a"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function"; + a.priority = "medium"; a.status = WI_PENDING; + a.dependencies = {"external_dep"}; + a.createdAt = workItemTimestamp(); + ws.queue.enqueue(a); + + CHECK(ws.getPhase() == WorkflowPhase::Modeling, "all pending = Modeling"); + PASS(); +} + +// 4. Phase: some assigned → Executing +void test_phase_executing() { + TEST(phase_executing); + WorkflowState ws("proj"); + + WorkItem a; + a.id = "a1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function"; + a.priority = "medium"; a.status = WI_ASSIGNED; + a.createdAt = workItemTimestamp(); + ws.queue.enqueue(a); + + WorkItem b; + b.id = "b1"; b.nodeId = "n2"; b.nodeName = "f2"; b.nodeType = "Function"; + b.priority = "medium"; b.status = WI_READY; + b.createdAt = workItemTimestamp(); + ws.queue.enqueue(b); + + CHECK(ws.getPhase() == WorkflowPhase::Executing, "some assigned = Executing"); + PASS(); +} + +// 5. Phase: some in review → Reviewing +void test_phase_reviewing() { + TEST(phase_reviewing); + WorkflowState ws("proj"); + + WorkItem a; + a.id = "r1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function"; + a.priority = "medium"; a.status = WI_REVIEW; + a.createdAt = workItemTimestamp(); + ws.queue.enqueue(a); + + CHECK(ws.getPhase() == WorkflowPhase::Reviewing, "some in review = Reviewing"); + PASS(); +} + +// 6. Phase: all complete → Complete +void test_phase_complete() { + TEST(phase_complete); + WorkflowState ws("proj"); + + WorkItem a; + a.id = "c1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function"; + a.priority = "medium"; a.status = WI_COMPLETE; + a.createdAt = workItemTimestamp(); + ws.queue.enqueue(a); + + WorkItem b; + b.id = "c2"; b.nodeId = "n2"; b.nodeName = "f2"; b.nodeType = "Function"; + b.priority = "medium"; b.status = WI_COMPLETE; + b.createdAt = workItemTimestamp(); + ws.queue.enqueue(b); + + CHECK(ws.getPhase() == WorkflowPhase::Complete, "all complete = Complete"); + PASS(); +} + +// 7. Stats accuracy +void test_stats_accuracy() { + TEST(stats_accuracy); + WorkflowState ws("proj"); + + auto addItem = [&](const std::string& id, const std::string& status, + const std::string& wtype, const std::string& prio) { + WorkItem wi; + wi.id = id; wi.nodeId = "n_" + id; wi.nodeName = "f_" + id; + wi.nodeType = "Function"; wi.priority = prio; + wi.workerType = wtype; wi.status = status; + wi.createdAt = workItemTimestamp(); + ws.queue.enqueue(wi); + }; + + addItem("s1", WI_READY, "llm", "high"); + addItem("s2", WI_READY, "llm", "medium"); + addItem("s3", WI_COMPLETE, "deterministic", "low"); + addItem("s4", WI_ASSIGNED, "llm", "high"); + + auto stats = ws.getStats(); + CHECK(stats.total == 4, "total=4"); + CHECK(stats.ready == 2, "ready=2"); + CHECK(stats.complete == 1, "complete=1"); + CHECK(stats.assigned == 1, "assigned=1"); + CHECK(stats.byWorkerType["llm"] == 3, "llm=3"); + CHECK(stats.byWorkerType["deterministic"] == 1, "deterministic=1"); + CHECK(stats.byPriority["high"] == 2, "high=2"); + CHECK(stats.completionPercent > 24.0f && stats.completionPercent < 26.0f, "25%"); + PASS(); +} + +// 8. History tracking +void test_history_tracking() { + TEST(history_tracking); + WorkflowState ws("proj"); + ws.recordChange("item1", "", "pending", "system", "created"); + ws.recordChange("item1", "pending", "ready", "system", "deps met"); + ws.recordChange("item1", "ready", "assigned", "routing-engine", "routed to llm"); + + auto hist = ws.getHistory("item1"); + CHECK(hist.size() == 3, "3 history entries"); + CHECK(hist[0].fromStatus == "", "first from empty"); + CHECK(hist[0].toStatus == "pending", "first to pending"); + CHECK(hist[2].actor == "routing-engine", "third actor"); + + // Other item has no history + auto hist2 = ws.getHistory("item2"); + CHECK(hist2.empty(), "no history for item2"); + PASS(); +} + +// 9. JSON roundtrip — full state preserved +void test_json_roundtrip() { + TEST(json_roundtrip); + WorkflowState ws("myproject"); + + WorkItem a; + a.id = "j1"; a.nodeId = "n1"; a.nodeName = "compute"; a.nodeType = "Function"; + a.priority = "high"; a.workerType = "llm"; a.status = WI_READY; + a.createdAt = "2026-01-01T00:00:00Z"; + ws.queue.enqueue(a); + + WorkItem b; + b.id = "j2"; b.nodeId = "n2"; b.nodeName = "validate"; b.nodeType = "Function"; + b.priority = "medium"; b.workerType = "deterministic"; b.status = WI_COMPLETE; + b.createdAt = "2026-01-01T00:01:00Z"; + ws.queue.enqueue(b); + + ws.recordChange("j1", "", "ready", "system"); + ws.recordChange("j2", "", "complete", "worker:det-001"); + + json j = ws.toJson(); + WorkflowState ws2 = WorkflowState::fromJson(j); + + CHECK(ws2.projectName == "myproject", "projectName"); + CHECK(ws2.queue.size() == 2, "2 items"); + CHECK(ws2.getHistory("j1").size() == 1, "j1 history preserved, got " + + std::to_string(ws2.getHistory("j1").size())); + CHECK(ws2.getHistory("j2").size() == 1, "j2 history preserved"); + + auto item1 = ws2.queue.getItem("j1"); + CHECK(item1.has_value(), "j1 exists"); + CHECK(item1->nodeName == "compute", "j1 name"); + PASS(); +} + +// 10. Empty workflow behavior +void test_empty_workflow() { + TEST(empty_workflow); + WorkflowState ws("empty"); + auto stats = ws.getStats(); + CHECK(stats.total == 0, "total=0"); + CHECK(stats.completionPercent == 0.0f, "0% complete"); + CHECK(ws.getPhase() == WorkflowPhase::Modeling, "Modeling phase"); + CHECK(ws.getHistory("any").empty(), "no history"); + + json j = ws.toJson(); + CHECK(j.contains("projectName"), "has projectName"); + CHECK(j["items"].size() == 0, "no items"); + PASS(); +} + +// 11. Phase transitions through lifecycle +void test_phase_transitions() { + TEST(phase_transitions); + WorkflowState ws("proj"); + + // Start empty → Modeling + CHECK(ws.getPhase() == WorkflowPhase::Modeling, "start=Modeling"); + + // Add ready items → Routing + WorkItem a; + a.id = "pt1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function"; + a.priority = "medium"; a.status = WI_READY; + a.createdAt = workItemTimestamp(); + ws.queue.enqueue(a); + CHECK(ws.getPhase() == WorkflowPhase::Routing, "with ready=Routing"); + + // Assign → Executing + ws.queue.dequeue(); + CHECK(ws.getPhase() == WorkflowPhase::Executing, "with assigned=Executing"); + + // Move to review + WorkItem updated = *ws.queue.getItem("pt1"); + transitionWorkItem(updated, WI_IN_PROGRESS); + ws.queue.updateItem("pt1", updated); + updated = *ws.queue.getItem("pt1"); + transitionWorkItem(updated, WI_REVIEW); + ws.queue.updateItem("pt1", updated); + CHECK(ws.getPhase() == WorkflowPhase::Reviewing, "with review=Reviewing"); + + // Complete + ws.queue.complete("pt1"); + CHECK(ws.getPhase() == WorkflowPhase::Complete, "all complete=Complete"); + PASS(); +} + +// 12. populateFromSkeleton records history entries +void test_populate_records_history() { + TEST(populate_records_history); + auto* mod = makeSkeletonModule("proj2", 3); + WorkflowState ws("proj2"); + ws.populateFromSkeleton(mod, "buf-2"); + + // Each item should have a creation history entry + int histCount = 0; + for (const auto& status : {WI_PENDING, WI_READY}) { + for (const auto& item : ws.queue.getByStatus(status)) { + auto h = ws.getHistory(item.id); + if (!h.empty()) histCount++; + } + } + CHECK(histCount == 3, "3 items have history, got " + std::to_string(histCount)); + delete mod; + PASS(); +} + +int main() { + std::cout << "=== Step 322: WorkflowState Project-Level Workflow Tracking ===\n"; + try { + test_populate_from_skeleton(); + test_phase_empty(); + test_phase_all_pending(); + test_phase_executing(); + test_phase_reviewing(); + test_phase_complete(); + test_stats_accuracy(); + test_history_tracking(); + test_json_roundtrip(); + test_empty_workflow(); + test_phase_transitions(); + test_populate_records_history(); + } catch (const std::exception& e) { + std::cout << "EXCEPTION: " << e.what() << "\n"; + ++failed; + } + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/progress.md b/progress.md index 49ca594..616a159 100644 --- a/progress.md +++ b/progress.md @@ -1245,6 +1245,26 @@ by priority (critical first) then creation time. **Files modified:** - `editor/CMakeLists.txt` — step321_test target +### Step 322: WorkflowState — Project-Level Workflow Tracking +**Status:** PASS (12/12 tests) + +Top-level state managing the full workflow lifecycle for a project: from skeleton +creation through routing, execution, review, and completion. Auto-detects phase +from item statuses, tracks audit trail, computes stats. + +**Files created:** +- `editor/src/WorkflowState.h` — WorkflowPhase enum (Modeling/Routing/Executing/ + Reviewing/Complete), StatusChange audit trail struct, WorkflowStats struct, + WorkflowState class with populateFromSkeleton, getStats, getPhase (auto-computed), + getHistory, recordChange, toJson/fromJson +- `editor/tests/step322_test.cpp` — 12 tests: populate from skeleton, phase + auto-detection (empty, all pending, executing, reviewing, complete), stats + accuracy, history tracking, JSON roundtrip, empty workflow, phase transitions + through lifecycle, populate records history + +**Files modified:** +- `editor/CMakeLists.txt` — step322_test target + --- # Roadmap Planning — Sprints 12-25+