mandrel 2.24.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.agents/agents/acceptance-critic.md +19 -21
  2. package/.agents/agents/auditor.md +16 -19
  3. package/.agents/agents/story-worker.md +41 -54
  4. package/.agents/audit-checklists/architecture.md +5 -1
  5. package/.agents/audit-checklists/baselines.md +21 -0
  6. package/.agents/audit-checklists/quality.md +3 -0
  7. package/.agents/docs/agentrc-reference.json +2 -1
  8. package/.agents/docs/configuration.md +2 -1
  9. package/.agents/docs/quality-gates.md +80 -18
  10. package/.agents/docs/workflows.md +4 -2
  11. package/.agents/instructions.md +1 -1
  12. package/.agents/rules/ci-remediation.md +68 -3
  13. package/.agents/schemas/agentrc.schema.json +6 -1
  14. package/.agents/schemas/audit-rules.json +15 -0
  15. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
  16. package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
  17. package/.agents/schemas/baselines/crap.schema.json +13 -1
  18. package/.agents/schemas/crap-report.schema.json +37 -0
  19. package/.agents/schemas/model-attribution.schema.json +4 -0
  20. package/.agents/scripts/acceptance-eval.js +124 -15
  21. package/.agents/scripts/audit-baselines.js +136 -0
  22. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  23. package/.agents/scripts/audit-to-stories.js +33 -3
  24. package/.agents/scripts/check-arch-cycles.js +12 -93
  25. package/.agents/scripts/check-baseline-drift.js +16 -3
  26. package/.agents/scripts/check-baselines.js +19 -3
  27. package/.agents/scripts/check-context-budget.js +95 -14
  28. package/.agents/scripts/check-cyclomatic.js +214 -0
  29. package/.agents/scripts/check-schema-references.js +392 -0
  30. package/.agents/scripts/check-test-temp-hygiene.js +121 -1
  31. package/.agents/scripts/check-workflow-timeouts.js +291 -0
  32. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  33. package/.agents/scripts/deliver-light.js +3 -4
  34. package/.agents/scripts/deliver-recover.js +13 -0
  35. package/.agents/scripts/diagnose-friction.js +85 -19
  36. package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
  37. package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
  38. package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
  39. package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
  40. package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
  41. package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
  42. package/.agents/scripts/lib/audit-baselines/read.js +87 -0
  43. package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
  44. package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
  45. package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
  46. package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
  47. package/.agents/scripts/lib/audit-suite/index.js +0 -5
  48. package/.agents/scripts/lib/audit-suite/selector.js +9 -62
  49. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  50. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
  51. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  52. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  53. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  54. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  55. package/.agents/scripts/lib/baseline-loader.js +0 -0
  56. package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
  57. package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
  58. package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
  59. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  60. package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
  61. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  62. package/.agents/scripts/lib/baselines/reader.js +10 -0
  63. package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
  64. package/.agents/scripts/lib/baselines/scope.js +39 -90
  65. package/.agents/scripts/lib/baselines/writer.js +16 -7
  66. package/.agents/scripts/lib/changed-files.js +8 -1
  67. package/.agents/scripts/lib/cli-args.js +115 -1
  68. package/.agents/scripts/lib/close-validation/runner.js +70 -25
  69. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  70. package/.agents/scripts/lib/crap-engine.js +124 -27
  71. package/.agents/scripts/lib/crap-method-identity.js +153 -0
  72. package/.agents/scripts/lib/crap-utils.js +86 -13
  73. package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
  74. package/.agents/scripts/lib/env-loader.js +46 -16
  75. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
  76. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
  77. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
  78. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  79. package/.agents/scripts/lib/findings/severity.js +80 -2
  80. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  81. package/.agents/scripts/lib/git-utils.js +136 -80
  82. package/.agents/scripts/lib/import-graph.js +156 -0
  83. package/.agents/scripts/lib/label-constants.js +17 -0
  84. package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
  85. package/.agents/scripts/lib/observability/source-classifier.js +175 -2
  86. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  87. package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
  88. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
  89. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
  90. package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
  91. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  92. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  94. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
  95. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
  96. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  97. package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
  98. package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
  99. package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
  100. package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
  101. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
  102. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
  103. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
  104. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
  105. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
  106. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
  107. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
  108. package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
  109. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  110. package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
  111. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
  118. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
  119. package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
  120. package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
  121. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
  122. package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
  123. package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
  124. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  125. package/.agents/scripts/lib/story-adjacency.js +3 -3
  126. package/.agents/scripts/lib/story-plan.js +137 -42
  127. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  128. package/.agents/scripts/lib/test-runner-contract.js +134 -0
  129. package/.agents/scripts/lib/test-tiers.js +11 -2
  130. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  131. package/.agents/scripts/lib/util/concurrent-map.js +17 -0
  132. package/.agents/scripts/lib/util/parse-id-list.js +103 -0
  133. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  134. package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
  135. package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
  136. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
  137. package/.agents/scripts/lib/workers/crap-worker.js +8 -13
  138. package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
  139. package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
  140. package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
  141. package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
  142. package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
  143. package/.agents/scripts/lib/worktree-manager.js +14 -0
  144. package/.agents/scripts/plan-run-epilogue.js +17 -5
  145. package/.agents/scripts/pr-watch-with-update.js +568 -213
  146. package/.agents/scripts/providers/github/tickets.js +33 -10
  147. package/.agents/scripts/provision-git-hooks.js +85 -0
  148. package/.agents/scripts/quality-preview.js +137 -28
  149. package/.agents/scripts/resolve-stories.js +4 -1
  150. package/.agents/scripts/run-coverage.js +86 -35
  151. package/.agents/scripts/run-lint.js +20 -0
  152. package/.agents/scripts/run-tests.js +36 -36
  153. package/.agents/scripts/single-story-close.js +34 -2
  154. package/.agents/scripts/single-story-confirm-merge.js +22 -6
  155. package/.agents/scripts/single-story-init.js +7 -0
  156. package/.agents/scripts/stories-wave-tick.js +308 -47
  157. package/.agents/scripts/story-plan.js +65 -9
  158. package/.agents/scripts/update-coverage-baseline.js +34 -4
  159. package/.agents/scripts/update-crap-baseline.js +42 -4
  160. package/.agents/scripts/update-duplication-baseline.js +209 -83
  161. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  162. package/.agents/scripts/validate-docs-freshness.js +1 -0
  163. package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
  165. package/.agents/skills/skills.index.json +2 -2
  166. package/.agents/templates/single-story-body.md +16 -8
  167. package/.agents/workflows/audit-architecture.md +69 -13
  168. package/.agents/workflows/audit-baselines.md +289 -0
  169. package/.agents/workflows/audit-navigability.md +5 -4
  170. package/.agents/workflows/audit-quality.md +26 -0
  171. package/.agents/workflows/audit-to-stories.md +30 -4
  172. package/.agents/workflows/deliver.md +92 -97
  173. package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
  174. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  175. package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
  176. package/.agents/workflows/helpers/deliver-digest.md +41 -21
  177. package/.agents/workflows/helpers/deliver-reference.md +95 -4
  178. package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
  179. package/.agents/workflows/helpers/deliver-story.md +104 -119
  180. package/.agents/workflows/helpers/plan-reference.md +49 -5
  181. package/.agents/workflows/memory-consolidate.md +116 -0
  182. package/.agents/workflows/plan.md +62 -75
  183. package/README.md +13 -6
  184. package/docs/CHANGELOG.md +93 -0
  185. package/lib/cli/update.js +14 -11
  186. package/lib/cli/version-check.js +9 -1
  187. package/package.json +9 -4
  188. package/.agents/schemas/friction-event.schema.json +0 -56
  189. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
