Step 428: add workflow task board model

This commit is contained in:
Bill
2026-02-16 17:07:21 -07:00
parent dd9c019e85
commit 4d8686cb7c
4 changed files with 398 additions and 0 deletions

View File

@@ -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)

View File

@@ -0,0 +1,154 @@
#pragma once
#include "WorkflowState.h"
#include <algorithm>
#include <map>
#include <string>
#include <vector>
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<std::string> tags;
};
struct TaskBoardColumn {
std::string key;
std::string label;
std::vector<TaskCard> cards;
};
class WorkflowTaskBoard {
public:
static std::vector<TaskBoardColumn> build(const WorkflowState& wf,
const TaskBoardFilter& filter = {}) {
std::map<std::string, TaskBoardColumn> 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<TaskBoardColumn> 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<std::string> tagsForItem(const WorkItem& wi) {
std::vector<std::string> 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;
}
};

View File

@@ -0,0 +1,187 @@
// Step 428: Kanban-Style Task Board Tests (12 tests)
#include "WorkflowTaskBoard.h"
#include <iostream>
#include <string>
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<TaskBoardColumn>& 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;
}

View File

@@ -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)