diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index aa2505d..f921bcb 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3244,4 +3244,13 @@ target_link_libraries(step484_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step485_test tests/step485_test.cpp) +target_include_directories(step485_test PRIVATE src) +target_link_libraries(step485_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/WorkerDispatchValidator.h b/editor/src/WorkerDispatchValidator.h new file mode 100644 index 0000000..23eca58 --- /dev/null +++ b/editor/src/WorkerDispatchValidator.h @@ -0,0 +1,145 @@ +#pragma once + +// Step 485: Worker Dispatch + Result Validation +// Models architect->worker handoff, result validation, retry budget, and escalation. + +#include "PriorStepContextInjector.h" +#include "ProjectConventionAnalyzer.h" +#include "StepSpecExpander.h" + +#include +#include +#include + +struct WorkerSubmission { + std::vector files; + bool buildPassed = false; + bool testsPassed = false; + std::string errorOutput; +}; + +struct WorkerDispatchInput { + std::string stepMarkdown; + std::string stepNumber; + std::string componentName; + std::string buildSystem = "cmake"; + std::string repoRoot = "."; + std::vector priorHeaders; +}; + +struct DispatchAttemptReport { + int attempt = 0; + bool accepted = false; + bool buildPassed = false; + bool testsPassed = false; + ConventionValidationReport conventionReport; + std::string status; +}; + +struct WorkerDispatchResult { + StepSpec spec; + StepContext context; + ProjectConventions conventions; + std::vector attempts; + bool completed = false; + bool escalated = false; + int retryBudget = 3; + int attemptsUsed = 0; + std::string finalStatus; +}; + +class WorkerDispatchValidator { +public: + static WorkerDispatchResult run(const WorkerDispatchInput& in, + const std::vector& submissions, + int retryBudget = 3) { + WorkerDispatchResult out; + out.retryBudget = retryBudget <= 0 ? 1 : retryBudget; + + out.spec = StepSpecExpander::expand({ + in.stepMarkdown, + in.stepNumber, + in.componentName, + in.buildSystem, + {}, // conventions are extracted from repo state + in.priorHeaders + }); + out.context = PriorStepContextInjector::build(in.priorHeaders); + out.conventions = ProjectConventionAnalyzer::extract(in.repoRoot); + + if (submissions.empty()) { + out.escalated = true; + out.finalStatus = "No worker submissions provided"; + return out; + } + + const int maxAttempts = out.retryBudget < (int)submissions.size() + ? out.retryBudget : (int)submissions.size(); + for (int i = 0; i < maxAttempts; ++i) { + DispatchAttemptReport report; + report.attempt = i + 1; + report.buildPassed = submissions[i].buildPassed; + report.testsPassed = submissions[i].testsPassed; + report.conventionReport = + ProjectConventionAnalyzer::validate(out.conventions, submissions[i].files); + + const bool conventionsOk = report.conventionReport.violations.empty(); + report.accepted = report.buildPassed && report.testsPassed && conventionsOk; + report.status = classifyAttemptStatus(report, conventionsOk, submissions[i].errorOutput); + out.attempts.push_back(report); + out.attemptsUsed = i + 1; + + if (report.accepted) { + out.completed = true; + out.finalStatus = "Step completed"; + return out; + } + } + + out.escalated = true; + out.finalStatus = out.attemptsUsed >= out.retryBudget + ? "Retry budget exhausted; escalate to architect" + : "All submissions consumed without completion"; + return out; + } + + static std::string buildWorkerBrief(const WorkerDispatchInput& in, + const StepSpec& spec, + const StepContext& context, + const ProjectConventions& conventions) { + std::ostringstream out; + out << "Step " << in.stepNumber << ": " << in.componentName << "\n"; + out << "Complexity: " << complexityLabel(spec.complexity) << "\n"; + out << "Tests planned: " << spec.testPlan.size() << "\n"; + out << "Header target: " << spec.headerSkeleton.fileName << "\n"; + out << "Build target: " << spec.buildEntry.targetName << "\n"; + out << "Conventions: " << conventions.classNaming << ", " + << conventions.methodNaming << ", " << conventions.fileNaming << "\n"; + out << "Context brief: " << context.condensedBrief() << "\n"; + return out.str(); + } + +private: + static std::string classifyAttemptStatus(const DispatchAttemptReport& report, + bool conventionsOk, + const std::string& errorOutput) { + if (!report.buildPassed) { + return "build_failed" + (errorOutput.empty() ? "" : ": " + errorOutput); + } + if (!report.testsPassed) { + return "tests_failed" + (errorOutput.empty() ? "" : ": " + errorOutput); + } + if (!conventionsOk) return "convention_warnings"; + return "accepted"; + } + + static std::string complexityLabel(StepComplexity c) { + switch (c) { + case StepComplexity::Trivial: return "trivial"; + case StepComplexity::Standard: return "standard"; + case StepComplexity::Complex: return "complex"; + case StepComplexity::Pipeline: return "pipeline"; + } + return "standard"; + } +}; diff --git a/editor/tests/step485_test.cpp b/editor/tests/step485_test.cpp new file mode 100644 index 0000000..4aec99b --- /dev/null +++ b/editor/tests/step485_test.cpp @@ -0,0 +1,165 @@ +// Step 485: Worker Dispatch + Result Validation Tests (unit, integration, negative) + +#include "WorkerDispatchValidator.h" + +#include +#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 WorkerDispatchInput sampleInput() { + WorkerDispatchInput in; + in.stepMarkdown = "Orchestrate worker dispatch pipeline and validate outputs."; + in.stepNumber = "485"; + in.componentName = "WorkerDispatchValidator"; + in.repoRoot = "."; + in.priorHeaders = { + "editor/src/StepSpecExpander.h", + "editor/src/ProjectConventionAnalyzer.h", + "editor/src/PriorStepContextInjector.h" + }; + return in; +} + +static SourceFileDraft goodHeader() { + return {"editor/src/DispatchOutput.h", "struct DispatchOutput { int x; }; \n"}; +} + +static SourceFileDraft goodTest() { + return {"editor/tests/step999_test.cpp", + "#define CHECK(a,b) do{}while(0)\nint main(){ CHECK(true, \"ok\"); return 0; }\n"}; +} + +static SourceFileDraft goodCmake() { + return {"editor/CMakeLists.txt", + "add_executable(step999_test tests/step999_test.cpp)\n"}; +} + +void test_dispatch_prepares_spec_context_and_conventions() { + TEST(dispatch_prepares_spec_context_and_conventions); + WorkerSubmission s{{goodHeader(), goodTest(), goodCmake()}, true, true, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}); + CHECK(!out.spec.testPlan.empty(), "expected non-empty test plan"); + CHECK(!out.context.priorApis.empty(), "expected prior api context"); + CHECK(out.conventions.buildSystem == "cmake", "expected cmake conventions"); + PASS(); +} + +void test_accepts_submission_when_build_tests_and_conventions_pass() { + TEST(accepts_submission_when_build_tests_and_conventions_pass); + WorkerSubmission s{{goodHeader(), goodTest(), goodCmake()}, true, true, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}); + CHECK(out.completed, "expected completed dispatch"); + CHECK(!out.escalated, "did not expect escalation"); + CHECK(out.attemptsUsed == 1, "expected one attempt"); + PASS(); +} + +void test_retries_after_test_failure_then_succeeds() { + TEST(retries_after_test_failure_then_succeeds); + WorkerSubmission bad{{goodHeader(), goodTest(), goodCmake()}, true, false, "1 failing test"}; + WorkerSubmission good{{goodHeader(), goodTest(), goodCmake()}, true, true, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {bad, good}, 3); + CHECK(out.completed, "expected success after retry"); + CHECK(out.attemptsUsed == 2, "expected two attempts used"); + CHECK(out.attempts[0].status.find("tests_failed") == 0, "first status should be tests_failed"); + PASS(); +} + +void test_escalates_when_retry_budget_exhausted() { + TEST(escalates_when_retry_budget_exhausted); + WorkerSubmission bad{{goodHeader(), goodTest(), goodCmake()}, false, false, "build break"}; + auto out = WorkerDispatchValidator::run(sampleInput(), {bad, bad, bad}, 2); + CHECK(!out.completed, "should not complete"); + CHECK(out.escalated, "expected escalation"); + CHECK(out.attemptsUsed == 2, "should stop at retry budget"); + CHECK(out.finalStatus.find("Retry budget exhausted") != std::string::npos, + "expected retry budget status"); + PASS(); +} + +void test_convention_violations_are_reported_and_not_accepted() { + TEST(convention_violations_are_reported_and_not_accepted); + SourceFileDraft badTest{"editor/tests/step999_test.cpp", "int main(){ return 0; }\n"}; + WorkerSubmission s{{goodHeader(), badTest, goodCmake()}, true, true, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}, 1); + CHECK(!out.completed, "should not accept convention violations"); + CHECK(out.escalated, "should escalate for architect decision"); + CHECK(out.attempts[0].conventionReport.hasViolation("TEST_ASSERTION_MISSING"), + "expected test assertion violation"); + PASS(); +} + +void test_build_failure_status_contains_error_output() { + TEST(build_failure_status_contains_error_output); + WorkerSubmission s{{goodHeader(), goodTest(), goodCmake()}, false, false, "linker error"}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}, 1); + CHECK(out.attempts[0].status.find("build_failed") == 0, "missing build_failed status"); + CHECK(out.attempts[0].status.find("linker error") != std::string::npos, + "missing error payload"); + PASS(); +} + +void test_no_submissions_immediately_escalates_negative_case() { + TEST(no_submissions_immediately_escalates_negative_case); + auto out = WorkerDispatchValidator::run(sampleInput(), {}, 3); + CHECK(out.escalated, "expected escalation"); + CHECK(!out.completed, "should not complete"); + CHECK(out.finalStatus.find("No worker submissions") != std::string::npos, + "missing no submission message"); + PASS(); +} + +void test_default_retry_budget_is_three() { + TEST(default_retry_budget_is_three); + WorkerSubmission bad{{goodHeader(), goodTest(), goodCmake()}, false, false, "x"}; + auto out = WorkerDispatchValidator::run(sampleInput(), {bad, bad, bad, bad}); + CHECK(out.retryBudget == 3, "default budget mismatch"); + CHECK(out.attemptsUsed == 3, "should use three attempts"); + PASS(); +} + +void test_worker_brief_contains_core_handoff_sections() { + TEST(worker_brief_contains_core_handoff_sections); + WorkerSubmission s{{goodHeader(), goodTest(), goodCmake()}, true, true, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}); + auto brief = WorkerDispatchValidator::buildWorkerBrief( + sampleInput(), out.spec, out.context, out.conventions); + CHECK(brief.find("Complexity:") != std::string::npos, "missing complexity"); + CHECK(brief.find("Tests planned:") != std::string::npos, "missing test plan count"); + CHECK(brief.find("Context brief:") != std::string::npos, "missing context brief"); + PASS(); +} + +void test_acceptance_requires_both_build_and_tests_even_with_clean_conventions() { + TEST(acceptance_requires_both_build_and_tests_even_with_clean_conventions); + WorkerSubmission s{{goodHeader(), goodTest(), goodCmake()}, true, false, ""}; + auto out = WorkerDispatchValidator::run(sampleInput(), {s}, 1); + CHECK(!out.completed, "must not complete when tests fail"); + CHECK(out.escalated, "expected escalation"); + PASS(); +} + +int main() { + std::cout << "Step 485: Worker Dispatch + Result Validation Tests\n"; + + test_dispatch_prepares_spec_context_and_conventions(); // 1 + test_accepts_submission_when_build_tests_and_conventions_pass(); // 2 + test_retries_after_test_failure_then_succeeds(); // 3 + test_escalates_when_retry_budget_exhausted(); // 4 + test_convention_violations_are_reported_and_not_accepted(); // 5 + test_build_failure_status_contains_error_output(); // 6 + test_no_submissions_immediately_escalates_negative_case(); // 7 + test_default_retry_budget_is_three(); // 8 + test_worker_brief_contains_core_handoff_sections(); // 9 + test_acceptance_requires_both_build_and_tests_even_with_clean_conventions(); // 10 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 6e8e1a7..e0213cd 100644 --- a/progress.md +++ b/progress.md @@ -6988,3 +6988,45 @@ headers. - `editor/src/PriorStepContextInjector.h` within header-size limit (`125` <= `600`) - `editor/tests/step484_test.cpp` within test-file size guidance (`120` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 485: Worker Dispatch + Result Validation +**Status:** PASS (10/10 tests) + +Implements the architect->worker dispatch protocol model for Phase 23c: +prepare spec/context/conventions, validate worker output, enforce retry +budget, and escalate unresolved submissions to architect review. + +**Files added:** +- `editor/src/WorkerDispatchValidator.h` — dispatch orchestration model: + - `WorkerDispatchInput`, `WorkerSubmission`, `DispatchAttemptReport`, `WorkerDispatchResult` + - `run(...)`: + - builds `StepSpec` via `StepSpecExpander` + - builds `StepContext` via `PriorStepContextInjector` + - extracts `ProjectConventions` via `ProjectConventionAnalyzer` + - validates each worker submission (build/tests/conventions) + - retries up to configurable budget (default `3`) + - marks completion only when build+tests pass and no convention warnings + - escalates on exhausted retries or missing submissions + - `buildWorkerBrief(...)` condensed handoff text for worker prompts +- `editor/tests/step485_test.cpp` — 10 tests (unit/integration/negative): + - pre-dispatch artifact generation (spec/context/conventions) + - successful first-attempt acceptance + - retry-after-failure success path + - retry-budget exhaustion escalation + - convention-violation reporting path + - build-failure status propagation + - empty-submission negative case + - default retry-budget behavior + - worker brief content checks + - acceptance gate requiring both build and tests +- `editor/CMakeLists.txt` — `step485_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step485_test step484_test` — PASS +- `./editor/build-native/step485_test` — PASS (10/10) +- `./editor/build-native/step484_test` — PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/WorkerDispatchValidator.h` within header-size limit (`145` <= `600`) +- `editor/tests/step485_test.cpp` within test-file size guidance (`165` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`