Step 429: add workflow task detail model
This commit is contained in:
@@ -2740,4 +2740,13 @@ target_link_libraries(step428_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step429_test tests/step429_test.cpp)
|
||||
target_include_directories(step429_test PRIVATE src)
|
||||
target_link_libraries(step429_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)
|
||||
|
||||
121
editor/src/WorkflowTaskDetail.h
Normal file
121
editor/src/WorkflowTaskDetail.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorkflowState.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct TaskDetailView {
|
||||
std::string itemId;
|
||||
std::string title;
|
||||
std::string status;
|
||||
std::string workerType;
|
||||
std::string priority;
|
||||
std::string skeletonCode;
|
||||
std::string generatedCode;
|
||||
std::string diffSummary;
|
||||
std::string routingExplanation;
|
||||
std::string rejectionFeedback;
|
||||
std::vector<RejectionAttempt> rejectionHistory;
|
||||
std::vector<std::string> annotationTags;
|
||||
};
|
||||
|
||||
class WorkflowTaskDetail {
|
||||
public:
|
||||
static std::optional<TaskDetailView> build(const WorkflowState& wf,
|
||||
const std::string& itemId) {
|
||||
auto item = wf.queue.getItem(itemId);
|
||||
if (!item.has_value()) return std::nullopt;
|
||||
TaskDetailView v;
|
||||
v.itemId = item->id;
|
||||
v.title = item->nodeName.empty() ? item->nodeId : item->nodeName;
|
||||
v.status = item->status;
|
||||
v.workerType = item->workerType;
|
||||
v.priority = item->priority;
|
||||
v.skeletonCode = buildSkeletonStub(*item);
|
||||
v.generatedCode = item->result.generatedCode;
|
||||
v.diffSummary = buildDiffSummary(v.skeletonCode, v.generatedCode);
|
||||
v.routingExplanation = buildRoutingExplanation(*item);
|
||||
v.rejectionFeedback = item->rejectionFeedback;
|
||||
v.rejectionHistory = item->rejectionHistory;
|
||||
v.annotationTags = buildAnnotationTags(*item);
|
||||
return v;
|
||||
}
|
||||
|
||||
static bool approve(WorkflowState& wf, const std::string& itemId,
|
||||
const std::string& reviewer = "human") {
|
||||
auto item = wf.queue.getItem(itemId);
|
||||
if (!item.has_value()) return false;
|
||||
if (item->status != WI_REVIEW) return false;
|
||||
WorkItem updated = *item;
|
||||
if (!transitionWorkItem(updated, WI_COMPLETE)) return false;
|
||||
wf.recordChange(itemId, WI_REVIEW, WI_COMPLETE, reviewer, "approved");
|
||||
return wf.queue.updateItem(itemId, updated);
|
||||
}
|
||||
|
||||
static bool reject(WorkflowState& wf, const std::string& itemId,
|
||||
const std::string& feedback,
|
||||
const std::string& reviewer = "human") {
|
||||
if (feedback.empty()) return false;
|
||||
auto item = wf.queue.getItem(itemId);
|
||||
if (!item.has_value()) return false;
|
||||
if (item->status != WI_REVIEW) return false;
|
||||
bool ok = wf.queue.reject(itemId, feedback);
|
||||
if (!ok) return false;
|
||||
wf.recordChange(itemId, WI_REVIEW, WI_READY, reviewer, feedback);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string buildSkeletonStub(const WorkItem& wi) {
|
||||
std::string name = wi.nodeName.empty() ? wi.nodeId : wi.nodeName;
|
||||
std::string lang = languageFromPath(wi.bufferId);
|
||||
if (lang == "python") return "def " + name + "():\n pass\n";
|
||||
if (lang == "cpp") return "void " + name + "() {\n}\n";
|
||||
return name + "() {}";
|
||||
}
|
||||
|
||||
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 == ".hpp" || ext == ".h") return "cpp";
|
||||
if (ext == ".py") return "python";
|
||||
return "";
|
||||
}
|
||||
|
||||
static int lineCount(const std::string& text) {
|
||||
if (text.empty()) return 0;
|
||||
int lines = 1;
|
||||
for (char c : text) if (c == '\n') ++lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
static std::string buildDiffSummary(const std::string& skeleton,
|
||||
const std::string& generated) {
|
||||
int s = lineCount(skeleton);
|
||||
int g = lineCount(generated);
|
||||
int delta = g - s;
|
||||
std::string sign = delta >= 0 ? "+" : "";
|
||||
return "line_delta=" + sign + std::to_string(delta) +
|
||||
" skeleton_lines=" + std::to_string(s) +
|
||||
" generated_lines=" + std::to_string(g);
|
||||
}
|
||||
|
||||
static std::string buildRoutingExplanation(const WorkItem& wi) {
|
||||
std::string reason = "worker=" + wi.workerType;
|
||||
if (!wi.contextWidth.empty()) reason += " context=" + wi.contextWidth;
|
||||
if (wi.reviewRequired) reason += " review=required";
|
||||
if (!wi.priority.empty()) reason += " priority=" + wi.priority;
|
||||
return reason;
|
||||
}
|
||||
|
||||
static std::vector<std::string> buildAnnotationTags(const WorkItem& wi) {
|
||||
std::vector<std::string> tags;
|
||||
if (!wi.contextWidth.empty()) tags.push_back("ContextWidth:" + wi.contextWidth);
|
||||
if (!wi.workerType.empty()) tags.push_back("Automatability:" + wi.workerType);
|
||||
if (wi.reviewRequired) tags.push_back("Review:required");
|
||||
if (!wi.priority.empty()) tags.push_back("Priority:" + wi.priority);
|
||||
return tags;
|
||||
}
|
||||
};
|
||||
169
editor/tests/step429_test.cpp
Normal file
169
editor/tests/step429_test.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
// Step 429: Task Detail View Tests (12 tests)
|
||||
|
||||
#include "WorkflowTaskDetail.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 WorkItem makeReviewItem(const std::string& id = "w1") {
|
||||
WorkItem wi;
|
||||
wi.id = id;
|
||||
wi.nodeId = "n1";
|
||||
wi.nodeName = "processOrder";
|
||||
wi.bufferId = "orders.py";
|
||||
wi.workerType = "llm";
|
||||
wi.contextWidth = "file";
|
||||
wi.reviewRequired = true;
|
||||
wi.priority = "high";
|
||||
wi.status = WI_REVIEW;
|
||||
wi.result.generatedCode = "def process_order(x):\n return x\n";
|
||||
wi.result.reasoning = "generated";
|
||||
return wi;
|
||||
}
|
||||
|
||||
static WorkflowState makeWorkflow() {
|
||||
WorkflowState wf("detail");
|
||||
wf.queue.enqueue(makeReviewItem("w1"));
|
||||
WorkItem done = makeReviewItem("w2");
|
||||
done.status = WI_COMPLETE;
|
||||
done.rejectionFeedback = "old feedback";
|
||||
RejectionAttempt attempt;
|
||||
attempt.workerType = "slm";
|
||||
attempt.feedback = "fix naming";
|
||||
attempt.rejectedBy = "human";
|
||||
done.rejectionHistory.push_back(attempt);
|
||||
wf.queue.enqueue(done);
|
||||
return wf;
|
||||
}
|
||||
|
||||
void test_build_detail_for_existing_item() {
|
||||
TEST(build_detail_for_existing_item);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w1");
|
||||
CHECK(view.has_value(), "detail should exist");
|
||||
CHECK(view->itemId == "w1", "item id mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_build_detail_missing_item_returns_nullopt() {
|
||||
TEST(build_detail_missing_item_returns_nullopt);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "missing");
|
||||
CHECK(!view.has_value(), "missing item should return nullopt");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detail_contains_skeleton_and_generated_code() {
|
||||
TEST(detail_contains_skeleton_and_generated_code);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w1");
|
||||
CHECK(view->skeletonCode.find("def processOrder") != std::string::npos ||
|
||||
view->skeletonCode.find("def process_order") != std::string::npos,
|
||||
"skeleton stub missing");
|
||||
CHECK(view->generatedCode.find("return x") != std::string::npos,
|
||||
"generated code missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detail_contains_diff_summary() {
|
||||
TEST(detail_contains_diff_summary);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w1");
|
||||
CHECK(view->diffSummary.find("line_delta=") != std::string::npos,
|
||||
"diff summary missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detail_contains_routing_explanation() {
|
||||
TEST(detail_contains_routing_explanation);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w1");
|
||||
CHECK(view->routingExplanation.find("worker=llm") != std::string::npos,
|
||||
"routing explanation missing worker");
|
||||
CHECK(view->routingExplanation.find("review=required") != std::string::npos,
|
||||
"routing explanation missing review");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detail_contains_annotation_tags() {
|
||||
TEST(detail_contains_annotation_tags);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w1");
|
||||
CHECK(!view->annotationTags.empty(), "annotation tags should exist");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detail_includes_rejection_history() {
|
||||
TEST(detail_includes_rejection_history);
|
||||
auto wf = makeWorkflow();
|
||||
auto view = WorkflowTaskDetail::build(wf, "w2");
|
||||
CHECK(!view->rejectionHistory.empty(), "rejection history missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_approve_review_item_sets_complete() {
|
||||
TEST(approve_review_item_sets_complete);
|
||||
auto wf = makeWorkflow();
|
||||
CHECK(WorkflowTaskDetail::approve(wf, "w1"), "approve should succeed");
|
||||
auto item = wf.queue.getItem("w1");
|
||||
CHECK(item.has_value() && item->status == WI_COMPLETE, "status should be complete");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_approve_non_review_item_fails() {
|
||||
TEST(approve_non_review_item_fails);
|
||||
auto wf = makeWorkflow();
|
||||
CHECK(!WorkflowTaskDetail::approve(wf, "w2"), "approve on complete item should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_reject_review_item_sets_ready_and_feedback() {
|
||||
TEST(reject_review_item_sets_ready_and_feedback);
|
||||
auto wf = makeWorkflow();
|
||||
CHECK(WorkflowTaskDetail::reject(wf, "w1", "needs tests"), "reject should succeed");
|
||||
auto item = wf.queue.getItem("w1");
|
||||
CHECK(item.has_value() && item->status == WI_READY, "status should be ready");
|
||||
CHECK(item->result.reasoning.find("needs tests") != std::string::npos,
|
||||
"feedback should be stored");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_reject_without_feedback_fails() {
|
||||
TEST(reject_without_feedback_fails);
|
||||
auto wf = makeWorkflow();
|
||||
CHECK(!WorkflowTaskDetail::reject(wf, "w1", ""), "empty feedback should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_reject_non_review_item_fails() {
|
||||
TEST(reject_non_review_item_fails);
|
||||
auto wf = makeWorkflow();
|
||||
CHECK(!WorkflowTaskDetail::reject(wf, "w2", "no"), "reject on non-review should fail");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 429: Task Detail View Tests\n";
|
||||
|
||||
test_build_detail_for_existing_item(); // 1
|
||||
test_build_detail_missing_item_returns_nullopt(); // 2
|
||||
test_detail_contains_skeleton_and_generated_code(); // 3
|
||||
test_detail_contains_diff_summary(); // 4
|
||||
test_detail_contains_routing_explanation(); // 5
|
||||
test_detail_contains_annotation_tags(); // 6
|
||||
test_detail_includes_rejection_history(); // 7
|
||||
test_approve_review_item_sets_complete(); // 8
|
||||
test_approve_non_review_item_fails(); // 9
|
||||
test_reject_review_item_sets_ready_and_feedback(); // 10
|
||||
test_reject_without_feedback_fails(); // 11
|
||||
test_reject_non_review_item_fails(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
47
progress.md
47
progress.md
@@ -4805,6 +4805,53 @@ columns, card metadata, filtering, and manual column reassignment semantics.
|
||||
- `editor/src/MCPServer.h` (`1940` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2768` > `600`)
|
||||
|
||||
### Step 429: Task Detail View
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Added a task-detail model layer with review actions so board-selected items can
|
||||
surface skeleton/result context, routing rationale, diff summary, and approval/
|
||||
rejection operations in one consistent view model.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/WorkflowTaskDetail.h` — task-detail support:
|
||||
- detail projection (`TaskDetailView`) for one work item
|
||||
- skeleton stub generation based on file language
|
||||
- generated-code and line-delta diff summary
|
||||
- routing explanation synthesis
|
||||
- annotation-tag extraction from work-item semantics
|
||||
- review actions:
|
||||
- `approve` (`review` -> `complete`)
|
||||
- `reject` (`review` -> `ready`, feedback required)
|
||||
- `editor/tests/step429_test.cpp` — 12 tests covering:
|
||||
1. detail build for existing item
|
||||
2. missing-item handling
|
||||
3. skeleton/generated payload inclusion
|
||||
4. diff summary inclusion
|
||||
5. routing explanation inclusion
|
||||
6. annotation tag inclusion
|
||||
7. rejection-history inclusion
|
||||
8. approve transition behavior
|
||||
9. approve invalid-status rejection
|
||||
10. reject transition + feedback persistence
|
||||
11. reject feedback-required validation
|
||||
12. reject invalid-status rejection
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — `step429_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step429_test` — PASS (12/12) new step coverage
|
||||
- `step428_test` — PASS (12/12) regression coverage
|
||||
- `step427_test` — PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/WorkflowTaskDetail.h` within header-size limit (`121` <= `600`)
|
||||
- `editor/tests/step429_test.cpp` within test-file size guidance (`169` 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