@@ -18,7 +18,7 @@ allowed_tools:
18
18
  - **No gate may be skipped.** Failing lint means fix lint, not disable the rule; a failing test means fix the code, not `.skip` or delete the test. Gates are ordered shift-left so cheap checks fail first, and CI failure output is fed back verbatim with the directive to reproduce and fix locally before re-pushing.
19
19
  - **Introducing a gate that asserts on pre-existing state** (doc-drift, lint-vocabulary, dependency-cycle, missing-coverage) MUST land green at merge: either advisory-first (report-only until the backlog is burned down) or with the populated baseline committed in the same change that turns the gate on. Never wire a gate into `requiredChecks` that lands red on latent findings nobody authored.
20
20
  - **Refresh a baseline only when the change is deliberate** — a rename/move, an operator-approved complexity bump, a signed-off perf delta, an intentional API-surface change. Never refresh to paper over an unintentional regression; fix the regression instead.
21
- - Run the kind-specific refresh (`npm run crap:update` / `npm run maintainability:update`; dead-exports and lighthouse have no npm script — regenerate the rows and edit `baselines/dead-exports*.json` / `baselines/lighthouse.json` directly) on the **Story branch**, not on `main`.
21
+ - **Pick the refresh that matches why you are refreshing** — the two are different scopes, and the wrong one silently no-ops. `npm run <kind>:update` is **diff-scoped**: it re-scores only files changed in `origin/main..HEAD` and preserves every other row verbatim. That is what you want after changing code. `npm run <kind>:reanchor` is the same script with `--full-scope`: it re-scores every file in every target dir. That is what you want when the baseline is **stale** — a scorer-version bump, a coverage-shape change, accumulated drift — because a diff-scoped run would leave the stale rows exactly as they were and the staleness finding would re-fire. Kinds: `coverage`, `crap`, `maintainability`, `duplication`; dead-exports and lighthouse have no npm script — regenerate the rows and edit `baselines/dead-exports*.json` / `baselines/lighthouse.json` directly. Run either on the **Story branch**, not on `main`.
22
22
  - Verify the refresh diff is scoped to the relevant `baselines/<kind>.json` (plus cosmetic `package-lock.json` churn only). If unrelated files appear, STOP — the refresh is contaminated. Stage baseline files **explicitly** (`git add baselines/<kind>.json`); never `git add -A` in a refresh commit.
23
23
  - Commit-subject contract: a **Conventional-Commits** subject `chore(baselines): refresh <kind> snapshot for <reason>` — never an ad-hoc leading token like `baseline-refresh:` (commitlint and the planner validator reject it). The body is **mandatory** and non-empty: what changed, why the new floor is correct, and the Story that triggered it.
24
24
  - Add the machine-readable trailer `baseline-refresh: true` (git-trailer `Key: value` style) and `Story: #<storyId>` to the body whenever observability classification matters. Never pass `--no-verify`; the `commit-msg` hook (commitlint) MUST run and pass.
@@ -95,18 +95,24 @@ own as the parseable marker for any future reader.)
95
95
 
96
96
  ### Procedure
97
97
 
98
- | Kind | Update command |
99
- | --------------- | -------------------------------- |
100
- | CRAP | `npm run crap:update` |
101
- | Maintainability | `npm run maintainability:update` |
102
- | Dead-exports | edit `baselines/dead-exports.json` / `baselines/dead-exports-production.json` (rows are `(file, symbol)`; `check-dead-exports.js --json` prints the current rows) |
103
- | Lighthouse | edit `baselines/lighthouse.json` |
104
-
105
- 1. **Run the matching update command** on the Story branch (HEAD must already be
106
- the Story branch, not `main`).
98
+ | Kind | After a code change (diff-scoped) | Stale baseline (full re-anchor) |
99
+ | --------------- | ---------------------------------- | ------------------------------------ |
100
+ | CRAP | `npm run crap:update` | `npm run crap:reanchor` |
101
+ | Maintainability | `npm run maintainability:update` | `npm run maintainability:reanchor` |
102
+ | Coverage | `npm run coverage:update` | `npm run coverage:reanchor` |
103
+ | Duplication | `npm run duplication:update` | `npm run duplication:reanchor` |
104
+ | Dead-exports | edit `baselines/dead-exports.json` / `baselines/dead-exports-production.json` (rows are `(file, symbol)`; `check-dead-exports.js --json` prints the current rows) | same — no scope flag applies |
105
+ | Lighthouse | edit `baselines/lighthouse.json` | same no scope flag applies |
106
+
107
+ 1. **Run the matching command for your reason** — left column after a code
108
+ change, right column for a stale baseline — on the Story branch (HEAD must
109
+ already be the Story branch, not `main`).
107
110
  2. **Verify the diff is scoped** to the relevant `baselines/<kind>.json` (and
108
111
  possibly cosmetic `package-lock.json` churn). Unrelated files → STOP, the
