// Step 1989: GR-029 fix — project-grounded taskitem titles, intents, and step IDs // // Three bugs fixed in buildRequirementWorkstream + buildExecutionContract: // // 1. Double prefix: "Implement implement itargetresolver..." → workstreamPrefix // already adds "Implement" and normalizedText had it lowercased too. // Fix: use sourceText for display; strip leading verb if it duplicates prefix. // // 2. Lowercase intent: intent was normalizedText (all-lowercase). Fix: use // sourceText so class names, paths, and caps are preserved. // // 3. Hardcoded whetstone step IDs: strictExecutionContract path always injected // ["1939","1940","1941"] when stepIds were empty, even for external project // specs. Fix: leave stepIds empty when none found in the spec. // // t1: Goal requirement title uses sourceText, no double "Implement implement" // t2: Grouped workstream still preserves requirement wording for non-empty output // t3: Intent field preserves original capitalisation from sourceText // t4: Intent falls back to normalizedText when sourceText is absent // t5: buildExecutionContract does not inject whetstone step IDs when none extracted // t6: grouped workstream titles use requirement summary instead of generic bucket labels // t7: work-item subsection context creates distinct grouped workstreams #include "ScopeMilestoneDecomposer.h" #include "RequirementNormalizationConflictDetector.h" #include #include #include static int p = 0, f = 0; #define T(n) { std::cout << " " << #n << "... "; } #define P() { std::cout << "PASS\n"; ++p; } #define F(m) { std::cout << "FAIL: " << (m) << "\n"; ++f; } #define C(c, m) if (!(c)) { F(m); return; } static NormalizedRequirement makeGoal(const std::string& id, const std::string& sourceText, const std::string& normalizedText) { NormalizedRequirement r; r.requirementId = id; r.kind = NormalizedRequirementKind::Goal; r.sourceText = sourceText; r.normalizedText = normalizedText; r.ambiguous = false; r.sourceLine = 1; return r; } static NormalizedRequirement makeRequirement(const std::string& id, const std::string& sourceText, const std::string& normalizedText) { NormalizedRequirement r; r.requirementId = id; r.kind = NormalizedRequirementKind::Requirement; r.sourceText = sourceText; r.normalizedText = normalizedText; r.ambiguous = false; r.sourceLine = 2; return r; } static bool contains(const std::string& haystack, const std::string& needle) { return haystack.find(needle) != std::string::npos; } void t1() { T(goal_title_no_double_implement); RequirementNormalizationResult norm; norm.requirements.push_back(makeGoal( "goal-1", "Implement ITargetResolver pure virtual interface in include/whimp/app/TargetResolver.h", "implement itargetresolver pure virtual interface in include whimp app targetresolver h")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty(), "no milestones"); C(!plan.milestones[0].workstreams.empty(), "no workstreams"); const std::string title = plan.milestones[0].workstreams[0].title; // Must NOT have "Implement implement" (double prefix) std::string lower = title; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); C(!contains(lower, "implement implement"), "double prefix found: " + title); C(title.rfind("Implement ", 0) == 0 || title.rfind("Plan ", 0) == 0, "expected meaningful prefix: " + title); // Must contain the class name from sourceText C(contains(title, "ITargetResolver"), "class name missing: " + title); P(); } void t2() { T(grouped_requirement_title_preserves_requirement_wording); RequirementNormalizationResult norm; norm.requirements.push_back(makeRequirement( "r-1", "Implement control-plane composition types in crates/whimpwm/src/presentation.rs", "implement control plane composition types in crates whimpwm src presentation rs")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty"); const std::string title = plan.milestones[0].workstreams[0].title; std::string lower = title; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); C(!contains(lower, "implement implement"), "double prefix: " + title); C(contains(title, "composition types"), "requirement wording missing: " + title); P(); } void t3() { T(intent_preserves_sourcetext_capitalisation); RequirementNormalizationResult norm; norm.requirements.push_back(makeGoal( "goal-2", "Implement LastFocusResolver using xdotool key forwarding in include/whimp/app/TargetResolver.h", "implement lastfocusresolver using xdotool key forwarding in include whimp app targetresolver h")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty"); const std::string intent = plan.milestones[0].workstreams[0].intent; // Intent must contain the original-case class name C(contains(intent, "LastFocusResolver"), "class name missing from intent: " + intent); // Must NOT be all-lowercase C(intent != "implement lastfocusresolver using xdotool key forwarding in include whimp app targetresolver h", "intent is still all-lowercase: " + intent); P(); } void t4() { T(intent_fallback_to_normalizedtext_when_sourcetext_absent); RequirementNormalizationResult norm; NormalizedRequirement r; r.requirementId = "goal-3"; r.kind = NormalizedRequirementKind::Goal; r.sourceText = ""; // absent r.normalizedText = "implement fiforesolver write key string to fifo"; r.ambiguous = false; r.sourceLine = 3; norm.requirements.push_back(r); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty"); const std::string intent = plan.milestones[0].workstreams[0].intent; // Falls back to normalizedText C(!intent.empty(), "intent empty"); C(contains(intent, "fiforesolver") || contains(intent, "FifoResolver"), "fallback intent wrong: " + intent); P(); } void t5() { T(no_whetstone_step_ids_injected_for_external_spec); // This test validates via the decomposer + taskitem generator output. // We check that stepIds in the executionContract come from the spec, // not from hardcoded whetstone defaults (1939/1940/1941). // The decomposer doesn't emit stepIds — that's contract territory. // We just validate title/intent are correct and no internal IDs leak // from ScopeMilestoneDecomposer itself. RequirementNormalizationResult norm; norm.requirements.push_back(makeGoal( "goal-4", "Implement GazeWindowResolver reading /tmp/whimp_gaze_target", "implement gazewindowresolver reading tmp whimp gaze target")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty"); const std::string title = plan.milestones[0].workstreams[0].title; const std::string intent = plan.milestones[0].workstreams[0].intent; // Verify no whetstone-internal text leaks from decomposer C(!contains(title, "1939") && !contains(title, "1940") && !contains(title, "1941"), "whetstone step ID leaked into title: " + title); C(!contains(intent, "whetstone_") && !contains(intent, "RegisterArchitectIntakeTools"), "whetstone-internal text in intent: " + intent); C(title.rfind("Implement ", 0) == 0 || title.rfind("Plan ", 0) == 0, "expected meaningful prefix: " + title); C(contains(title, "GazeWindowResolver"), "class name missing: " + title); P(); } void t6() { T(grouped_title_uses_requirement_summary_not_generic_bucket); RequirementNormalizationResult norm; norm.requirements.push_back(makeGoal( "goal-5", "`whimpwm` is a Rust-based compositor project.", "whimpwm is a rust based compositor project")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty"); const std::string title = plan.milestones[0].workstreams[0].title; C(title.find("Scaffold minimal compositor runtime") == std::string::npos, "generic bucket title leaked: " + title); C(title.find("whimpwm") != std::string::npos || title.find("Rust-based compositor project") != std::string::npos, "requirement summary missing: " + title); P(); } void t7() { T(work_item_subsections_group_separately); RequirementNormalizationResult norm; norm.requirements.push_back(makeRequirement( "r-w1a", "W1. Introduce Composition Types", "w1 introduce composition types")); norm.requirements.push_back(makeRequirement( "r-w1b", "W1. Introduce Composition Types: add typed layout structs in crates/whimpwm/src/presentation.rs", "w1 introduce composition types add typed layout structs in crates whimpwm src presentation rs")); norm.requirements.push_back(makeRequirement( "r-w2a", "W2. Translate Policy Artifact Into Composition Inputs", "w2 translate policy artifact into composition inputs")); norm.requirements.push_back(makeRequirement( "r-w2b", "W2. Translate Policy Artifact Into Composition Inputs: extend crates/whimpwm/src/presentation_artifact.rs", "w2 translate policy artifact into composition inputs extend crates whimpwm src presentation artifact rs")); DecomposedScopePlan plan; std::string err; C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err); C(!plan.milestones.empty(), "no milestones"); C(plan.milestones[0].workstreams.size() >= 2, "expected multiple workstreams"); bool sawW1 = false, sawW2 = false; for (const auto& ws : plan.milestones[0].workstreams) { if (ws.title.find("W1") != std::string::npos) sawW1 = true; if (ws.title.find("W2") != std::string::npos) sawW2 = true; } C(sawW1, "missing W1 workstream"); C(sawW2, "missing W2 workstream"); P(); } int main() { std::cout << "Step 1989: GR-029 fix — project-grounded taskitem titles, intents, stepIds\n"; t1(); t2(); t3(); t4(); t5(); t6(); t7(); std::cout << "\n" << p << "/" << (p + f) << " passed\n"; return f > 0 ? 1 : 0; }