Step 428: add workflow task board model
This commit is contained in:
@@ -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)
|
||||
|
||||
154
editor/src/WorkflowTaskBoard.h
Normal file
154
editor/src/WorkflowTaskBoard.h
Normal 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;
|
||||
}
|
||||
};
|
||||
187
editor/tests/step428_test.cpp
Normal file
187
editor/tests/step428_test.cpp
Normal 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;
|
||||
}
|
||||
48
progress.md
48
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)
|
||||
|
||||
Reference in New Issue
Block a user