109
- refresh is contaminated.
112
+ refresh is contaminated. Judge contamination by **which files** changed, not
113
+ how many rows moved inside the baseline: a `:reanchor` legitimately rewrites
114
+ most of its own file, so say in the commit body that the breadth is a
115
+ re-anchor and not a mass regression.
110
116
  3. **Author the commit.** Stage the baseline file(s) explicitly, then commit with
111
117
  the Conventional-Commits subject + body trailer above:
112
118
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-07-21T13:53:23.232Z",
2
+ "generatedAt": "2026-08-02T11:35:53.522Z",
3
3
  "generator": "generate-skills-index.js@1",
4
4
  "skills": [
5
5
  {
@@ -48,7 +48,7 @@
48
48
  "category": "core",
49
49
  "path": ".agents/skills/core/diagnose-friction/SKILL.md",
50
50
  "description": "Wrap a shell command with diagnostic capture. On failure, print static suggestions and append a structured `friction` record to the per-Story signals.ndjson stream. Use whenever a script in the orchestration loop invokes a tool whose failure shape we want the analyzer to attribute.",
51
- "policyCapsuleBullets": 8,
51
+ "policyCapsuleBullets": 9,
52
52
  "allowedTools": ["Bash", "Read"],
53
53
  "vendor": null
54
54
  },
@@ -1,17 +1,25 @@
1
1
  # {{title}}
2
2
 
3
- ## Context
3
+ ## Goal
4
4
 
5
- {{context}}
5
+ {{goal}}
6
6
 
7
- ## Acceptance Criteria
7
+ ## Spec
8
8
 
9
- {{acceptanceCriteria}}
9
+ {{spec}}
10
10
 
11
- ## Out of Scope
11
+ ## Changes
12
12
 
13
- {{outOfScope}}
13
+ {{changes}}
14
14
 
15
- ## Notes
15
+ ## Non-Goals
16
16
 
17
- {{notes}}
17
+ {{nonGoals}}
18
+
19
+ <!--
20
+ `acceptance[]` and `verify[]` are the ticket's top-level machine contract.
21
+ Author each list ONCE, at the top level (`--acceptance` / `--verify`); do
22
+ NOT mirror them into this body — persist synthesizes the `## Acceptance`
23
+ and `## Verify` sections from those arrays so the issue still reads as a
24
+ complete executable document.
25
+ -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Audit architectural boundaries, module coupling, and layering violations; emit a structured findings report keyed to High/Medium/Low severity.
2
+ description: Audit architectural boundaries, module coupling, layering violations, and shipped-but-uncalled seams; emit a structured findings report keyed to the canonical severity scale.
3
3
  ---
4
4
 
5
5
  # Architecture & Clean Code Audit
@@ -57,9 +57,14 @@ the failure mode this lens exists to prevent.
57
57
  node .agents/scripts/check-dead-exports.js
58
58
  ```
59
59
 
60
- Each unreferenced export is a grounded candidate. **Cede it** to
61
- audit-clean-code's Dead Code dimension rather than re-deriving it here (see
62
- the deferral in Step 2). When the shipped checker is unavailable, fall back
60
+ Each unreferenced export is a grounded candidate. **Cede the
61
+ merely-unreferenced ones** to audit-clean-code's Dead Code dimension rather
62
+ than re-deriving them here (see the deferral in Step 2); keep the ones that
63
+ are dead *wiring* — a seam a delivery shipped that no live production path
64
+ reaches — for this lens's Shipped-But-Never-Wired dimension. Note that this
65
+ checker only sees unreferenced symbols, so it cannot surface the worst shape
66
+ (a writer with no reader, both fully referenced); that one you must trace by
67
+ hand. When the shipped checker is unavailable, fall back
63
68
  to `npx knip --production` — and heed the `!`-suffix entry-pattern caveat
64
69
  that [`audit-clean-code`](audit-clean-code.md) documents, since
65
70
  `knip --production` is a silent no-op without it.
@@ -116,21 +121,35 @@ legitimately have no layered architecture to guard.
116
121
 
117
122
  ## Step 2: Analysis Dimensions
118
123
 
119
- For every finding you surface, grade **Impact** on a High / Medium / Low axis
120
- reflecting the severity of the architectural risk (how much correctness,
121
- maintainability, or testability the gap erodes), independent of the
122
- **Category** effort axis a Quick Win can still be High Impact, and a
123
- Structural Change can be Medium. As a loose default, Quick Wins typically land
124
- High (cheap to fix, real payoff) and Structural Changes Medium/High, but grade
125
- Impact on the risk itself rather than deriving it mechanically from Category.
124
+ For every finding you surface, grade **Impact** on the **canonical severity
125
+ scale** `Critical | High | Medium | Low | Info`, defined in the core's
126
+ [Severity scale](helpers/audit-lens-core.md#severity-scale) and owned by
127
+ `lib/findings/severity.js`. This lens relabels the axis `Impact` (it reflects how
128
+ much correctness, maintainability, or testability the gap erodes), but the
129
+ *levels* are the shared five and nothing else: a narrower or invented vocabulary
130
+ resolves to no severity and the finding is dropped from every severity-filtered
131
+ run. In particular an architectural defect that is actively data-losing or
132
+ release-blocking grades **Critical**, and a grounded observation that asks for no
133
+ scheduled work grades **Info** — neither collapses into `High` or `Low`.
134
+
135
+ Impact is independent of the **Category** effort axis — a Quick Win can still be
136
+ High Impact, and a Structural Change can be Medium. As a loose default, Quick
137
+ Wins typically land High (cheap to fix, real payoff) and Structural Changes
138
+ Medium/High, but grade Impact on the risk itself rather than deriving it
139
+ mechanically from Category.
126
140
 
127
141
  > **Boundary with `audit-clean-code`.** The clean-code-overlapping smells
128
142
  > (over-engineering & abstractions, cognitive load & nesting, dead code &
129
143
  > redundancy, naming & self-documentation, coupling & cohesion) are owned by
130
144
  > [`audit-clean-code`](audit-clean-code.md); the Step 0 dead-export candidates
131
145
  > flow into its Dead Code dimension. Do **not** duplicate them here. This lens
132
- > keeps only the two structural dimensions no other lens owns — the
133
- > testable-surface boundary and the automated-guardrail maturity.
146
+ > keeps only the three structural dimensions no other lens owns — the
147
+ > testable-surface boundary, the automated-guardrail maturity, and
148
+ > shipped-but-never-wired seams. The split with clean-code's Dead Code dimension
149
+ > is by *question asked*: clean-code asks whether a symbol is referenced at all,
150
+ > this lens asks whether a **live production path** reaches it. An unreferenced
151
+ > helper is clean-code's; a fully-referenced writer whose reader was never built
152
+ > is this lens's.
134
153
 
135
154
  Evaluate the gathered context against the following architecture dimensions:
136
155
 
@@ -208,6 +227,43 @@ Evaluate the gathered context against the following architecture dimensions:
208
227
  full maturity assessment runs only in codebase-wide mode (when
209
228
  `{{changedFiles}}` renders as the literal substitution token).
210
229
 
230
+ 3. **Shipped-But-Never-Wired Seams (mandatory).** Report code this repository
231
+ ships that **no live production caller ever reaches**. This is the failure
232
+ mode the per-Story suite structurally cannot catch: every piece passes its own
233
+ unit tests, the diff looks complete, and the assembled path is dead — so it is
234
+ found only after delivery, if at all. A green suite is not evidence of a live
235
+ path; only a caller is.
236
+
237
+ Walk the seam **from the consumer backwards**, not from the producer forwards.
238
+ For each candidate, name the production entry point you traced to — or state
239
+ that you could not reach one, which is the finding. Cover at least:
240
+
241
+ - **A produced-but-never-consumed artifact.** Something the code computes,
242
+ stamps, writes, or returns that nothing downstream ever reads: an envelope
243
+ field no consumer parses, a file or log written and never opened, a
244
+ provenance marker stamped by the writer with no reader. Both halves must
245
+ exist for the feature to work, and shipping only the writer looks exactly
246
+ like shipping the feature.
247
+ - **An optional field nothing populates.** The mirror image: a parameter,
248
+ config key, or schema property a consumer reads and branches on that no
249
+ caller ever sets. The branch is unreachable, so the behaviour it guards has
250
+ never once run, and the default silently *is* the behaviour.
251
+ - **An exported seam with no in-tree caller** that the core's exclusion list
252
+ does not bless — not a test seam, not a CLI/`exports` entry point, not
253
+ dynamically reached. Cite the exclusion and drop it when it is one of those;
254
+ the Step 0 dead-export reading is the grounding instrument here, and the
255
+ candidates it surfaces that are genuinely dead **internal** wiring belong in
256
+ this dimension rather than ceded to clean-code's Dead Code dimension.
257
+
258
+ Grade Impact by what the dead wiring was supposed to do: a dead *guard, gate,
259
+ or enforcement path* is **High** or **Critical** — the protection it was
260
+ shipped to provide has never been in force, and the gate reads green because
261
+ it never runs. Dead reporting or convenience wiring is **Medium**. Each
262
+ finding's **Acceptance signal** must be the observable that proves the path is
263
+ live: a test that fails when the caller is removed, or a trace from the
264
+ production entry point to the seam. "Added a unit test for the seam" is not
265
+ that signal — the seam already had one.
266
+
211
267
  ### Maturity Rubric
212
268
 
213
269
  Use these definitions to classify the project's `Architecture Guardrail
@@ -0,0 +1,289 @@
1
+ ---
2
+ description: Audit the committed baseline surface — dead instruments, stale baselines, cross-gate hotspot clusters, trend drift, and floor-tightening headroom — and emit findings whose remediation burns the measured debt down and tightens the ratchet behind it.
3
+ ---
4
+
5
+ # Baseline & Ratchet Audit
6
+
7
+ You are a Principal Engineer and Quality-Systems Owner auditing this
8
+ repository's **committed baseline surface**: the ratchet artifacts under
9
+ `baselines/` and the gate floors under `delivery.quality.gates` in
10
+ `.agentrc.json`. Those instruments only prevent **regression** — nothing owns
11
+ the loop that burns the measured debt **down** and tightens the floors behind
12
+ it, so a repo can hold a floor it cleared years ago and never notice. This lens
13
+ is that loop's read-only entry point. The shared lens machinery — read-only
14
+ constraint, scope interpretation, report envelope + finding-block skeleton,
15
+ severity scale, self-cross-check, and execution strategy — lives in
16
+ [`helpers/audit-lens-core.md`](helpers/audit-lens-core.md). Write the report to
17
+ `{{auditOutputDir}}/audit-baselines-results.md`. Dimension values:
18
+ `Dead Instrument | Staleness | Hotspot Cluster | Trend Drift | Tightening Headroom`.
19
+
20
+ > **Value-free titles (mandatory).** A finding title MUST NOT embed a measured
21
+ > number — write ``### `baselines/crap.json` — crap floor holds unused slack``,
22
+ > not ``… — crap floor 13 vs measured 8``. Every re-run re-measures, so a title
23
+ > carrying the reading changes each pass, its fingerprint changes with it, and
24
+ > `/audit-to-stories` files a duplicate instead of deduping against the open
25
+ > Story. Numbers belong in Current State, never in the title.
26
+
27
+ ## Scope
28
+
29
+ Interpret this lens's change-set fence per the core's Scope interpretation:
30
+
31
+ ```text
32
+ {{changedFiles}}
33
+ ```
34
+
35
+ When the fence resolves to a file list, keep only Hotspot Cluster findings
36
+ whose cluster key is in that list. The other four dimensions are properties of
37
+ the instrument set as a whole rather than of any changed file, so report them
38
+ only in codebase-wide mode.
39
+
40
+ ## Constraint (lens-specific carve-out)
41
+
42
+ This lens **refines** the core's read-only constraint; it never relaxes it.
43
+
44
+ - The only command it runs is the read-only engine in Step 0. It never runs a
45
+ test, coverage, mutation, duplication, or lint suite.
46
+ - It never writes under `baselines/` and never edits `.agentrc.json`. It never
47
+ invokes an `update-*-baseline` script. **Regeneration is a finding, never an
48
+ in-run step** — the remediation Story owns every write to the surface.
49
+ - Reading committed baseline rows is explicitly permitted and required: citing
50
+ an already-computed metric is analysis, not measurement.
51
+
52
+ ## Execution strategy
53
+
54
+ Run this lens as a single `subagent_type: auditor` dispatch returning the report
55
+ path + Executive Summary; sequential inline execution is the fallback (see the
56
+ core's Execution strategy).
57
+
58
+ ## Step 0: Run the engine (mandatory — measure before you judge)
59
+
60
+ ```bash
61
+ node .agents/scripts/audit-baselines.js --out temp/audit-baselines/envelope.json
62
+ ```
63
+
64
+ Optional flags: `--cwd` (repository root), `--top-n` (outlier rows per gate,
65
+ default 20), `--hotspot-limit` (clusters emitted, default 50), `--trend-depth`
66
+ (baseline commits sampled per kind, default 5). Exit 0 means evidence was
67
+ assembled **including every degraded input**; exit 1 means the envelope could
68
+ not be built or written — report that and stop, because no evidence base
69
+ exists to author against.
70
+
71
+ The envelope is validated against
72
+ `.agents/schemas/baselines/audit-baselines-envelope.schema.json` and is this
73
+ lens's sole evidence base. Cite its fields by name:
74
+
75
+ | Section | Fields you cite |
76
+ | --- | --- |
77
+ | root | `generatedAt`, `cwd`, `topN`, `configError`, `degradations` |
78
+ | `gateSurface[]` | `kind`, `surface`, `baselinePath`, `configured`, `baselineExists`, `stub`, `rowCount`, `measured` (`unit` plus `value`), `generatedAt`, `staleDays`, `staleCommits`, `surfaceStale`, `deadIgnoreGlobs`, `parseError` |
79
+ | `hotspots[]` | `path`, `gates` (each `kind`, `metric`, `value`, `rowCount`, `severityWeight`), `gateKinds`, `gateCount`, `severityWeight`, `churnWeight`, `centralityWeight`, `frictionWeight`, `rank` |
80
+ | `trend[]` | `kind`, `baselinePath`, `sampleCount`, `from`, `to` (each a `sha` plus `committedAt`), `deltas` |
81
+ | `headroom[]` | `kind`, `axis`, `floor`, `measured`, `direction`, `headroom` |
82
+
83
+ `surface` is `gate` (a closed `delivery.quality.gates` kind) or `ratchet` (an
84
+ out-of-band baseline the CI baselines job owns). `direction` is `gte` or `lte`.
85
+ `rowCount` counts rows **after** per-file aggregation; `measured` is the
86
+ quantity the instrument reports, in its own unit. Cite `measured` when the two
87
+ disagree — 589 dead-export symbols sit in 187 files.
88
+
89
+ Two envelope-level reads come **before** any finding:
90
+
91
+ - **`configError` non-null.** Floors, target directories, and ignore globs
92
+ were unavailable and the engine fell back to default baseline paths. Every
93
+ Tightening Headroom finding would be unfounded this run: file the config
94
+ failure itself as one `Dead Instrument` finding and skip that dimension.
95
+ - **`degradations`.** Each of `gitHistory`, `importGraph`, `frictionLedger`
96
+ reading `true` collapsed its rank multiplier to exactly 1.0, so the hotspot
97
+ ordering is weaker evidence. Name the degraded inputs in the Executive
98
+ Summary; never present a degraded rank as a churn-informed one.
99
+
100
+ ## Step 1: Evaluation Dimensions
101
+
102
+ 1. **Dead Instruments.** An instrument that cannot fail is worse than none: it
103
+ reads green forever and the surface it names looks governed. Four shapes,
104
+ read straight off `gateSurface[]`:
105
+ - `stub` is `true` — zero rows **and** an all-zero rollup, so the gate
106
+ passes vacuously. The engine requires both halves, so a ratchet with
107
+ genuinely nothing to report is never mistaken for a dead one.
108
+ - `configured` is `false` on a `gate` row — a baseline is committed but no
109
+ `delivery.quality.gates` block enforces it, so nothing reads it.
110
+ - `baselineExists` is `false`, or `parseError` is non-null — the instrument
111
+ cannot be read at all.
112
+ - `deadIgnoreGlobs` is non-empty — a configured ignore pattern matches zero
113
+ files. It protects nothing today and silently exempts the next file that
114
+ happens to match it.
115
+
116
+ Grade a stub or unenforced gate **Medium**, a `parseError` on an enforced
117
+ gate **High** (delivery reads that file every run), a dead glob **Low**.
118
+
119
+ 2. **Staleness.** Two clocks. `staleDays` is whole days since the baseline's
120
+ own `generatedAt`; `staleCommits` is commits touching the measured surface
121
+ since the baseline was last committed, with `surfaceStale` its boolean. A
122
+ `null` on either is never a fabricated zero — the stamp is unreadable, git
123
+ cannot answer, or the rows are not file paths — and is itself the finding.
124
+ **`surfaceStale` with `staleDays: 0` is still stale:** refreshed recently in
125
+ wall time, already behind the surface it scores. Grade an enforced gate
126
+ stale beyond roughly a month or `surfaceStale` **Medium**, a `null` stamp
127
+ **Medium**, an unenforced kind **Low** or **Info**.
128
+
129
+ **Regeneration is the remediation, never an in-run step.** The Agent Prompt
130
+ names the matching script and its one-shot acknowledgment:
131
+
132
+ | Kind | Regeneration script | Acknowledgment |
133
+ | --- | --- | --- |
134
+ | `coverage` | `npm run coverage:reanchor` | `COVERAGE_REFRESH=1` |
135
+ | `crap` | `npm run crap:reanchor` | `CRAP_REFRESH=1` |
136
+ | `duplication` | `npm run duplication:reanchor` | `DUPLICATION_REFRESH=1` |
137
+ | `maintainability` | `npm run maintainability:reanchor` | `MAINTAINABILITY_REFRESH=1` |
138
+
139
+ **Prescribe the `:reanchor` script, never the bare `:update` one.** Every
140
+ updater defaults to a **diff-scoped** refresh — only files changed in
141
+ `origin/main..HEAD` are re-scored, and everything else is preserved
142
+ verbatim. That is the right default for "I changed code, re-score what I
143
+ touched", and it is exactly wrong here: a baseline is stale because the
144
+ *world* moved (a scorer bump, a coverage-shape change, months of unrelated
145
+ drift), so a diff-scoped run leaves almost every stale row untouched and
146
+ the staleness finding re-fires on the next sweep. `:reanchor` is the same
147
+ script with `--full-scope`, which re-scores every file in every target
148
+ dir. Confirm the flag on any kind you are unsure of with that script's
149
+ `--help`.
150
+
151
+ Expect a re-anchor to touch far more rows than a code change would — that
152
+ breadth is the point, but say so in the finding so a reviewer can tell a
153
+ re-anchor from a mass regression.
154
+
155
+ The acknowledgment is the kind upper-snaked. It demotes that kind's
156
+ head-vs-base regressions to unchanged **for one run only** — floors stay
157
+ enforced, so a genuine breach is still caught. The durable equivalent is a
158
+ commit in the compared range whose subject carries the gate's `refreshTag`
159
+ (default `baseline-refresh:`) **and** whose diff touches that kind's
160
+ baseline file. Confirm both against
161
+ `node .agents/scripts/check-baselines.js --help` before writing the prompt,
162
+ and never invent an acknowledgment for a kind that ships no regeneration
163
+ script — there, the remediation is to add one, not to hand-edit rows.
164
+
165
+ 3. **Hotspot Clusters.** `hotspots[]` is already the cross-gate join, one entry
166
+ per cluster key, ranked highest first. **Emit one finding per cluster —
167
+ never one per metric row.** A file that is a CRAP outlier and a
168
+ maintainability outlier is one debt item with two symptoms; splitting it
169
+ files two Stories that fight over the same refactor. The cluster key is a
170
+ repository file path for every kind except `lighthouse` (a route) and
171
+ `bundle-size` (a bundle name) — say which it is when it is not a file.
172
+
173
+ Quote `rank` with the four factors behind it — `severityWeight`,
174
+ `churnWeight`, `centralityWeight`, `frictionWeight` — and the per-gate rows
175
+ under `gates`. Grade by breadth first: three or more entries in `gateKinds`
176
+ is **High**, two is **Medium**, one is **Low** unless its `severityWeight`
177
+ alone is extreme.
178
+
179
+ 4. **Trend Drift.** `trend[]` carries newest-versus-previous rollup deltas per
180
+ kind, bracketed by the commits in `from` and `to`. A delta moving **away**
181
+ from the floor is the finding; one moving toward it is headroom the next
182
+ dimension owns. Read the axis's `direction` in `headroom[]` before assigning
183
+ a sign — lower is not universally better. An entry needs `sampleCount` of at
184
+ least 2 to mean anything, and an empty `trend[]` means no readable history:
185
+ record that as **Info** rather than inferring a flat trend from silence.
186
+ Each `deltas` key **names its unit** — `symbols`, `bytes`, `filesTracked` —
187
+ so quote the axis with the number, never a bare delta.
188
+
189
+ 5. **Tightening Headroom.** `headroom[]` is what this lens exists for. Positive
190
+ headroom is slack the floor could be tightened into; negative headroom means
191
+ the floor is already breached — grade that **High** and route it as a
192
+ regression, not an opportunity. File a tightening finding only when the
193
+ slack is **durable**: the same kind's trend is flat or improving. A one-run
194
+ dip tightened into a floor turns the next honest change red for no defect.
195
+ Grade durable multi-point slack **Medium**, marginal slack **Low**.
196
+
197
+ ## Step 2: Hotspot budget and the dropped log
198
+
199
+ Cap the Detailed Findings at the **top 8 hotspot clusters by `rank`**. The
200
+ engine emits up to `--hotspot-limit` clusters, and the point of the lens is a
201
+ ranked actionable batch, not an exhaustive dump nobody schedules.
202
+
203
+ A silent truncation reads as full coverage, so the report MUST carry a
204
+ **Dropped Hotspots** section naming every cluster the cap excluded with its
205
+ cluster key, `rank`, and `gateKinds`. Write `_None dropped._` when the cap did
206
+ not bite; the section's absence is itself a defect. This budget log is separate
207
+ from — and additional to — the core's self-cross-check `kept / dropped` line,
208
+ which counts evidence-bar drops rather than budget drops. State the cap in the
209
+ Executive Summary and change it only on an explicit operator instruction.
210
+
211
+ ## Step 3: The floor-tightening contract (mandatory)
212
+
213
+ A remediation Story that only burns debt down leaves the floor where it was,
214
+ and the reclaimed slack is silently re-spent by the next change — the loop
215
+ runs and the ratchet never moves. So **every Hotspot Cluster and Tightening
216
+ Headroom finding's Agent Prompt MUST** end the remediation with the ratchet
217
+ tightened and gate-enforced:
218
+
219
+ 1. Lower the floor under `delivery.quality.gates` in `.agentrc.json` to the
220
+ newly measured level, **or** delete the burnt-down rows from that kind's
221
+ file under `baselines/`.
222
+ 2. Carry `node .agents/scripts/check-baselines.js --gate <kind>` in the
223
+ remediation Story's `verify[]`, so the tightened floor is enforced by the
224
+ gate that already exists at that Story's delivery time rather than by prose
225
+ nobody runs.
226
+
227
+ Use these two Agent Prompt templates verbatim, substituting the envelope's own
228
+ values for the angle-bracketed slots:
229
+
230
+ - **Hotspot Cluster template:**
231
+ `Burn down the measured debt in <hotspots.path>, an outlier across <gateKinds>. Refactor and add tests until its rows leave that kind's file under baselines/, then regenerate that baseline with the matching update-*-baseline script in a commit whose subject carries the baseline-refresh: tag. Finish by TIGHTENING the ratchet in the same Story — lower the kind's floor under delivery.quality.gates in .agentrc.json to the new measured level, or delete the burnt-down rows — and carry node .agents/scripts/check-baselines.js --gate <kind> in this Story's verify[] so the tightened floor is enforced at delivery.`
232
+ - **Tightening Headroom template:**
233
+ `The <kind> gate's <axis> floor sits at <floor> while the measured rollup is <measured> (headroom <headroom>, direction <direction>), and that kind's trend is flat or improving. Tighten it: set that axis under delivery.quality.gates in .agentrc.json to the measured level so no slack remains for the next change to re-spend, and carry node .agents/scripts/check-baselines.js --gate <kind> in this Story's verify[] so the new floor is enforced. Regenerate no baseline in this Story — the floor edit is the whole change.`
234
+
235
+ Staleness, Dead Instrument, and Trend Drift findings do **not** carry the
236
+ tightening clause: there is no measured slack to claim until the instrument is
237
+ alive and current again.
238
+
239
+ ## Step 4: Cadence (host-owned — documented, never scheduled)
240
+
241
+ This lens ships **no scheduler**, and building one is out of scope; cadence
242
+ belongs to the host that invokes it. Document the intent and let the operator
243
+ or the host's own timer drive it: **monthly** for a codebase-wide pass (long
244
+ enough for `trend[]` to hold signal, short enough to catch a stale instrument
245
+ before a release leans on it); **after a large refactor lands**, when headroom
246
+ appears and is most likely to be silently re-spent; and **before any floor is
247
+ raised**, so the raise is argued against measured headroom rather than
248
+ convenience. Nothing here self-triggers.
249
+
250
+ ## Step 5: Hand off to `/audit-to-stories`
251
+
252
+ The report is the deliverable. Hand it to the converter, which parses the
253
+ shared finding skeleton, fingerprints each finding for dedupe, and groups the
254
+ batch:
255
+
256
+ ```bash
257
+ node .agents/scripts/audit-to-stories.js --scan --glob temp/audits/audit-baselines-results.md --out temp/audits/audit-to-stories-plan.json
258
+ ```
259
+
260
+ Report the plan path and the group count; the converter owns everything
261
+ downstream, including whether a finding becomes a Story at all.
262
+
263
+ ## Report additions
264
+
265
+ Beyond the shared skeleton (Executive Summary + Detailed Findings from the
266
+ core), this report carries its own title, a Gate Surface Health table, a
267
+ Tightening Ledger, and the Dropped Hotspots budget log:
268
+
269
+ ```markdown
270
+ # Baseline & Ratchet Audit Report
271
+
272
+ ## Gate Surface Health
273
+
274
+ | Kind | Surface | Configured | Rows | Measured | Stale (days) | Stale (commits) | Verdict |
275
+ | --- | --- | --- | --- | --- | --- | --- | --- |
276
+ | [kind] | [gate / ratchet] | [yes / no] | [rowCount] | [measured.value measured.unit] | [staleDays or `null`] | [staleCommits or `null`] | [Live / Stub / Unenforced / Unreadable] |
277
+
278
+ ## Tightening Ledger
279
+
280
+ | Kind | Axis | Floor | Measured | Headroom | Trend | Proposed floor |
281
+ | --- | --- | --- | --- | --- | --- | --- |
282
+ | [kind] | [axis] | [floor] | [measured] | [headroom] | [improving / flat / worsening] | [value] |
283
+
284
+ ## Dropped Hotspots
285
+
286
+ | Cluster key | Rank | Gates |
287
+ | --- | --- | --- |
288
+ | [key] | [rank] | [gateKinds] |
289
+ ```
@@ -42,10 +42,11 @@ touched only one route file. Reachability is a global property: adding one
42
42
  route can orphan it, but removing or renaming a route elsewhere can also break
43
43
  a nav href that the change set never touched.
44
44
 
45
- Because of this, the navigability lens is registered in the **global-lens
46
- allowlist** (`GLOBAL_LENS_ALLOWLIST` in
47
- [`lib/audit-suite/selector.js`](../scripts/lib/audit-suite/selector.js)) and is
48
- **exempt from the cross-epic-leak guard** that narrows every other lens's
45
+ Because of this, the navigability lens declares `"scope": "global"` in
46
+ [`audit-rules.json`](../schemas/audit-rules.json) — the single source of truth
47
+ `resolveLensTier` in
48
+ [`lib/audit-suite/selector.js`](../scripts/lib/audit-suite/selector.js) reads
49
+ and is **exempt from the cross-epic-leak guard** that narrows every other lens's
49
50
  evidence to the change set's `changedFiles`. The exemption is scoped to this
50
51
  lens only — the guard is **not** weakened for any other lens, and it never lets
51
52
  a foreign change set leak into a scoped lens.
@@ -107,6 +107,32 @@ Evaluate the gathered context against the following test quality dimensions:
107
107
  criteria to ensure they have corresponding and complete test coverage.
108
108
  Verify that the implementation found in the codebase correctly matches the
109
109
  architectural requirements and highlight any inconsistencies or gaps.
110
+ 7. **Unwired Seams — Coverage Without a Caller (mandatory).** Report code the
111
+ suite **covers** but no live production path **calls**. This is the blind spot
112
+ this dimension exists for, and it is a property of the suite, not of the code:
113
+ a seam with its own passing unit test reports as covered, contributes to the
114
+ coverage number, and is never exercised in assembly — so the suite reads green
115
+ over wiring that has never run once. High coverage is therefore not evidence
116
+ of a live path; it is what conceals a dead one. Cover at least:
117
+
118
+ - **A produced-but-never-consumed artifact** — a field, file, or marker the
119
+ code writes that no test and no consumer ever reads back. A writer test that
120
+ asserts on the writer's own output is not a reader.
121
+ `assert(written === expected)` proves the write, never the round-trip.
122
+ - **An optional field nothing populates** — a parameter or config key a
123
+ consumer branches on that only *tests* ever set. The suite covers both
124
+ branches; production has only ever taken the default.
125
+
126
+ For each, name the missing test rather than the missing caller: the gap is
127
+ that **no test would fail if the wiring were deleted**. That is the assertion
128
+ to recommend — an integration-tier test that drives the real production entry
129
+ point and fails when the seam is unwired. Grade a dead guard/gate **High** or
130
+ **Critical** (the enforcement has never been in force and the bar reads green
131
+ because it never runs), other dead wiring **Medium**. The core's exclusion
132
+ list still applies: a sanctioned test seam or a declared entry point is not a
133
+ finding here. Route the *architectural* framing of the same defect to
134
+ [`audit-architecture`](audit-architecture.md)'s Shipped-But-Never-Wired
135
+ dimension; this lens owns the **missing-test** framing.
110
136
 
111
137
  ## Constraint (lens-specific carve-out)
112
138
 
@@ -143,9 +143,24 @@ Chain into the existing planning entrypoint:
143
143
 
144
144
  (`/plan --seed "$(cat <path>)"` also works for small seeds). `/plan`
145
145
  then runs its author → persist path, as documented in its workflow.
146
- Each Story it spawns from the seed carries `context::audit:
147
- <reportLink>` and `audit-fingerprint: <sha>` in its body so future
148
- Phase 6 idempotency works on the next run.
146
+
147
+ **Dedup provenance is carried mechanically do not hand-copy it.** The seed's
148
+ MVP Scope bullets carry each group's `audit-fingerprints` and
149
+ `audit-semantic-keys` footers as HTML comments (invisible in the rendered
150
+ one-pager). `plan-persist` harvests them out of the seed on the
151
+ `plan-context.json` envelope and appends them to **every** Story body it
152
+ persists, via `carryProvenanceFooters`
153
+ ([`lib/findings/route-finding.js`](../scripts/lib/findings/route-finding.js)).
154
+ The carry is additive, union-preserving and idempotent, so a resumed persist
155
+ cannot stack footers and a hand-authored fingerprint is never dropped.
156
+
157
+ This is deliberately not an authoring step. It used to be: the footers reached
158
+ the seed and stopped there, leaving the authoring agent to notice HTML comments
159
+ in a one-pager and copy them forward — a remembered step, which is to say no
160
+ step at all. Stories filed on the recommended path were therefore invisible to
161
+ the next sweep's Phase 6 dedup, which re-filed work it had already planned. If
162
+ you find yourself copying a footer by hand, the carry is broken — fix it there
163
+ rather than papering over it in the body.
149
164
 
150
165
  ## Phase 5b — Standalone-Stories path
151
166
 
@@ -261,7 +276,18 @@ When the single-plan path ran, link the Story (or plan-run) the chained
261
276
  dedup/route implementation, shared with `qa-explore`.
262
277
  - **Always** stamp the fingerprint footer (via the helper's
263
278
  `fingerprintFooter`) in the body of every created Story. Without it,
264
- the next run cannot dedupe.
279
+ the next run cannot dedupe. On the Single-plan path this is mechanical
280
+ (`carryProvenanceFooters`, Phase 5a) — never an authoring step.
281
+ - **Never** re-mint a finding fingerprint while normalising a finding.
282
+ `severity` and `labels` are identity fields folded into the sha, so
283
+ normalising either without holding the hash stable silently breaks
284
+ dedup for every finding already filed. The projection that keeps the
285
+ fingerprint invariant under normalisation is
286
+ `severity.js#fingerprintSeverity`; the contract is pinned by
287
+ `tests/lib/findings/route-finding.contract.test.js`.
288
+ - **Always** grade findings on the canonical five-level scale
289
+ (`lib/findings/severity.js`). A level outside it parses as no severity
290
+ and the finding is dropped by every severity-filtered run.
265
291
  - **Always** present the Phase 2, 3, and 4 HITL gates. Do not bypass —
266
292
  even when "obvious" — because the severity / grouping / mode picks
267
293
  are operator decisions that the workflow's UX contract relies on.