fix: join FR-keyed story-map rows when user-stories is skipped - #1176
Conversation
At units-generation the traceability sensor resolves source ids with a US-to-FR fallback when stories.md is absent, but storyAssignments() parsed unit-of-work-story-map.md with the US pattern only. A map whose rows carry FR ids yielded no assignments, so every FR was reported as a gap and the sensor could never pass in a scope without user-stories. storyAssignments() now takes the id pattern to join on, and the units-generation branch passes the same pattern it used for the source. The per-unit call sites already sit behind an existsSync(stories) guard and pass US explicitly, so their behaviour is unchanged. t281 adds the FR-only case: complete FR coverage passes, and a row that maps an FR to the wrong unit is still refused through invalid_targets. Closes awslabs#1117
5686315 to
51bdd2c
Compare
|
Rebased onto |
…skipped Issue awslabs#1117's reproduction traces NFR1–NFR5 as OK rows next to the FRs. With the join restricted to the FR pattern, storyAssignments() dropped the NFR rows and verifyTargets() reported each of them as "not mapped in unit-of-work-story-map.md" even though the row exists, so the sensor still failed on the reporter's artifact. storyAssignments() now takes the pattern list and the units-generation branch joins on [FR, NFR] when stories.md is absent. The required upstream set stays FR-only ("otherwise enumerate every FR"); NFR coverage is optional but validates against the map when present. The regression case covers all three: FR-only coverage passes with NFRs in requirements.md, an NFR row joins, and a wrong-unit NFR row is refused.
apackeer
left a comment
There was a problem hiding this comment.
Thanks for this fix, and for the precise root-cause write-up — threading the source fallback through storyAssignments() so the two halves of the join can't disagree is exactly the right shape, and the regression case was well targeted.
I reviewed the current head 02a393f1 against main at 0e54914f. The change restores the FR fallback documented in units-generation.md:129-132, and Leandro's #1179 review already scoped this PR as the complementary follow-up.
During review I found that #1117's own reproduction still failed at 51bdd2cf: the reporter traces NFR1–NFR5 OK alongside the FRs with matching story-map rows, but the FR-only join dropped those rows, so verifyTargets reported each as not mapped in unit-of-work-story-map.md — five false invalid_targets, pass=false. I pushed 02a393f1 on top of your commit: storyAssignments() takes the pattern list and the no-stories join accepts [FR, NFR], while the required upstream set stays FR-only so NFR coverage remains optional. The t281 case now also covers FR-only coverage passing with NFRs in requirements.md, an NFR row joining, and a wrong-unit NFR row being refused. Your sourcePattern became hasStories, which also removes the repeated existsSync.
Validation on 02a393f1: t281 16/16 (each half fails on the prior sensor as claimed), lint, typecheck, coverage registry, packaging determinism across seven harnesses; CI, Markdownlint, and Security Scanners are green on this head.
Approving. One non-blocking follow-up for a separate PR: units-generation.md:124 still says story-map rows are keyed USx.y — worth adding "or FR/NFR when stories.md is not produced" so authors in a no-stories scope know to write those rows.
…ess-v3 Base moved to 9290c03 (seven commits since 488ab10: awslabs#1176, awslabs#1254, awslabs#1150, awslabs#1191, awslabs#1253, awslabs#1230, awslabs#1236), which left the PR DIRTY. Five conflicts, all of them traceable to one upstream commit - 261083c (awslabs#1150, review Construction at verified Unit and batch checkpoints) - and each resolved from the merge stages rather than by taking a side: - core/aidlc-common/protocols/stage-protocol-construction.md - upstream rewrote the per-harness Construction routing for `### Kiro CLI` and `### Kiro IDE` with byte-identical bodies (sha c768b1d0, 3046 bytes each). This branch collapses the two sections into a single `### Kiro`, and the surviving body merged cleanly against upstream's rewrite, so the unified section already carries it. The duplicated CLI block and the `### Kiro IDE` heading are dropped; no upstream change is lost. - core/aidlc-common/protocols/stage-protocol-swarm.md - the same shape, and upstream's two bodies are byte-identical here as well (sha 8223e9d1, 6091 bytes each). - harness/kiro/skills/aidlc/SKILL.md - the `run-stage` row was changed only upstream, so upstream's row is taken; the `ask` row was changed only on this branch, so this branch's row is kept. Row order is preserved. - harness/kiro-ide/skills/aidlc/SKILL.md (modify/delete) - stays deleted. The upstream change it carried is the `legacy-plan-approval-recovery` ask clause, which is already present in the unified harness/kiro/skills/aidlc/SKILL.md on this branch and absent from upstream's kiro copy, so the deletion drops a duplicate rather than a fix. - tests/.coverage-registry.json - regenerated with `bun tests/gen-coverage-registry.ts` instead of being hand-merged, since it is generated output. `--check` reports "OK (fresh, guards green, ratchet held)". tests/.coverage-ratchet.json is the generator's companion output. Verified locally with `bun run typecheck` only (0 errors across all three projects, after `bun scripts/package.ts` generates the gitignored dist trees the registry generator reads). The test battery is deliberately not run here - CI is the gate for this PR.
Summary
When a scope skips
user-stories, thetraceabilitysensor atunits-generationresolves its source ids correctly -resolveUpstream()falls back fromUSids instories.mdtoFRids inrequirements.md- but then resolves unit assignments throughstoryAssignments(), which parsedunit-of-work-story-map.mdwith theUSpattern only (core/tools/aidlc-sensor-traceability.ts:233). A map whose rows carryFR1 | U1 | u1-auththerefore yields zero assignments, and everyFRid is reported as a gap even when each is mapped, so the sensor can never pass for that scope shape.Closes #1117.
Changes
core/tools/aidlc-sensor-traceability.ts-storyAssignments()takes the id pattern to join on. Theunits-generationbranch passes the same pattern it used for the source (USwhenstories.mdexists, otherwiseFR), so the two halves of the join can no longer disagree. The two per-unit call sites (functional-design,code-generation) are already insideexistsSync(stories)guards and passUSexplicitly; their behaviour is unchanged.tests/unit/t281-sensor-traceability.test.ts- new case "units-generation joins FR rows when user-stories is skipped": requirements only, units seeded, an FR-keyed story map, and an FR-keyedtraceability.jsonpass; a row that mapsFR2to the wrong unit is still refused throughinvalid_targets(FR2: target "u2-profile" is not mapped in unit-of-work-story-map.md).No version bump, badge, or CHANGELOG entry, per the Release Metadata Policy in
AGENTS.md.User experience
Before: in any scope without
user-stories(the fallback theunits-generationstage prose describes: "otherwise enumerate every FR"), the sensor reported every FR as a phantom gap atunits-generationand the stage recordedSENSOR_FAILEDregardless of the artifact's content.After: FR-keyed rows join to units the same way US-keyed rows do; the sensor passes on complete coverage and still names a wrong-unit target.
Checklist
Test plan
On
572c8265(main):The new case fails without the sensor change:
git stash push -- core/ && bun test tests/unit/t281-sensor-traceability.test.tsreports 1 failure (passisfalsebecause every FR is a gap).Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.