Step 585: add workflow visualization v2
This commit is contained in:
@@ -4144,4 +4144,13 @@ target_link_libraries(step584_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step585_test tests/step585_test.cpp)
|
||||
target_include_directories(step585_test PRIVATE src)
|
||||
target_link_libraries(step585_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)
|
||||
|
||||
118
editor/src/WorkflowVisualizationV2.h
Normal file
118
editor/src/WorkflowVisualizationV2.h
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
// Step 585: Workflow Visualization 2.0
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class WorkflowNodeKind {
|
||||
Intake,
|
||||
ReviewGate,
|
||||
TaskQueue,
|
||||
Execution,
|
||||
Verification
|
||||
};
|
||||
|
||||
struct WorkflowNodeVisual {
|
||||
std::string nodeId;
|
||||
WorkflowNodeKind kind = WorkflowNodeKind::Intake;
|
||||
std::string label;
|
||||
std::string accentColorHex = "#2F7BFF"; // electric-blue baseline
|
||||
};
|
||||
|
||||
struct WorkflowEdgeVisual {
|
||||
std::string edgeId;
|
||||
std::string fromNodeId;
|
||||
std::string toNodeId;
|
||||
std::string routingReason;
|
||||
};
|
||||
|
||||
struct WorkflowVisualizationGraph {
|
||||
std::vector<WorkflowNodeVisual> nodes;
|
||||
std::vector<WorkflowEdgeVisual> edges;
|
||||
};
|
||||
|
||||
class WorkflowVisualizationV2 {
|
||||
public:
|
||||
static bool addNode(WorkflowVisualizationGraph* graph,
|
||||
const WorkflowNodeVisual& node,
|
||||
std::string* error) {
|
||||
if (!graph || !error) return false;
|
||||
error->clear();
|
||||
if (node.nodeId.empty()) return fail(error, "node_id_missing");
|
||||
if (node.label.empty()) return fail(error, "node_label_missing");
|
||||
if (!isValidColor(node.accentColorHex)) return fail(error, "node_color_invalid");
|
||||
if (findNode(*graph, node.nodeId) != nullptr) return fail(error, "node_duplicate");
|
||||
graph->nodes.push_back(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addEdge(WorkflowVisualizationGraph* graph,
|
||||
const WorkflowEdgeVisual& edge,
|
||||
std::string* error) {
|
||||
if (!graph || !error) return false;
|
||||
error->clear();
|
||||
if (edge.edgeId.empty()) return fail(error, "edge_id_missing");
|
||||
if (edge.fromNodeId.empty() || edge.toNodeId.empty()) return fail(error, "edge_endpoint_missing");
|
||||
if (edge.routingReason.empty()) return fail(error, "edge_reason_missing");
|
||||
if (findEdge(*graph, edge.edgeId) != nullptr) return fail(error, "edge_duplicate");
|
||||
if (!findNode(*graph, edge.fromNodeId) || !findNode(*graph, edge.toNodeId)) {
|
||||
return fail(error, "edge_node_missing");
|
||||
}
|
||||
graph->edges.push_back(edge);
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<WorkflowEdgeVisual> outgoing(const WorkflowVisualizationGraph& graph,
|
||||
const std::string& nodeId) {
|
||||
std::vector<WorkflowEdgeVisual> out;
|
||||
for (const auto& edge : graph.edges) if (edge.fromNodeId == nodeId) out.push_back(edge);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::vector<std::string> plainLanguageRoutingSummary(
|
||||
const WorkflowVisualizationGraph& graph) {
|
||||
std::vector<std::string> lines;
|
||||
for (const auto& edge : graph.edges) {
|
||||
lines.push_back(edge.fromNodeId + " -> " + edge.toNodeId + ": " + edge.routingReason);
|
||||
}
|
||||
std::stable_sort(lines.begin(), lines.end());
|
||||
return lines;
|
||||
}
|
||||
|
||||
static std::vector<WorkflowNodeVisual> nodesByKind(const WorkflowVisualizationGraph& graph,
|
||||
WorkflowNodeKind kind) {
|
||||
std::vector<WorkflowNodeVisual> out;
|
||||
for (const auto& node : graph.nodes) if (node.kind == kind) out.push_back(node);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool fail(std::string* error, const char* code) {
|
||||
*error = code;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool isValidColor(const std::string& c) {
|
||||
if (c.size() != 7 || c[0] != '#') return false;
|
||||
for (size_t i = 1; i < c.size(); ++i) {
|
||||
const char x = c[i];
|
||||
const bool hex = (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F');
|
||||
if (!hex) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static const WorkflowNodeVisual* findNode(const WorkflowVisualizationGraph& graph,
|
||||
const std::string& nodeId) {
|
||||
for (const auto& node : graph.nodes) if (node.nodeId == nodeId) return &node;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const WorkflowEdgeVisual* findEdge(const WorkflowVisualizationGraph& graph,
|
||||
const std::string& edgeId) {
|
||||
for (const auto& edge : graph.edges) if (edge.edgeId == edgeId) return &edge;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
177
editor/tests/step585_test.cpp
Normal file
177
editor/tests/step585_test.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
// Step 585: Workflow Visualization 2.0 (12 tests)
|
||||
|
||||
#include "WorkflowVisualizationV2.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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 WorkflowNodeVisual node(const std::string& id, WorkflowNodeKind kind, const std::string& label) {
|
||||
WorkflowNodeVisual n;
|
||||
n.nodeId = id;
|
||||
n.kind = kind;
|
||||
n.label = label;
|
||||
n.accentColorHex = "#2F7BFF";
|
||||
return n;
|
||||
}
|
||||
|
||||
void test_add_node_success() {
|
||||
TEST(add_node_success);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("intake", WorkflowNodeKind::Intake, "Intake"), &error), "add node should succeed");
|
||||
CHECK(graph.nodes.size() == 1, "node count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_node_rejects_missing_id() {
|
||||
TEST(add_node_rejects_missing_id);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
auto n = node("", WorkflowNodeKind::Intake, "Intake");
|
||||
CHECK(!WorkflowVisualizationV2::addNode(&graph, n, &error), "add node should fail");
|
||||
CHECK(error == "node_id_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_node_rejects_invalid_color() {
|
||||
TEST(add_node_rejects_invalid_color);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
auto n = node("intake", WorkflowNodeKind::Intake, "Intake");
|
||||
n.accentColorHex = "blue";
|
||||
CHECK(!WorkflowVisualizationV2::addNode(&graph, n, &error), "add node should fail");
|
||||
CHECK(error == "node_color_invalid", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_node_rejects_duplicate() {
|
||||
TEST(add_node_rejects_duplicate);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("intake", WorkflowNodeKind::Intake, "Intake"), &error), "first add failed");
|
||||
CHECK(!WorkflowVisualizationV2::addNode(&graph, node("intake", WorkflowNodeKind::Intake, "Intake2"), &error), "duplicate should fail");
|
||||
CHECK(error == "node_duplicate", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_edge_success() {
|
||||
TEST(add_edge_success);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("a", WorkflowNodeKind::Intake, "A"), &error), "add A failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("b", WorkflowNodeKind::ReviewGate, "B"), &error), "add B failed");
|
||||
WorkflowEdgeVisual e{"edge-1", "a", "b", "needs review due to ambiguity"};
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, e, &error), "add edge should succeed");
|
||||
CHECK(graph.edges.size() == 1, "edge count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_edge_rejects_missing_node_reference() {
|
||||
TEST(add_edge_rejects_missing_node_reference);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("a", WorkflowNodeKind::Intake, "A"), &error), "add A failed");
|
||||
WorkflowEdgeVisual e{"edge-1", "a", "missing", "reason"};
|
||||
CHECK(!WorkflowVisualizationV2::addEdge(&graph, e, &error), "add edge should fail");
|
||||
CHECK(error == "edge_node_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_edge_rejects_duplicate_edge_id() {
|
||||
TEST(add_edge_rejects_duplicate_edge_id);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("a", WorkflowNodeKind::Intake, "A"), &error), "add A failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("b", WorkflowNodeKind::ReviewGate, "B"), &error), "add B failed");
|
||||
WorkflowEdgeVisual e{"edge-1", "a", "b", "reason"};
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, e, &error), "first edge add failed");
|
||||
CHECK(!WorkflowVisualizationV2::addEdge(&graph, e, &error), "duplicate edge should fail");
|
||||
CHECK(error == "edge_duplicate", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_outgoing_filters_by_from_node() {
|
||||
TEST(outgoing_filters_by_from_node);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("a", WorkflowNodeKind::Intake, "A"), &error), "add A failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("b", WorkflowNodeKind::ReviewGate, "B"), &error), "add B failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("c", WorkflowNodeKind::TaskQueue, "C"), &error), "add C failed");
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, {"e1", "a", "b", "r1"}, &error), "add e1 failed");
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, {"e2", "a", "c", "r2"}, &error), "add e2 failed");
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, {"e3", "b", "c", "r3"}, &error), "add e3 failed");
|
||||
const auto out = WorkflowVisualizationV2::outgoing(graph, "a");
|
||||
CHECK(out.size() == 2, "outgoing count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_routing_summary_emits_plain_language_lines() {
|
||||
TEST(routing_summary_emits_plain_language_lines);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("intake", WorkflowNodeKind::Intake, "Intake"), &error), "add intake failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("review", WorkflowNodeKind::ReviewGate, "Review"), &error), "add review failed");
|
||||
CHECK(WorkflowVisualizationV2::addEdge(&graph, {"e1", "intake", "review", "ambiguity requires architect review"}, &error), "add edge failed");
|
||||
const auto lines = WorkflowVisualizationV2::plainLanguageRoutingSummary(graph);
|
||||
CHECK(lines.size() == 1, "summary size mismatch");
|
||||
CHECK(lines[0].find("ambiguity requires architect review") != std::string::npos, "summary reason missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_nodes_by_kind_filters_review_gates() {
|
||||
TEST(nodes_by_kind_filters_review_gates);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("intake", WorkflowNodeKind::Intake, "Intake"), &error), "add intake failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("review1", WorkflowNodeKind::ReviewGate, "Review1"), &error), "add review1 failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("review2", WorkflowNodeKind::ReviewGate, "Review2"), &error), "add review2 failed");
|
||||
const auto reviewNodes = WorkflowVisualizationV2::nodesByKind(graph, WorkflowNodeKind::ReviewGate);
|
||||
CHECK(reviewNodes.size() == 2, "review node count mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_electric_blue_default_is_preserved() {
|
||||
TEST(electric_blue_default_is_preserved);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
auto n = node("intake", WorkflowNodeKind::Intake, "Intake");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, n, &error), "add node failed");
|
||||
CHECK(graph.nodes[0].accentColorHex == "#2F7BFF", "default electric-blue mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_edge_rejects_missing_reason() {
|
||||
TEST(add_edge_rejects_missing_reason);
|
||||
WorkflowVisualizationGraph graph;
|
||||
std::string error;
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("a", WorkflowNodeKind::Intake, "A"), &error), "add A failed");
|
||||
CHECK(WorkflowVisualizationV2::addNode(&graph, node("b", WorkflowNodeKind::ReviewGate, "B"), &error), "add B failed");
|
||||
CHECK(!WorkflowVisualizationV2::addEdge(&graph, {"e1", "a", "b", ""}, &error), "edge should fail");
|
||||
CHECK(error == "edge_reason_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 585: Workflow Visualization 2.0\n";
|
||||
|
||||
test_add_node_success(); // 1
|
||||
test_add_node_rejects_missing_id(); // 2
|
||||
test_add_node_rejects_invalid_color(); // 3
|
||||
test_add_node_rejects_duplicate(); // 4
|
||||
test_add_edge_success(); // 5
|
||||
test_add_edge_rejects_missing_node_reference(); // 6
|
||||
test_add_edge_rejects_duplicate_edge_id(); // 7
|
||||
test_outgoing_filters_by_from_node(); // 8
|
||||
test_routing_summary_emits_plain_language_lines();// 9
|
||||
test_nodes_by_kind_filters_review_gates(); // 10
|
||||
test_electric_blue_default_is_preserved(); // 11
|
||||
test_add_edge_rejects_missing_reason(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
35
progress.md
35
progress.md
@@ -10985,3 +10985,38 @@ constrained execution safeguards, verification, and routing transparency.
|
||||
- `editor/src/ValueForwardOnboardingFlow.h` within header-size limit (`105` <= `600`)
|
||||
- `editor/tests/step584_test.cpp` within test-file size guidance (`154` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 585: Workflow Visualization 2.0
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements workflow visualization state for long-range dependency links and
|
||||
plain-language routing/review explanations, with electric-blue visual defaults.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/WorkflowVisualizationV2.h` - workflow visualization module:
|
||||
- visualization node/edge schemas
|
||||
- node/edge insertion validation and duplicate guards
|
||||
- outgoing edge queries for dependency tracing
|
||||
- plain-language routing summary generation
|
||||
- node filtering by workflow role
|
||||
- `editor/tests/step585_test.cpp` - 12 tests covering:
|
||||
- node/edge creation success/failure behavior
|
||||
- duplicate/id/color/endpoint guard behavior
|
||||
- outgoing dependency query behavior
|
||||
- routing summary explanation behavior
|
||||
- review-gate filtering behavior
|
||||
- electric-blue default preservation behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step585_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step585_test step584_test` - PASS
|
||||
- `./editor/build-native/step585_test` - PASS (12/12)
|
||||
- `./editor/build-native/step584_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/WorkflowVisualizationV2.h` within header-size limit (`118` <= `600`)
|
||||
- `editor/tests/step585_test.cpp` within test-file size guidance (`177` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user