From fd5adf89c2ea6ed963451065b469da0dcfda2039 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:12:42 -0700 Subject: [PATCH] Step 581: add acceptance criteria binding --- editor/CMakeLists.txt | 9 ++ editor/src/AcceptanceCriteriaBinding.h | 83 +++++++++++ editor/tests/step581_test.cpp | 185 +++++++++++++++++++++++++ progress.md | 35 +++++ 4 files changed, 312 insertions(+) create mode 100644 editor/src/AcceptanceCriteriaBinding.h create mode 100644 editor/tests/step581_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 3eb2922..96af0d5 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4108,4 +4108,13 @@ target_link_libraries(step580_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step581_test tests/step581_test.cpp) +target_include_directories(step581_test PRIVATE src) +target_link_libraries(step581_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/AcceptanceCriteriaBinding.h b/editor/src/AcceptanceCriteriaBinding.h new file mode 100644 index 0000000..e380a3b --- /dev/null +++ b/editor/src/AcceptanceCriteriaBinding.h @@ -0,0 +1,83 @@ +#pragma once +// Step 581: Acceptance-Criteria Binding + +#include "TaskitemConfidenceAmbiguity.h" + +#include +#include +#include +#include + +struct BoundAcceptanceCheck { + std::string checkId; + std::string text; + std::string testSkeleton; +}; + +struct BoundTaskitem { + AnnotatedTaskitem task; + std::vector checks; + bool hasCoverage = false; +}; + +class AcceptanceCriteriaBinding { +public: + static bool bind(const std::vector& tasks, + const std::vector& requirements, + std::vector* out, + std::string* error) { + if (!out || !error) return false; + error->clear(); + out->clear(); + if (tasks.empty()) { + *error = "tasks_empty"; + return false; + } + + std::vector acceptance; + for (const auto& requirement : requirements) { + if (requirement.kind == NormalizedRequirementKind::Acceptance) acceptance.push_back(requirement); + } + if (acceptance.empty()) { + *error = "acceptance_requirements_empty"; + return false; + } + + for (const auto& task : tasks) { + BoundTaskitem bound; + bound.task = task; + for (const auto& req : acceptance) { + BoundAcceptanceCheck check; + check.checkId = task.base.taskId + "::" + req.requirementId; + check.text = req.normalizedText; + check.testSkeleton = makeTestSkeleton(task.base.taskId, req.normalizedText); + bound.checks.push_back(check); + } + bound.hasCoverage = !bound.checks.empty(); + out->push_back(bound); + } + return true; + } + +private: + static std::string makeTestSkeleton(const std::string& taskId, const std::string& checkText) { + return "test_" + sanitize(taskId) + "_" + sanitize(checkText); + } + + static std::string sanitize(const std::string& text) { + std::string out; + bool lastUnderscore = false; + for (char c : text) { + if (std::isalnum(static_cast(c))) { + out.push_back(static_cast(std::tolower(static_cast(c)))); + lastUnderscore = false; + } else if (!lastUnderscore) { + out.push_back('_'); + lastUnderscore = true; + } + } + while (!out.empty() && out.front() == '_') out.erase(out.begin()); + while (!out.empty() && out.back() == '_') out.pop_back(); + return out.empty() ? "check" : out; + } +}; diff --git a/editor/tests/step581_test.cpp b/editor/tests/step581_test.cpp new file mode 100644 index 0000000..a94a16f --- /dev/null +++ b/editor/tests/step581_test.cpp @@ -0,0 +1,185 @@ +// Step 581: Acceptance-Criteria Binding (12 tests) + +#include "AcceptanceCriteriaBinding.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 AnnotatedTaskitem task(const std::string& id) { + AnnotatedTaskitem t; + t.base.taskId = id; + t.base.title = "title"; + t.base.milestoneId = "m1"; + t.base.queueReady = true; + t.confidence = 80; + return t; +} + +static NormalizedRequirement acceptance(const std::string& id, const std::string& text) { + NormalizedRequirement r; + r.requirementId = id; + r.kind = NormalizedRequirementKind::Acceptance; + r.normalizedText = text; + r.anchor = "acceptance"; + r.sourceLine = 1; + return r; +} + +void test_bind_success() { + TEST(bind_success); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, {acceptance("acc-1", "all tests pass")}, &out, &error), "bind should succeed"); + CHECK(out.size() == 1, "bound task count mismatch"); + PASS(); +} + +void test_bind_fails_for_empty_tasks() { + TEST(bind_fails_for_empty_tasks); + std::vector out; + std::string error; + CHECK(!AcceptanceCriteriaBinding::bind({}, {acceptance("acc-1", "a")}, &out, &error), "bind should fail"); + CHECK(error == "tasks_empty", "wrong error"); + PASS(); +} + +void test_bind_fails_for_missing_acceptance_requirements() { + TEST(bind_fails_for_missing_acceptance_requirements); + std::vector out; + std::string error; + CHECK(!AcceptanceCriteriaBinding::bind({task("task-1")}, {}, &out, &error), "bind should fail"); + CHECK(error == "acceptance_requirements_empty", "wrong error"); + PASS(); +} + +void test_each_task_receives_all_acceptance_checks() { + TEST(each_task_receives_all_acceptance_checks); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1"), task("task-2")}, + {acceptance("acc-1", "a"), acceptance("acc-2", "b")}, + &out, &error), "bind should succeed"); + CHECK(out[0].checks.size() == 2, "task-1 check count mismatch"); + CHECK(out[1].checks.size() == 2, "task-2 check count mismatch"); + PASS(); +} + +void test_check_ids_include_task_and_requirement_ids() { + TEST(check_ids_include_task_and_requirement_ids); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, + {acceptance("acc-7", "a")}, + &out, &error), "bind should succeed"); + CHECK(out[0].checks[0].checkId == "task-1::acc-7", "check id mismatch"); + PASS(); +} + +void test_check_text_matches_acceptance_text() { + TEST(check_text_matches_acceptance_text); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, + {acceptance("acc-1", "must pass integration")}, + &out, &error), "bind should succeed"); + CHECK(out[0].checks[0].text == "must pass integration", "check text mismatch"); + PASS(); +} + +void test_test_skeleton_is_generated() { + TEST(test_skeleton_is_generated); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, + {acceptance("acc-1", "must pass integration")}, + &out, &error), "bind should succeed"); + CHECK(out[0].checks[0].testSkeleton.find("test_task_1_") == 0, "test skeleton prefix mismatch"); + PASS(); +} + +void test_test_skeleton_sanitizes_symbols() { + TEST(test_skeleton_sanitizes_symbols); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, + {acceptance("acc-1", "must-pass, integration!")}, + &out, &error), "bind should succeed"); + CHECK(out[0].checks[0].testSkeleton == "test_task_1_must_pass_integration", "sanitized skeleton mismatch"); + PASS(); +} + +void test_has_coverage_true_when_checks_present() { + TEST(has_coverage_true_when_checks_present); + std::vector out; + std::string error; + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, + {acceptance("acc-1", "a")}, + &out, &error), "bind should succeed"); + CHECK(out[0].hasCoverage, "hasCoverage should be true"); + PASS(); +} + +void test_non_acceptance_requirements_are_ignored() { + TEST(non_acceptance_requirements_are_ignored); + std::vector out; + std::string error; + NormalizedRequirement r; + r.requirementId = "goal-1"; + r.kind = NormalizedRequirementKind::Goal; + r.normalizedText = "goal"; + CHECK(!AcceptanceCriteriaBinding::bind({task("task-1")}, {r}, &out, &error), "bind should fail"); + CHECK(error == "acceptance_requirements_empty", "wrong error"); + PASS(); +} + +void test_multiple_tasks_preserve_original_annotation_fields() { + TEST(multiple_tasks_preserve_original_annotation_fields); + std::vector out; + std::string error; + auto t1 = task("task-1"); + t1.confidence = 42; + auto t2 = task("task-2"); + t2.confidence = 99; + CHECK(AcceptanceCriteriaBinding::bind({t1, t2}, + {acceptance("acc-1", "a")}, + &out, &error), "bind should succeed"); + CHECK(out[0].task.confidence == 42, "task-1 confidence should be preserved"); + CHECK(out[1].task.confidence == 99, "task-2 confidence should be preserved"); + PASS(); +} + +void test_binding_scales_with_many_acceptance_checks() { + TEST(binding_scales_with_many_acceptance_checks); + std::vector out; + std::string error; + std::vector reqs; + for (int i = 0; i < 10; ++i) reqs.push_back(acceptance("acc-" + std::to_string(i), "check " + std::to_string(i))); + CHECK(AcceptanceCriteriaBinding::bind({task("task-1")}, reqs, &out, &error), "bind should succeed"); + CHECK(out[0].checks.size() == 10, "all checks should bind"); + PASS(); +} + +int main() { + std::cout << "Step 581: Acceptance-Criteria Binding\n"; + + test_bind_success(); // 1 + test_bind_fails_for_empty_tasks(); // 2 + test_bind_fails_for_missing_acceptance_requirements();// 3 + test_each_task_receives_all_acceptance_checks(); // 4 + test_check_ids_include_task_and_requirement_ids(); // 5 + test_check_text_matches_acceptance_text(); // 6 + test_test_skeleton_is_generated(); // 7 + test_test_skeleton_sanitizes_symbols(); // 8 + test_has_coverage_true_when_checks_present(); // 9 + test_non_acceptance_requirements_are_ignored(); // 10 + test_multiple_tasks_preserve_original_annotation_fields(); // 11 + test_binding_scales_with_many_acceptance_checks(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 8ffa06c..3f8c8f7 100644 --- a/progress.md +++ b/progress.md @@ -10801,3 +10801,38 @@ routing/escalation behavior in constrained execution pipelines. - `editor/src/TaskitemConfidenceAmbiguity.h` within header-size limit (`85` <= `600`) - `editor/tests/step580_test.cpp` within test-file size guidance (`189` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 581: Acceptance-Criteria Binding +**Status:** PASS (12/12 tests) + +Implements acceptance-criteria binding for annotated taskitems with explicit +check identities and generated test-skeleton handles. + +**Files added:** +- `editor/src/AcceptanceCriteriaBinding.h` - acceptance binding module: + - binds acceptance requirements onto each annotated taskitem + - emits stable check ids (`task::requirement`) + - generates sanitized test-skeleton identifiers + - marks per-task acceptance coverage state + - validates presence of tasks and acceptance requirements +- `editor/tests/step581_test.cpp` - 12 tests covering: + - binding success/failure behavior + - check fan-out behavior across tasks/requirements + - check id/text correctness behavior + - test-skeleton generation/sanitization behavior + - coverage flag behavior + - annotation field preservation behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step581_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step581_test step580_test` - PASS +- `./editor/build-native/step581_test` - PASS (12/12) +- `./editor/build-native/step580_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/AcceptanceCriteriaBinding.h` within header-size limit (`83` <= `600`) +- `editor/tests/step581_test.cpp` within test-file size guidance (`185` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`