Step 581: add acceptance criteria binding
This commit is contained in:
@@ -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)
|
||||
|
||||
83
editor/src/AcceptanceCriteriaBinding.h
Normal file
83
editor/src/AcceptanceCriteriaBinding.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
// Step 581: Acceptance-Criteria Binding
|
||||
|
||||
#include "TaskitemConfidenceAmbiguity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct BoundAcceptanceCheck {
|
||||
std::string checkId;
|
||||
std::string text;
|
||||
std::string testSkeleton;
|
||||
};
|
||||
|
||||
struct BoundTaskitem {
|
||||
AnnotatedTaskitem task;
|
||||
std::vector<BoundAcceptanceCheck> checks;
|
||||
bool hasCoverage = false;
|
||||
};
|
||||
|
||||
class AcceptanceCriteriaBinding {
|
||||
public:
|
||||
static bool bind(const std::vector<AnnotatedTaskitem>& tasks,
|
||||
const std::vector<NormalizedRequirement>& requirements,
|
||||
std::vector<BoundTaskitem>* out,
|
||||
std::string* error) {
|
||||
if (!out || !error) return false;
|
||||
error->clear();
|
||||
out->clear();
|
||||
if (tasks.empty()) {
|
||||
*error = "tasks_empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<NormalizedRequirement> 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<unsigned char>(c))) {
|
||||
out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(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;
|
||||
}
|
||||
};
|
||||
185
editor/tests/step581_test.cpp
Normal file
185
editor/tests/step581_test.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
// Step 581: Acceptance-Criteria Binding (12 tests)
|
||||
|
||||
#include "AcceptanceCriteriaBinding.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 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> 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<BoundTaskitem> out;
|
||||
std::string error;
|
||||
std::vector<NormalizedRequirement> 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;
|
||||
}
|
||||
35
progress.md
35
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`
|
||||
|
||||
Reference in New Issue
Block a user