From 9092eaa2b80781fd8000befc33bce6b2514ebd23 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:06:03 -0700 Subject: [PATCH] Step 576: add scope milestone decomposer --- editor/CMakeLists.txt | 9 ++ editor/src/ScopeMilestoneDecomposer.h | 147 +++++++++++++++++++ editor/tests/step576_test.cpp | 201 ++++++++++++++++++++++++++ progress.md | 35 +++++ 4 files changed, 392 insertions(+) create mode 100644 editor/src/ScopeMilestoneDecomposer.h create mode 100644 editor/tests/step576_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index eb5bb9e..7b6a72f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4063,4 +4063,13 @@ target_link_libraries(step575_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step576_test tests/step576_test.cpp) +target_include_directories(step576_test PRIVATE src) +target_link_libraries(step576_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/ScopeMilestoneDecomposer.h b/editor/src/ScopeMilestoneDecomposer.h new file mode 100644 index 0000000..66063dd --- /dev/null +++ b/editor/src/ScopeMilestoneDecomposer.h @@ -0,0 +1,147 @@ +#pragma once +// Step 576: Scope and Milestone Decomposer + +#include "RequirementNormalizationConflictDetector.h" + +#include +#include +#include + +struct DecomposedWorkstream { + std::string workstreamId; + std::string title; + std::vector requirementIds; + int uncertaintyScore = 0; // 0..100 +}; + +struct DecomposedMilestone { + std::string milestoneId; + std::string title; + std::vector workstreams; + int uncertaintyScore = 0; // 0..100 +}; + +struct DecomposedScopePlan { + std::vector milestones; + int overallUncertainty = 0; // 0..100 +}; + +class ScopeMilestoneDecomposer { +public: + static bool decompose(const RequirementNormalizationResult& normalized, + DecomposedScopePlan* outPlan, + std::string* error) { + if (!outPlan || !error) return false; + error->clear(); + if (normalized.requirements.empty()) { + *error = "requirements_empty"; + return false; + } + + DecomposedScopePlan plan; + plan.milestones.push_back(buildMilestone("milestone-1", "Intake Foundation", normalized, { + NormalizedRequirementKind::Goal, + NormalizedRequirementKind::Constraint + })); + plan.milestones.push_back(buildMilestone("milestone-2", "Execution Readiness", normalized, { + NormalizedRequirementKind::Dependency, + NormalizedRequirementKind::Acceptance + })); + + pruneEmptyMilestones(&plan); + if (plan.milestones.empty()) { + *error = "decomposition_empty"; + return false; + } + + plan.overallUncertainty = computeOverallUncertainty(plan.milestones); + *outPlan = plan; + return true; + } + +private: + static DecomposedMilestone buildMilestone(const std::string& milestoneId, + const std::string& title, + const RequirementNormalizationResult& normalized, + const std::vector& kinds) { + DecomposedMilestone milestone; + milestone.milestoneId = milestoneId; + milestone.title = title; + + std::vector selected; + for (const auto& requirement : normalized.requirements) { + if (containsKind(kinds, requirement.kind)) selected.push_back(requirement); + } + + if (!selected.empty()) { + DecomposedWorkstream primary; + primary.workstreamId = milestoneId + "-ws-1"; + primary.title = title + " Primary"; + for (const auto& requirement : selected) primary.requirementIds.push_back(requirement.requirementId); + primary.uncertaintyScore = computeUncertainty(selected); + milestone.workstreams.push_back(primary); + + DecomposedWorkstream review; + review.workstreamId = milestoneId + "-ws-2"; + review.title = "Architect Review"; + for (const auto& conflict : normalized.conflicts) { + review.requirementIds.push_back(conflict.leftRequirementId); + review.requirementIds.push_back(conflict.rightRequirementId); + } + dedupeIds(&review.requirementIds); + if (!review.requirementIds.empty()) { + review.uncertaintyScore = std::min(100, primary.uncertaintyScore + 20); + milestone.workstreams.push_back(review); + } + } + + milestone.uncertaintyScore = computeMilestoneUncertainty(milestone.workstreams); + return milestone; + } + + static bool containsKind(const std::vector& kinds, + NormalizedRequirementKind kind) { + for (auto candidate : kinds) { + if (candidate == kind) return true; + } + return false; + } + + static int computeUncertainty(const std::vector& requirements) { + int score = 15; + for (const auto& requirement : requirements) { + if (requirement.ambiguous) score += 15; + if (requirement.kind == NormalizedRequirementKind::Constraint) score += 5; + if (requirement.kind == NormalizedRequirementKind::Acceptance) score -= 2; + } + if (requirements.size() > 6) score += 10; + return std::max(0, std::min(100, score)); + } + + static int computeMilestoneUncertainty(const std::vector& workstreams) { + if (workstreams.empty()) return 0; + int sum = 0; + for (const auto& workstream : workstreams) sum += workstream.uncertaintyScore; + return sum / static_cast(workstreams.size()); + } + + static int computeOverallUncertainty(const std::vector& milestones) { + if (milestones.empty()) return 0; + int sum = 0; + for (const auto& milestone : milestones) sum += milestone.uncertaintyScore; + return sum / static_cast(milestones.size()); + } + + static void pruneEmptyMilestones(DecomposedScopePlan* plan) { + std::vector kept; + for (const auto& milestone : plan->milestones) { + if (!milestone.workstreams.empty()) kept.push_back(milestone); + } + plan->milestones = kept; + } + + static void dedupeIds(std::vector* ids) { + std::sort(ids->begin(), ids->end()); + ids->erase(std::unique(ids->begin(), ids->end()), ids->end()); + } +}; diff --git a/editor/tests/step576_test.cpp b/editor/tests/step576_test.cpp new file mode 100644 index 0000000..474280f --- /dev/null +++ b/editor/tests/step576_test.cpp @@ -0,0 +1,201 @@ +// Step 576: Scope and Milestone Decomposer (12 tests) + +#include "ScopeMilestoneDecomposer.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 NormalizedRequirement req(const std::string& id, + NormalizedRequirementKind kind, + bool ambiguous = false) { + NormalizedRequirement r; + r.requirementId = id; + r.kind = kind; + r.normalizedText = "text " + id; + r.anchor = "anchor"; + r.sourceLine = 1; + r.ambiguous = ambiguous; + return r; +} + +void test_decompose_success() { + TEST(decompose_success); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal)); + normalized.requirements.push_back(req("constraint-1", NormalizedRequirementKind::Constraint)); + normalized.requirements.push_back(req("dependency-1", NormalizedRequirementKind::Dependency)); + normalized.requirements.push_back(req("acceptance-1", NormalizedRequirementKind::Acceptance)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(!plan.milestones.empty(), "milestones expected"); + PASS(); +} + +void test_decompose_fails_when_requirements_empty() { + TEST(decompose_fails_when_requirements_empty); + RequirementNormalizationResult normalized; + DecomposedScopePlan plan; + std::string error; + CHECK(!ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should fail"); + CHECK(error == "requirements_empty", "wrong error"); + PASS(); +} + +void test_generates_intake_foundation_milestone() { + TEST(generates_intake_foundation_milestone); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].title == "Intake Foundation", "first milestone title mismatch"); + PASS(); +} + +void test_generates_execution_readiness_milestone_for_acceptance_dependency() { + TEST(generates_execution_readiness_milestone_for_acceptance_dependency); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("dependency-1", NormalizedRequirementKind::Dependency)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].title == "Execution Readiness", "milestone title mismatch"); + PASS(); +} + +void test_workstream_includes_requirement_ids() { + TEST(workstream_includes_requirement_ids); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal)); + normalized.requirements.push_back(req("constraint-1", NormalizedRequirementKind::Constraint)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].workstreams[0].requirementIds.size() == 2, "requirement id count mismatch"); + PASS(); +} + +void test_conflicts_create_architect_review_workstream() { + TEST(conflicts_create_architect_review_workstream); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("constraint-1", NormalizedRequirementKind::Constraint)); + normalized.requirements.push_back(req("constraint-2", NormalizedRequirementKind::Constraint)); + normalized.conflicts.push_back({"constraint-1", "constraint-2", "constraint_contradiction", "detail"}); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].workstreams.size() == 2, "review workstream expected"); + CHECK(plan.milestones[0].workstreams[1].title == "Architect Review", "review title mismatch"); + PASS(); +} + +void test_architect_review_workstream_dedupes_requirement_ids() { + TEST(architect_review_workstream_dedupes_requirement_ids); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("constraint-1", NormalizedRequirementKind::Constraint)); + normalized.requirements.push_back(req("constraint-2", NormalizedRequirementKind::Constraint)); + normalized.conflicts.push_back({"constraint-1", "constraint-2", "constraint_contradiction", "detail"}); + normalized.conflicts.push_back({"constraint-1", "constraint-2", "constraint_contradiction", "detail-2"}); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].workstreams[1].requirementIds.size() == 2, "ids should be deduped"); + PASS(); +} + +void test_ambiguous_requirements_raise_uncertainty() { + TEST(ambiguous_requirements_raise_uncertainty); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal, false)); + normalized.requirements.push_back(req("goal-2", NormalizedRequirementKind::Goal, true)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].uncertaintyScore >= 25, "uncertainty should be elevated"); + PASS(); +} + +void test_acceptance_requirements_reduce_uncertainty() { + TEST(acceptance_requirements_reduce_uncertainty); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("acceptance-1", NormalizedRequirementKind::Acceptance, false)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].uncertaintyScore <= 20, "acceptance should lower uncertainty"); + PASS(); +} + +void test_overall_uncertainty_is_averaged() { + TEST(overall_uncertainty_is_averaged); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal, true)); + normalized.requirements.push_back(req("dependency-1", NormalizedRequirementKind::Dependency, false)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.overallUncertainty > 0, "overall uncertainty should be set"); + PASS(); +} + +void test_only_relevant_milestones_are_kept() { + TEST(only_relevant_milestones_are_kept); + RequirementNormalizationResult normalized; + normalized.requirements.push_back(req("goal-1", NormalizedRequirementKind::Goal)); + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones.size() == 1, "only one milestone should remain"); + PASS(); +} + +void test_uncertainty_scores_stay_within_bounds() { + TEST(uncertainty_scores_stay_within_bounds); + RequirementNormalizationResult normalized; + for (int i = 0; i < 20; ++i) { + normalized.requirements.push_back(req("goal-" + std::to_string(i), NormalizedRequirementKind::Goal, true)); + } + + DecomposedScopePlan plan; + std::string error; + CHECK(ScopeMilestoneDecomposer::decompose(normalized, &plan, &error), "decompose should succeed"); + CHECK(plan.milestones[0].uncertaintyScore <= 100, "uncertainty upper bound exceeded"); + CHECK(plan.overallUncertainty <= 100, "overall uncertainty upper bound exceeded"); + PASS(); +} + +int main() { + std::cout << "Step 576: Scope and Milestone Decomposer\n"; + + test_decompose_success(); // 1 + test_decompose_fails_when_requirements_empty(); // 2 + test_generates_intake_foundation_milestone(); // 3 + test_generates_execution_readiness_milestone_for_acceptance_dependency(); // 4 + test_workstream_includes_requirement_ids(); // 5 + test_conflicts_create_architect_review_workstream(); // 6 + test_architect_review_workstream_dedupes_requirement_ids(); // 7 + test_ambiguous_requirements_raise_uncertainty(); // 8 + test_acceptance_requirements_reduce_uncertainty(); // 9 + test_overall_uncertainty_is_averaged(); // 10 + test_only_relevant_milestones_are_kept(); // 11 + test_uncertainty_scores_stay_within_bounds(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index f70d69a..c46b2f4 100644 --- a/progress.md +++ b/progress.md @@ -10626,3 +10626,38 @@ intake requirements, including ambiguity flags and source traceability. - `editor/src/RequirementNormalizationConflictDetector.h` within header-size limit (`166` <= `600`) - `editor/tests/step575_test.cpp` within test-file size guidance (`200` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 576: Scope and Milestone Decomposer +**Status:** PASS (12/12 tests) + +Implements requirement decomposition into milestone/workstream planning units, +including requirement mapping and uncertainty scoring for architect intake. + +**Files added:** +- `editor/src/ScopeMilestoneDecomposer.h` - decomposition module: + - requirement-kind partitioning into milestone buckets + - workstream generation with requirement-id linkage + - architect review workstream generation from detected conflicts + - per-workstream/per-milestone/overall uncertainty scoring + - milestone pruning and uncertainty bound handling +- `editor/tests/step576_test.cpp` - 12 tests covering: + - decomposition success/failure behavior + - milestone/workstream routing behavior + - requirement-id mapping behavior + - conflict-driven review stream behavior + - uncertainty scoring behavior and bounds + - empty milestone pruning behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step576_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step576_test step575_test` - PASS +- `./editor/build-native/step576_test` - PASS (12/12) +- `./editor/build-native/step575_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ScopeMilestoneDecomposer.h` within header-size limit (`147` <= `600`) +- `editor/tests/step576_test.cpp` within test-file size guidance (`201` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`