Step 392: add dependency graph data and critical path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
190
editor/src/DependencyGraph.h
Normal file
190
editor/src/DependencyGraph.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <algorithm>
|
||||
|
||||
// --- 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<std::string>();
|
||||
if (j.contains("label")) n.label = j["label"].get<std::string>();
|
||||
if (j.contains("type")) n.type = j["type"].get<std::string>();
|
||||
if (j.contains("status")) n.status = j["status"].get<std::string>();
|
||||
if (j.contains("workerType")) n.workerType = j["workerType"].get<std::string>();
|
||||
if (j.contains("priority")) n.priority = j["priority"].get<std::string>();
|
||||
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<std::string>();
|
||||
if (j.contains("to")) e.to = j["to"].get<std::string>();
|
||||
if (j.contains("type")) e.type = j["type"].get<std::string>();
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
// --- GraphData ---
|
||||
|
||||
struct GraphData {
|
||||
std::vector<GraphNode> nodes;
|
||||
std::vector<GraphEdge> 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<std::string> getCriticalPath(const GraphData& graph) {
|
||||
// Build adjacency: from -> [to]
|
||||
std::map<std::string, std::vector<std::string>> adj;
|
||||
std::set<std::string> 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<std::string> 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<std::string, int> memo;
|
||||
std::map<std::string, std::string> parent;
|
||||
|
||||
std::function<int(const std::string&)> 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<std::string> 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;
|
||||
}
|
||||
249
editor/tests/step392_test.cpp
Normal file
249
editor/tests/step392_test.cpp
Normal file
@@ -0,0 +1,249 @@
|
||||
// Step 392: Dependency Graph Data (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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<std::string>& 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;
|
||||
}
|
||||
Reference in New Issue
Block a user