diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b0ca59d..0f521c8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/WorkflowTaskDetail.h b/editor/src/WorkflowTaskDetail.h new file mode 100644 index 0000000..fdc9322 --- /dev/null +++ b/editor/src/WorkflowTaskDetail.h @@ -0,0 +1,121 @@ +#pragma once + +#include "WorkflowState.h" + +#include +#include + +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 rejectionHistory; + std::vector annotationTags; +}; + +class WorkflowTaskDetail { +public: + static std::optional 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 buildAnnotationTags(const WorkItem& wi) { + std::vector 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; + } +}; diff --git a/editor/tests/step429_test.cpp b/editor/tests/step429_test.cpp new file mode 100644 index 0000000..c66f622 --- /dev/null +++ b/editor/tests/step429_test.cpp @@ -0,0 +1,169 @@ +// Step 429: Task Detail View Tests (12 tests) + +#include "WorkflowTaskDetail.h" + +#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 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; +} diff --git a/progress.md b/progress.md index b4a1db3..8120a5e 100644 --- a/progress.md +++ b/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)