diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index eef66ea..996d995 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2407,4 +2407,13 @@ target_link_libraries(step391_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step392_test tests/step392_test.cpp) +target_include_directories(step392_test PRIVATE src) +target_link_libraries(step392_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/DependencyGraph.h b/editor/src/DependencyGraph.h new file mode 100644 index 0000000..c282ab2 --- /dev/null +++ b/editor/src/DependencyGraph.h @@ -0,0 +1,190 @@ +#pragma once +// Step 392: DependencyGraph — Export workflow dependency graph for visualization +// +// Builds a node/edge graph from WorkflowState. Computes critical path +// (longest dependency chain). Data format ready for Sprint 19's GUI. + +#include "WorkflowState.h" +#include "RoutingEngine.h" +#include "CostEstimator.h" +#include +#include +#include +#include +#include + +// --- GraphNode --- + +struct GraphNode { + std::string id; + std::string label; + std::string type; // "Function" | "Class" | "Method" + std::string status; // WorkItem status + std::string workerType; // routing decision + std::string priority; + + json toJson() const { + return json{{"id", id}, {"label", label}, {"type", type}, + {"status", status}, {"workerType", workerType}, {"priority", priority}}; + } + + static GraphNode fromJson(const json& j) { + GraphNode n; + if (j.contains("id")) n.id = j["id"].get(); + if (j.contains("label")) n.label = j["label"].get(); + if (j.contains("type")) n.type = j["type"].get(); + if (j.contains("status")) n.status = j["status"].get(); + if (j.contains("workerType")) n.workerType = j["workerType"].get(); + if (j.contains("priority")) n.priority = j["priority"].get(); + return n; + } +}; + +// --- GraphEdge --- + +struct GraphEdge { + std::string from; + std::string to; + std::string type; // "depends-on" | "blocks" + + json toJson() const { + return json{{"from", from}, {"to", to}, {"type", type}}; + } + + static GraphEdge fromJson(const json& j) { + GraphEdge e; + if (j.contains("from")) e.from = j["from"].get(); + if (j.contains("to")) e.to = j["to"].get(); + if (j.contains("type")) e.type = j["type"].get(); + return e; + } +}; + +// --- GraphData --- + +struct GraphData { + std::vector nodes; + std::vector edges; + + json toJson() const { + json nodeArr = json::array(); + for (const auto& n : nodes) nodeArr.push_back(n.toJson()); + json edgeArr = json::array(); + for (const auto& e : edges) edgeArr.push_back(e.toJson()); + return json{{"nodes", nodeArr}, {"edges", edgeArr}}; + } + + static GraphData fromJson(const json& j) { + GraphData g; + if (j.contains("nodes") && j["nodes"].is_array()) { + for (const auto& n : j["nodes"]) g.nodes.push_back(GraphNode::fromJson(n)); + } + if (j.contains("edges") && j["edges"].is_array()) { + for (const auto& e : j["edges"]) g.edges.push_back(GraphEdge::fromJson(e)); + } + return g; + } +}; + +// --- DependencyGraph builder --- + +inline GraphData buildDependencyGraph(const WorkflowState& workflow) { + GraphData graph; + auto allItems = collectAllItems(workflow.queue); + + for (const auto& item : allItems) { + GraphNode node; + node.id = item.id; + node.label = item.nodeName; + node.type = item.nodeType; + node.status = item.status; + node.workerType = item.workerType; + node.priority = item.priority; + graph.nodes.push_back(node); + + for (const auto& depId : item.dependencies) { + GraphEdge edge; + edge.from = depId; + edge.to = item.id; + edge.type = "depends-on"; + graph.edges.push_back(edge); + } + } + return graph; +} + +inline json graphToJson(const GraphData& graph) { + return graph.toJson(); +} + +// --- Critical path computation --- +// Returns the node IDs in the longest dependency chain. + +inline std::vector getCriticalPath(const GraphData& graph) { + // Build adjacency: from -> [to] + std::map> adj; + std::set allIds; + for (const auto& node : graph.nodes) allIds.insert(node.id); + for (const auto& edge : graph.edges) { + if (edge.type == "depends-on") { + adj[edge.from].push_back(edge.to); + } + } + + // Find roots (nodes with no incoming "depends-on" edges) + std::set hasIncoming; + for (const auto& edge : graph.edges) { + if (edge.type == "depends-on") hasIncoming.insert(edge.to); + } + + // DFS to find longest path from each root + std::map memo; + std::map parent; + + std::function longestFrom = [&](const std::string& id) -> int { + if (memo.count(id)) return memo[id]; + int best = 0; + std::string bestChild; + for (const auto& next : adj[id]) { + int childLen = longestFrom(next); + if (childLen + 1 > best) { + best = childLen + 1; + bestChild = next; + } + } + memo[id] = best; + if (!bestChild.empty()) parent[id] = bestChild; + return best; + }; + + // Find the longest path across all roots + std::string bestStart; + int bestLen = 0; + for (const auto& id : allIds) { + if (hasIncoming.find(id) == hasIncoming.end()) { + int len = longestFrom(id); + if (len >= bestLen) { + bestLen = len; + bestStart = id; + } + } + } + + // If no roots found (isolated nodes), just pick any node + if (bestStart.empty() && !allIds.empty()) { + bestStart = *allIds.begin(); + } + + // Reconstruct path + std::vector path; + if (!bestStart.empty()) { + std::string cur = bestStart; + path.push_back(cur); + while (parent.count(cur)) { + cur = parent[cur]; + path.push_back(cur); + } + } + + return path; +} diff --git a/editor/tests/step392_test.cpp b/editor/tests/step392_test.cpp new file mode 100644 index 0000000..32b319d --- /dev/null +++ b/editor/tests/step392_test.cpp @@ -0,0 +1,249 @@ +// Step 392: Dependency Graph Data (12 tests) + +#include +#include +#include +#include "DependencyGraph.h" + +static WorkItem makeItem(const std::string& id, + const std::string& name = "doWork", + const std::string& workerType = "slm", + const std::string& contextWidth = "local", + const std::string& priority = "medium", + const std::vector& deps = {}) { + WorkItem item; + item.id = id; + item.nodeId = id + "_node"; + item.nodeName = name; + item.nodeType = "Function"; + item.bufferId = "main.py"; + item.workerType = workerType; + item.contextWidth = contextWidth; + item.priority = priority; + item.status = WI_READY; + item.createdAt = workItemTimestamp(); + item.dependencies = deps; + return item; +} + +int main() { + int passed = 0; + + // Test 1: Graph has correct node count + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "funcA")); + wf.queue.enqueue(makeItem("t2", "funcB")); + wf.queue.enqueue(makeItem("t3", "funcC")); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes.size() == 3); + std::cout << "PASS: test 1 — graph has correct node count\n"; + passed++; + } + + // Test 2: Edges match dependencies + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "funcA")); + wf.queue.enqueue(makeItem("t2", "funcB", "slm", "local", "medium", {"t1"})); + wf.queue.enqueue(makeItem("t3", "funcC", "slm", "local", "medium", {"t2"})); + + auto graph = buildDependencyGraph(wf); + assert(graph.edges.size() == 2); + // t1 -> t2 + assert(graph.edges[0].from == "t1"); + assert(graph.edges[0].to == "t2"); + assert(graph.edges[0].type == "depends-on"); + // t2 -> t3 + assert(graph.edges[1].from == "t2"); + assert(graph.edges[1].to == "t3"); + std::cout << "PASS: test 2 — edges match dependencies\n"; + passed++; + } + + // Test 3: Critical path is longest chain + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "funcA")); + wf.queue.enqueue(makeItem("t2", "funcB", "slm", "local", "medium", {"t1"})); + wf.queue.enqueue(makeItem("t3", "funcC", "slm", "local", "medium", {"t2"})); + wf.queue.enqueue(makeItem("t4", "funcD")); // independent, shorter + + auto graph = buildDependencyGraph(wf); + auto path = getCriticalPath(graph); + assert(path.size() == 3); // t1 -> t2 -> t3 + assert(path[0] == "t1"); + assert(path[1] == "t2"); + assert(path[2] == "t3"); + std::cout << "PASS: test 3 — critical path is longest chain\n"; + passed++; + } + + // Test 4: Node status matches workflow state + { + WorkflowState wf("test"); + auto item = makeItem("t1", "funcA"); + item.status = WI_COMPLETE; + wf.queue.enqueue(item); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes.size() == 1); + assert(graph.nodes[0].status == WI_COMPLETE); + std::cout << "PASS: test 4 — node status matches workflow state\n"; + passed++; + } + + // Test 5: Worker type populated from routing + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "funcA", "llm")); + wf.queue.enqueue(makeItem("t2", "getName", "template")); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes[0].workerType == "llm"); + assert(graph.nodes[1].workerType == "template"); + std::cout << "PASS: test 5 — worker type populated\n"; + passed++; + } + + // Test 6: JSON serialization + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "funcA")); + wf.queue.enqueue(makeItem("t2", "funcB", "slm", "local", "medium", {"t1"})); + + auto graph = buildDependencyGraph(wf); + json j = graphToJson(graph); + + assert(j.contains("nodes")); + assert(j.contains("edges")); + assert(j["nodes"].size() == 2); + assert(j["edges"].size() == 1); + assert(j["nodes"][0]["id"] == "t1"); + + // Roundtrip + auto restored = GraphData::fromJson(j); + assert(restored.nodes.size() == 2); + assert(restored.edges.size() == 1); + std::cout << "PASS: test 6 — JSON serialization\n"; + passed++; + } + + // Test 7: Empty workflow produces empty graph + { + WorkflowState wf("test"); + auto graph = buildDependencyGraph(wf); + assert(graph.nodes.empty()); + assert(graph.edges.empty()); + + auto path = getCriticalPath(graph); + assert(path.empty()); + std::cout << "PASS: test 7 — empty workflow produces empty graph\n"; + passed++; + } + + // Test 8: Single-node graph + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "only")); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes.size() == 1); + assert(graph.edges.empty()); + + auto path = getCriticalPath(graph); + assert(path.size() == 1); + assert(path[0] == "t1"); + std::cout << "PASS: test 8 — single-node graph\n"; + passed++; + } + + // Test 9: Complex graph (diamond dependencies) + { + // t1 + // / \. + // t2 t3 + // \ / + // t4 + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "root")); + wf.queue.enqueue(makeItem("t2", "left", "slm", "local", "medium", {"t1"})); + wf.queue.enqueue(makeItem("t3", "right", "slm", "local", "medium", {"t1"})); + wf.queue.enqueue(makeItem("t4", "merge", "slm", "local", "medium", {"t2", "t3"})); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes.size() == 4); + assert(graph.edges.size() == 4); // t1->t2, t1->t3, t2->t4, t3->t4 + + auto path = getCriticalPath(graph); + // Longest path: t1 -> t2 -> t4 or t1 -> t3 -> t4 (both length 3) + assert(path.size() == 3); + assert(path[0] == "t1"); + assert(path[2] == "t4"); + std::cout << "PASS: test 9 — complex graph (diamond dependencies)\n"; + passed++; + } + + // Test 10: Graph updates after task completion + { + WorkflowState wf("test"); + auto item1 = makeItem("t1", "funcA"); + auto item2 = makeItem("t2", "funcB", "slm", "local", "medium", {"t1"}); + wf.queue.enqueue(item1); + wf.queue.enqueue(item2); + + auto graph1 = buildDependencyGraph(wf); + assert(graph1.nodes[0].status == WI_READY); + + // Complete t1 + item1.status = WI_COMPLETE; + wf.queue.updateItem("t1", item1); + + auto graph2 = buildDependencyGraph(wf); + bool t1Complete = false; + for (const auto& n : graph2.nodes) { + if (n.id == "t1" && n.status == WI_COMPLETE) t1Complete = true; + } + assert(t1Complete); + std::cout << "PASS: test 10 — graph updates after task completion\n"; + passed++; + } + + // Test 11: Node label and type from work item + { + WorkflowState wf("test"); + auto item = makeItem("t1", "myFunction"); + item.nodeType = "Method"; + item.priority = "critical"; + wf.queue.enqueue(item); + + auto graph = buildDependencyGraph(wf); + assert(graph.nodes[0].label == "myFunction"); + assert(graph.nodes[0].type == "Method"); + assert(graph.nodes[0].priority == "critical"); + std::cout << "PASS: test 11 — node label and type from work item\n"; + passed++; + } + + // Test 12: 5-function chain critical path = 5 + { + WorkflowState wf("test"); + wf.queue.enqueue(makeItem("t1", "step1")); + wf.queue.enqueue(makeItem("t2", "step2", "slm", "local", "medium", {"t1"})); + wf.queue.enqueue(makeItem("t3", "step3", "slm", "local", "medium", {"t2"})); + wf.queue.enqueue(makeItem("t4", "step4", "slm", "local", "medium", {"t3"})); + wf.queue.enqueue(makeItem("t5", "step5", "slm", "local", "medium", {"t4"})); + + auto graph = buildDependencyGraph(wf); + auto path = getCriticalPath(graph); + assert(path.size() == 5); + assert(path[0] == "t1"); + assert(path[4] == "t5"); + std::cout << "PASS: test 12 — 5-function chain critical path\n"; + passed++; + } + + std::cout << "\nStep 392 result: " << passed << "/12 tests passed\n"; + return (passed == 12) ? 0 : 1; +}