From 4d8686cb7c63684ce9e6011babe81df6485e9caf Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 17:07:21 -0700 Subject: [PATCH] Step 428: add workflow task board model --- editor/CMakeLists.txt | 9 ++ editor/src/WorkflowTaskBoard.h | 154 +++++++++++++++++++++++++++ editor/tests/step428_test.cpp | 187 +++++++++++++++++++++++++++++++++ progress.md | 48 +++++++++ 4 files changed, 398 insertions(+) create mode 100644 editor/src/WorkflowTaskBoard.h create mode 100644 editor/tests/step428_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 451f7e5..b0ca59d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2731,4 +2731,13 @@ target_link_libraries(step427_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step428_test tests/step428_test.cpp) +target_include_directories(step428_test PRIVATE src) +target_link_libraries(step428_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/WorkflowTaskBoard.h b/editor/src/WorkflowTaskBoard.h new file mode 100644 index 0000000..43a1918 --- /dev/null +++ b/editor/src/WorkflowTaskBoard.h @@ -0,0 +1,154 @@ +#pragma once + +#include "WorkflowState.h" + +#include +#include +#include +#include + +struct TaskBoardFilter { + std::string workerType; + std::string priority; + std::string language; + std::string fileContains; +}; + +struct TaskCard { + std::string itemId; + std::string title; + std::string workerType; + std::string workerIcon; + std::string priority; + std::string language; + std::string file; + std::string status; + std::vector tags; +}; + +struct TaskBoardColumn { + std::string key; + std::string label; + std::vector cards; +}; + +class WorkflowTaskBoard { +public: + static std::vector build(const WorkflowState& wf, + const TaskBoardFilter& filter = {}) { + std::map cols; + cols["pending"] = {"pending", "Pending", {}}; + cols["ready"] = {"ready", "Ready", {}}; + cols["in-progress"] = {"in-progress", "In Progress", {}}; + cols["review"] = {"review", "Review", {}}; + cols["complete"] = {"complete", "Complete", {}}; + + for (const auto& status : {WI_PENDING, WI_READY, WI_ASSIGNED, WI_IN_PROGRESS, + WI_REVIEW, WI_COMPLETE, WI_REJECTED}) { + for (const auto& wi : wf.queue.getByStatus(status)) { + TaskCard card = toCard(wi); + if (!matchesFilter(card, filter)) continue; + std::string col = normalizeColumnKey(wi.status); + if (cols.find(col) == cols.end()) continue; + cols[col].cards.push_back(card); + } + } + + std::vector out; + for (const auto& k : {"pending", "ready", "in-progress", "review", "complete"}) { + auto& c = cols[k]; + std::sort(c.cards.begin(), c.cards.end(), + [](const TaskCard& a, const TaskCard& b) { + if (priorityToInt(a.priority) != priorityToInt(b.priority)) { + return priorityToInt(a.priority) < priorityToInt(b.priority); + } + return a.itemId < b.itemId; + }); + out.push_back(c); + } + return out; + } + + static bool moveItem(WorkflowState& wf, const std::string& itemId, + const std::string& targetColumn) { + auto itemOpt = wf.queue.getItem(itemId); + if (!itemOpt.has_value()) return false; + std::string status = columnToStatus(targetColumn); + if (status.empty()) return false; + WorkItem item = *itemOpt; + item.status = status; + return wf.queue.updateItem(itemId, item); + } + +private: + static TaskCard toCard(const WorkItem& wi) { + TaskCard c; + c.itemId = wi.id; + c.title = wi.nodeName.empty() ? wi.nodeId : wi.nodeName; + c.workerType = wi.workerType; + c.workerIcon = iconForWorker(wi.workerType); + c.priority = wi.priority; + c.language = languageFromPath(wi.bufferId); + c.file = wi.bufferId; + c.status = normalizeColumnKey(wi.status); + c.tags = tagsForItem(wi); + return c; + } + + static std::vector tagsForItem(const WorkItem& wi) { + std::vector tags; + if (!wi.contextWidth.empty()) tags.push_back("ctx:" + wi.contextWidth); + if (wi.reviewRequired) tags.push_back("review"); + if (!wi.dependencies.empty()) tags.push_back("deps:" + std::to_string(wi.dependencies.size())); + return tags; + } + + static std::string normalizeColumnKey(const std::string& status) { + if (status == WI_ASSIGNED || status == WI_IN_PROGRESS) return "in-progress"; + if (status == WI_REJECTED) return "pending"; + if (status == WI_PENDING) return "pending"; + if (status == WI_READY) return "ready"; + if (status == WI_REVIEW) return "review"; + if (status == WI_COMPLETE) return "complete"; + return ""; + } + + static std::string columnToStatus(const std::string& column) { + if (column == "pending") return WI_PENDING; + if (column == "ready") return WI_READY; + if (column == "in-progress") return WI_IN_PROGRESS; + if (column == "review") return WI_REVIEW; + if (column == "complete") return WI_COMPLETE; + return ""; + } + + static std::string iconForWorker(const std::string& worker) { + if (worker == "deterministic" || worker == "template") return "D"; + if (worker == "slm") return "S"; + if (worker == "llm") return "L"; + if (worker == "human") return "H"; + return "?"; + } + + static std::string languageFromPath(const std::string& path) { + auto dot = path.find_last_of('.'); + if (dot == std::string::npos) return ""; + std::string ext = path.substr(dot); + if (ext == ".cpp" || ext == ".cc" || ext == ".hpp" || ext == ".h") return "cpp"; + if (ext == ".py") return "python"; + if (ext == ".js") return "javascript"; + if (ext == ".ts") return "typescript"; + if (ext == ".java") return "java"; + if (ext == ".rs") return "rust"; + if (ext == ".go") return "go"; + return ""; + } + + static bool matchesFilter(const TaskCard& card, const TaskBoardFilter& f) { + if (!f.workerType.empty() && card.workerType != f.workerType) return false; + if (!f.priority.empty() && card.priority != f.priority) return false; + if (!f.language.empty() && card.language != f.language) return false; + if (!f.fileContains.empty() && card.file.find(f.fileContains) == std::string::npos) return false; + return true; + } +}; diff --git a/editor/tests/step428_test.cpp b/editor/tests/step428_test.cpp new file mode 100644 index 0000000..2893b55 --- /dev/null +++ b/editor/tests/step428_test.cpp @@ -0,0 +1,187 @@ +// Step 428: Kanban-Style Task Board Tests (12 tests) + +#include "WorkflowTaskBoard.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& status, + const std::string& worker, const std::string& priority, + const std::string& file, const std::string& name) { + WorkItem wi; + wi.id = id; + wi.status = status; + wi.workerType = worker; + wi.priority = priority; + wi.bufferId = file; + wi.nodeName = name; + wi.nodeId = id + "_node"; + wi.contextWidth = "file"; + wi.reviewRequired = (status == WI_REVIEW); + if (status == WI_PENDING) wi.dependencies = {"dep-blocker"}; + return wi; +} + +static WorkflowState makeWorkflow() { + WorkflowState wf("task-board"); + wf.queue.enqueue(makeItem("w1", WI_PENDING, "llm", "high", "src/a.cpp", "TaskA")); + wf.queue.enqueue(makeItem("w2", WI_READY, "slm", "medium", "src/b.py", "TaskB")); + wf.queue.enqueue(makeItem("w3", WI_IN_PROGRESS, "template", "low", "src/c.cpp", "TaskC")); + wf.queue.enqueue(makeItem("w4", WI_REVIEW, "llm", "critical", "src/d.ts", "TaskD")); + wf.queue.enqueue(makeItem("w5", WI_COMPLETE, "human", "high", "src/e.cpp", "TaskE")); + return wf; +} + +static int countCards(const std::vector& cols, const std::string& key) { + for (const auto& c : cols) if (c.key == key) return (int)c.cards.size(); + return 0; +} + +void test_board_has_five_columns() { + TEST(board_has_five_columns); + auto cols = WorkflowTaskBoard::build(makeWorkflow()); + CHECK(cols.size() == 5, "expected 5 columns"); + PASS(); +} + +void test_statuses_map_to_expected_columns() { + TEST(statuses_map_to_expected_columns); + auto cols = WorkflowTaskBoard::build(makeWorkflow()); + CHECK(countCards(cols, "pending") >= 1, "pending column should contain at least one card"); + CHECK(countCards(cols, "ready") == 1, "ready count mismatch"); + CHECK(countCards(cols, "in-progress") == 1, "in-progress count mismatch"); + CHECK(countCards(cols, "review") == 1, "review count mismatch"); + CHECK(countCards(cols, "complete") == 1, "complete count mismatch"); + PASS(); +} + +void test_assigned_maps_to_in_progress_column() { + TEST(assigned_maps_to_in_progress_column); + WorkflowState wf("x"); + wf.queue.enqueue(makeItem("w1", WI_ASSIGNED, "llm", "high", "f.cpp", "TaskA")); + auto cols = WorkflowTaskBoard::build(wf); + CHECK(countCards(cols, "in-progress") == 1, "assigned should map to in-progress"); + PASS(); +} + +void test_filter_by_worker_type() { + TEST(filter_by_worker_type); + TaskBoardFilter f; + f.workerType = "llm"; + auto cols = WorkflowTaskBoard::build(makeWorkflow(), f); + CHECK(countCards(cols, "pending") >= 1, "llm pending missing"); + CHECK(countCards(cols, "review") == 1, "llm review missing"); + CHECK(countCards(cols, "ready") == 0, "non-llm should be filtered out"); + PASS(); +} + +void test_filter_by_priority() { + TEST(filter_by_priority); + TaskBoardFilter f; + f.priority = "critical"; + auto cols = WorkflowTaskBoard::build(makeWorkflow(), f); + CHECK(countCards(cols, "review") == 1, "critical review missing"); + CHECK(countCards(cols, "pending") == 0, "non-critical item should be filtered"); + PASS(); +} + +void test_filter_by_language() { + TEST(filter_by_language); + TaskBoardFilter f; + f.language = "python"; + auto cols = WorkflowTaskBoard::build(makeWorkflow(), f); + CHECK(countCards(cols, "ready") == 1, "python card missing"); + CHECK(countCards(cols, "pending") == 0, "cpp should be filtered"); + PASS(); +} + +void test_filter_by_file_substring() { + TEST(filter_by_file_substring); + TaskBoardFilter f; + f.fileContains = "d.ts"; + auto cols = WorkflowTaskBoard::build(makeWorkflow(), f); + CHECK(countCards(cols, "review") == 1, "expected d.ts task in review"); + CHECK(countCards(cols, "pending") == 0, "other files should be filtered"); + PASS(); +} + +void test_card_contains_worker_icon_and_tags() { + TEST(card_contains_worker_icon_and_tags); + auto cols = WorkflowTaskBoard::build(makeWorkflow()); + TaskCard reviewCard; + bool found = false; + for (const auto& c : cols) { + if (c.key != "review" || c.cards.empty()) continue; + reviewCard = c.cards[0]; + found = true; + break; + } + CHECK(found, "review card missing"); + CHECK(reviewCard.workerIcon == "L", "llm icon should be L"); + CHECK(!reviewCard.tags.empty(), "tags should not be empty"); + PASS(); +} + +void test_move_item_between_columns() { + TEST(move_item_between_columns); + auto wf = makeWorkflow(); + CHECK(WorkflowTaskBoard::moveItem(wf, "w1", "ready"), "move should succeed"); + auto moved = wf.queue.getItem("w1"); + CHECK(moved.has_value(), "item missing after move"); + CHECK(moved->status == WI_READY, "status should be ready"); + PASS(); +} + +void test_move_item_invalid_column_fails() { + TEST(move_item_invalid_column_fails); + auto wf = makeWorkflow(); + CHECK(!WorkflowTaskBoard::moveItem(wf, "w1", "unknown"), "invalid column should fail"); + PASS(); +} + +void test_move_item_missing_item_fails() { + TEST(move_item_missing_item_fails); + auto wf = makeWorkflow(); + CHECK(!WorkflowTaskBoard::moveItem(wf, "missing", "ready"), "missing item should fail"); + PASS(); +} + +void test_cards_sorted_by_priority_then_id() { + TEST(cards_sorted_by_priority_then_id); + WorkflowState wf("sort"); + wf.queue.enqueue(makeItem("w2", WI_READY, "llm", "medium", "f.cpp", "B")); + wf.queue.enqueue(makeItem("w1", WI_READY, "llm", "high", "f.cpp", "A")); + auto cols = WorkflowTaskBoard::build(wf); + TaskBoardColumn ready; + for (const auto& c : cols) if (c.key == "ready") ready = c; + CHECK(ready.cards.size() == 2, "ready should have two cards"); + CHECK(ready.cards[0].itemId == "w1", "high priority should sort first"); + PASS(); +} + +int main() { + std::cout << "Step 428: Kanban-Style Task Board Tests\n"; + + test_board_has_five_columns(); // 1 + test_statuses_map_to_expected_columns(); // 2 + test_assigned_maps_to_in_progress_column();// 3 + test_filter_by_worker_type(); // 4 + test_filter_by_priority(); // 5 + test_filter_by_language(); // 6 + test_filter_by_file_substring(); // 7 + test_card_contains_worker_icon_and_tags(); // 8 + test_move_item_between_columns(); // 9 + test_move_item_invalid_column_fails(); // 10 + test_move_item_missing_item_fails(); // 11 + test_cards_sorted_by_priority_then_id(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 9e236b0..b4a1db3 100644 --- a/progress.md +++ b/progress.md @@ -4757,6 +4757,54 @@ coherent dispatch pipeline. - `editor/src/MCPServer.h` (`1940` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`) +# Sprint 19 Progress — GUI Phase 2: Workflow Visualization + +## Phase 19a: Task Board Panel + +### Step 428: Kanban-Style Task Board +**Status:** PASS (12/12 tests) + +Added a task-board model layer for workflow visualization with canonical Kanban +columns, card metadata, filtering, and manual column reassignment semantics. + +**Files created:** +- `editor/src/WorkflowTaskBoard.h` — task-board support: + - 5 canonical columns: `pending`, `ready`, `in-progress`, `review`, `complete` + - status normalization (`assigned`/`in-progress` collapse, rejected->pending view) + - card projection with worker icon, priority, language/file metadata, tags + - board filters: worker type, priority, language, file substring + - deterministic card sorting (priority then item ID) + - manual move API (`moveItem`) for board-driven reassignment +- `editor/tests/step428_test.cpp` — 12 tests covering: + 1. board column presence + 2. status-to-column mapping + 3. assigned->in-progress normalization + 4. worker filter + 5. priority filter + 6. language filter + 7. file filter + 8. worker icon + tag payloads + 9. move item across columns + 10. invalid-column move rejection + 11. missing-item move rejection + 12. deterministic ready-column sort + +**Files modified:** +- `editor/CMakeLists.txt` — `step428_test` target + +**Verification run:** +- `step428_test` — PASS (12/12) new step coverage +- `step427_test` — PASS (8/8) regression coverage +- `step426_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/WorkflowTaskBoard.h` within header-size limit (`154` <= `600`) +- `editor/tests/step428_test.cpp` within test-file size guidance (`186` 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)