mandrel 2.31.0 → 2.32.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 (250) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -75,11 +75,80 @@ export function readKnipOutput(filePath) {
75
75
  }
76
76
  }
77
77
 
78
+ /**
79
+ * Sentinel `symbol` recorded for a **whole-file death** row.
80
+ *
81
+ * Knip's `files` category names a module nothing in the graph imports. There
82
+ * is no per-export identity to record — the whole file is the finding — so the
83
+ * ratchet encodes exactly one row per dead file carrying this symbol. `*` is
84
+ * not a legal JavaScript identifier, so a whole-file row can never collide
85
+ * with a real export row for the same path.
86
+ */
87
+ const WHOLE_FILE_SYMBOL = '*';
88
+
89
+ /**
90
+ * Pull the dead-file paths out of one knip issue's `files` category.
91
+ *
92
+ * Knip emits `files: [{ name: '<path>' }]`, but tolerate a bare string and
93
+ * fall back to the issue's own `file` so a reporter-shape change degrades to
94
+ * "one row for this path" rather than to silence.
95
+ *
96
+ * @param {{ files?: unknown }} issue
97
+ * @param {string} fallbackFile The issue's own `file` path.
98
+ * @returns {string[]}
99
+ */
100
+ function extractDeadFileNames(issue, fallbackFile) {
101
+ const entries = Array.isArray(issue.files) ? issue.files : [];
102
+ const names = [];
103
+ for (const entry of entries) {
104
+ const name =
105
+ (typeof entry === 'string' && entry) ||
106
+ (entry && typeof entry.name === 'string' && entry.name) ||
107
+ fallbackFile;
108
+ if (typeof name === 'string' && name.length > 0) names.push(name);
109
+ }
110
+ return names;
111
+ }
112
+
113
+ /**
114
+ * Pull the dead-export symbol names out of one knip issue's `exports`
115
+ * category. Knip emits `exports: [{ name, ... }]`; older shapes used `symbol`.
116
+ *
117
+ * @param {{ exports?: unknown }} issue
118
+ * @returns {string[]}
119
+ */
120
+ function extractDeadExportSymbols(issue) {
121
+ const entries = Array.isArray(issue.exports) ? issue.exports : [];
122
+ const symbols = [];
123
+ for (const e of entries) {
124
+ const symbol =
125
+ (e && typeof e.name === 'string' && e.name) ||
126
+ (e && typeof e.symbol === 'string' && e.symbol) ||
127
+ null;
128
+ if (symbol) symbols.push(symbol);
129
+ }
130
+ return symbols;
131
+ }
132
+
78
133
  /**
79
134
  * Flatten knip's `--reporter json` output into `{ file, symbol }` rows. Knip
80
- * emits `{ issues: [{ file, exports: [{ name, ... }], ... }, ...] }`. Only
81
- * `exports` rows are mapped — the ratchet ignores file-, dependency- and
82
- * duplicate-level issues, which knip surfaces under separate `rules` keys.
135
+ * emits `{ issues: [{ file, files: [...], exports: [{ name, ... }], ... }] }`.
136
+ *
137
+ * Two categories are mapped, and the ratchet treats their rows identically:
138
+ *
139
+ * - **`exports`** → one row per unused export, `{ file, symbol: '<name>' }`.
140
+ * - **`files`** → one row per module nothing imports,
141
+ * `{ file, symbol: '*' }`. Story #5001 added this leg: mapping only
142
+ * `exports` made the ratchet structurally blind to *whole-file* death, so a
143
+ * module could lose its last caller and every one of its exports go unused
144
+ * without a single row changing. Knip reports such a module once, under
145
+ * `files`, and suppresses its per-export rows — which is exactly why the
146
+ * export-only reading saw nothing.
147
+ *
148
+ * Whole-file rows are de-duplicated by path so the row set is stable across
149
+ * runs regardless of how many issue records mention the same file. Dependency-
150
+ * and duplicate-level issues stay ignored; knip surfaces those under their own
151
+ * `rules` keys and they are not a code-death signal.
83
152
  *
84
153
  * @param {unknown} knipEnvelope The parsed knip JSON report.
85
154
  * @returns {Array<{ file: string, symbol: string }>}
@@ -88,16 +157,16 @@ export function extractRowsFromKnip(knipEnvelope) {
88
157
  const rows = [];
89
158
  if (!knipEnvelope || typeof knipEnvelope !== 'object') return rows;
90
159
  const issues = Array.isArray(knipEnvelope.issues) ? knipEnvelope.issues : [];
160
+ const seenDeadFiles = new Set();
91
161
  for (const issue of issues) {
92
162
  const file = issue?.file;
93
163
  if (typeof file !== 'string' || file.length === 0) continue;
94
- const exports_ = Array.isArray(issue.exports) ? issue.exports : [];
95
- for (const e of exports_) {
96
- const symbol =
97
- (e && typeof e.name === 'string' && e.name) ||
98
- (e && typeof e.symbol === 'string' && e.symbol) ||
99
- null;
100
- if (!symbol) continue;
164
+ for (const name of extractDeadFileNames(issue, file)) {
165
+ if (seenDeadFiles.has(name)) continue;
166
+ seenDeadFiles.add(name);
167
+ rows.push({ file: name, symbol: WHOLE_FILE_SYMBOL });
168
+ }
169
+ for (const symbol of extractDeadExportSymbols(issue)) {
101
170
  rows.push({ file, symbol });
102
171
  }
103
172
  }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Tech Spec #819 §"Degraded-mode contract (Story 3)" — three soft-fail sites
5
5
  * (audit-suite selector diff-timeout, lint-baseline JSON-parse, baseline-refresh
6
- * guardrail git-diff) historically returned a silent zero/empty result. The
7
- * new contract is:
6
+ * guardrail git-diff) historically returned a silent zero/empty result. Two
7
+ * remain; the lint-baseline site left with its CLI. The contract is:
8
8
  *
9
9
  * { ok: false, degraded: true, reason: <code>, detail: <human> }
10
10
  *
@@ -201,9 +201,9 @@ export function resolveAlwaysLoadedClosure(root, { fs = nodeFs } = {}) {
201
201
 
202
202
  /**
203
203
  * Read the resolved `project.docsContextFiles` list from a config object,
204
- * prefixed by `project.paths.docsRoot`. Mirrors the `contextDocs` half of
205
- * `resolveDocList` in `validate-docs-freshness.js` (new-shape first, legacy
206
- * top-level fallback).
204
+ * prefixed by `project.paths.docsRoot`. Resolves new-shape
205
+ * `project.docsContextFiles` first and falls back to the legacy top-level
206
+ * key, so a config written against either shape yields the same list.
207
207
  *
208
208
  * @param {object} config resolved config (`resolveConfig()` output)
209
209
  * @returns {string[]} repo-relative posix doc paths (existence not yet checked)
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * graduator-core.js — shared mechanism for the feedback-loop graduators.
3
3
  *
4
- * Story #3845 / Epic #3823. The audit-results graduator
5
- * (`audit-results-graduator.js`) and the code-review graduator
6
- * (the since-retired `code-review-graduator.js`) duplicated ~90% of their mechanism: a
4
+ * Story #3845 / Epic #3823. The since-retired audit-results and code-review
5
+ * graduators duplicated ~90% of their mechanism: a
7
6
  * `spawn`-based child runner, the `git cat-file` path probe, the
8
7
  * `gh search issues` idempotency probe, the `gh issue create` filer, the
9
8
  * `isAutoFileEnabled` toggle reader, and the route → probe → file
10
- * envelope walk. This module folds all of that into one place so the two
11
- * graduators become thin shells that inject the bits that genuinely
12
- * differ (the finding parser and the body/title/label builders).
9
+ * envelope walk. This module folds all of that into one place so a
10
+ * graduator becomes a thin shell that injects the bits that genuinely
11
+ * differ (the body/title/label builders).
12
+ *
13
+ * Story #5003 removed the structured-comment read limb. Both comment-reading
14
+ * graduators were Epic-era — they walked an Epic's `verification-results`
15
+ * comment, and v2 has no Epics — leaving `retro-proposals-graduator.js` and
16
+ * its pre-parsed `findings[]` as the one live caller.
13
17
  *
14
18
  * The third `runGh` spawn copy in `prior-feedback-fetcher.js` is also
15
19
  * collapsed onto the single `runChild` helper here.
@@ -34,11 +38,10 @@
34
38
  * - **Per-run filing cap.** `graduate()` stops filing once
35
39
  * `maxFilingsPerRun` issues are created and records the excess as
36
40
  * skipped `cap-reached`.
37
- * - **Pre-parsed / path-less seam.** `graduate()` accepts a pre-parsed
38
- * `findings` array (bypassing structured-comment parsing), and a
39
- * path-less finding skips the path-exists gate instead of being
40
- * misclassified `file-removed` — the seam the retro auto-filer
41
- * consumes.
41
+ * - **Pre-parsed / path-less seam.** `graduate()` takes its findings as a
42
+ * pre-parsed array, and a path-less finding skips the path-exists gate
43
+ * instead of being misclassified `file-removed` the seam the retro
44
+ * auto-filer consumes.
42
45
  * - **Durable cross-repo deferral.** Cross-repo-deferred findings are
43
46
  * upserted into a structured comment on the Epic instead of only a
44
47
  * log line.
@@ -50,34 +53,7 @@ import { createHash } from 'node:crypto';
50
53
  import { inNodeTestContext } from '../config/temp-paths.js';
51
54
  import { LABEL_COLORS } from '../label-constants.js';
52
55
  import { classifyPathSource as defaultClassifier } from '../observability/source-classifier.js';
53
- import {
54
- structuredCommentMarker,
55
- upsertStructuredComment,
56
- } from '../orchestration/ticketing.js';
57
-
58
- /**
59
- * The single structured-comment marker for the unified `verification-results`
60
- * findings contract (Story #4411, Epic #4405). It replaces the two retired
61
- * per-graduator markers (the former code-review and audit-results
62
- * structured-comment markers). Both feedback-loop graduators
63
- * search the Epic's comments for this one marker so they file follow-ups
64
- * from the same unified source comment that `runCodeReview` upserts (comment
65
- * type `verification-results`). Derived from the canonical
66
- * `structuredCommentMarker` builder so the read-side marker stays byte-stable
67
- * with the write side rather than being hand-copied.
68
- */
69
- export const VERIFICATION_RESULTS_MARKER = structuredCommentMarker(
70
- 'verification-results',
71
- );
72
-
73
- /**
74
- * The single "no source comment" skip reason for the unified contract. Both
75
- * graduators surface this reason when the Epic carries no
76
- * `verification-results` comment, replacing the two retired
77
- * `no-code-review-comment` / `no-audit-results-comment` reasons.
78
- */
79
- export const NO_VERIFICATION_RESULTS_COMMENT_REASON =
80
- 'no-verification-results-comment';
56
+ import { upsertStructuredComment } from '../orchestration/ticketing.js';
81
57
 
82
58
  /**
83
59
  * Default child-process timeout. A hung `gh`/`git` spawn previously blocked
@@ -814,28 +790,24 @@ export async function createFollowUpIssue({
814
790
  }
815
791
 
816
792
  /**
817
- * Validate the `graduate` preconditions (toggle, epicId, provider shape,
818
- * currentRepo shape). Returns `null` when all preconditions pass, or a
819
- * `{ skipped?, errors? }` partial-envelope the caller short-circuits on.
820
- * Story #4075 — extracted from `graduate` so the orchestrating body holds
821
- * no guard-chain branching.
793
+ * Validate the `graduate` preconditions (toggle, epicId, currentRepo shape).
794
+ * Returns `null` when all preconditions pass, or a `{ skipped?, errors? }`
795
+ * partial-envelope the caller short-circuits on. Story #4075 — extracted from
796
+ * `graduate` so the orchestrating body holds no guard-chain branching.
797
+ *
798
+ * Story #5003 dropped the `provider.getTicketComments` gate with the
799
+ * structured-comment read limb. The provider is now consulted only by the
800
+ * best-effort cross-repo-deferred upsert, which reports its own faults into
801
+ * `errors[]` — gating the whole walk on a shape only that path needs would
802
+ * refuse work the walk can complete.
822
803
  */
823
- function checkGraduatePreconditions({
824
- epicId,
825
- provider,
826
- currentRepo,
827
- config,
828
- spec,
829
- }) {
804
+ function checkGraduatePreconditions({ epicId, currentRepo, config, spec }) {
830
805
  if (!spec.isAutoFileEnabled(config)) {
831
806
  return { skipped: [{ reason: 'toggle-disabled' }] };
832
807
  }
833
808
  if (!Number.isInteger(epicId) || epicId < 1) {
834
809
  return { errors: [`${spec.fnName}: missing or invalid epicId`] };
835
810
  }
836
- if (!provider || typeof provider.getTicketComments !== 'function') {
837
- return { errors: [`${spec.fnName}: provider lacks getTicketComments`] };
838
- }
839
811
  if (
840
812
  !currentRepo ||
841
813
  typeof currentRepo.owner !== 'string' ||
@@ -846,36 +818,6 @@ function checkGraduatePreconditions({
846
818
  return null;
847
819
  }
848
820
 
849
- /**
850
- * Read the source structured comment off the Epic and parse its findings.
851
- * Returns `{ findings }` on success, or `{ skipped?, errors? }` for the
852
- * no-comment / parse-empty / fetch-error short-circuits. Story #4075 —
853
- * extracted from `graduate`.
854
- */
855
- async function loadGraduateFindings({ epicId, provider, spec }) {
856
- let comments;
857
- try {
858
- comments = await provider.getTicketComments(epicId);
859
- } catch (err) {
860
- return {
861
- errors: [
862
- `getTicketComments failed for epic #${epicId}: ${err?.message ?? err}`,
863
- ],
864
- };
865
- }
866
- const matched = (Array.isArray(comments) ? comments : []).filter(
867
- (c) => typeof c?.body === 'string' && c.body.includes(spec.commentMarker),
868
- );
869
- if (matched.length === 0) {
870
- return { skipped: [{ reason: spec.noCommentReason }] };
871
- }
872
- const findings = spec.parseFindings(matched[matched.length - 1].body);
873
- if (findings.length === 0) {
874
- return { skipped: [{ reason: 'no-non-blocking-findings' }] };
875
- }
876
- return { findings };
877
- }
878
-
879
821
  /**
880
822
  * Probe whether a finding was already filed, checking both the current
881
823
  * content-hash marker AND the legacy `(epicId, parse-index)` marker so
@@ -1267,17 +1209,16 @@ async function persistCrossRepoDeferred({
1267
1209
  }
1268
1210
 
1269
1211
  /**
1270
- * Parametrized graduator walk. Parses non-blocking findings (from the
1271
- * Epic's structured comment, or a pre-parsed `findings` array), then for
1272
- * each finding runs the shared route → path probe → idempotency probe →
1212
+ * Parametrized graduator walk. Takes a pre-parsed `findings` array and, for
1213
+ * each finding, runs the shared route path probe → idempotency probe →
1273
1214
  * cap → file sequence. Never throws — every failure path is captured in
1274
1215
  * `errors[]`.
1275
1216
  *
1217
+ * Each finding MUST carry `{ severity, path, summary, index }` and MAY carry
1218
+ * additional fields (e.g. `category`) that the builders use.
1219
+ *
1276
1220
  * The per-graduator variation lives entirely in the injected callbacks:
1277
1221
  *
1278
- * - `parseFindings(body)` — turns the rendered comment into findings.
1279
- * Each finding MUST carry `{ severity, path, summary, index }` and
1280
- * MAY carry additional fields (e.g. `lens`) that the builder uses.
1281
1222
  * - `buildContentMarker(epicId, finding)` — the content-hash HTML-comment
1282
1223
  * marker embedded in (and searched for in) follow-up bodies.
1283
1224
  * - `buildLegacyMarker(epicId, index)` — the pre-cutover ordinal marker,
@@ -1299,7 +1240,8 @@ async function persistCrossRepoDeferred({
1299
1240
  *
1300
1241
  * @param {object} opts
1301
1242
  * @param {number} opts.epicId
1302
- * @param {object} opts.provider — exposes `getTicketComments(ticketId)`
1243
+ * @param {object} opts.provider — exposes `postComment(ticketId, body)` for
1244
+ * the durable cross-repo-deferred persistence
1303
1245
  * @param {object} [opts.config]
1304
1246
  * @param {{owner: string, repo: string}} opts.currentRepo
1305
1247
  * @param {{owner: string, repo: string}} [opts.frameworkRepo]
@@ -1310,9 +1252,9 @@ async function persistCrossRepoDeferred({
1310
1252
  * @param {string} [opts.cwd]
1311
1253
  * @param {number} [opts.timeoutMs] — per-spawn watchdog bound
1312
1254
  * @param {number} [opts.maxFilingsPerRun] — per-run filing cap
1313
- * @param {Array<object>} [opts.findings] — pre-parsed findings; when
1314
- * provided, the structured-comment read/parse is bypassed (the retro
1315
- * auto-filer seam).
1255
+ * @param {Array<object>} opts.findings — the pre-parsed findings to file.
1256
+ * Required: a non-array is an `errors[]` short-circuit, never a silent
1257
+ * no-op.
1316
1258
  * @param {Set<string>} [opts.filedMarkers] — in-process memo of content
1317
1259
  * markers filed so far. Pass a shared Set across multiple `graduate()`
1318
1260
  * calls in one logical invocation (e.g. the retro graduator's two source
@@ -1360,23 +1302,23 @@ export async function graduate({
1360
1302
 
1361
1303
  const precondition = checkGraduatePreconditions({
1362
1304
  epicId,
1363
- provider,
1364
1305
  currentRepo,
1365
1306
  config,
1366
1307
  spec,
1367
1308
  });
1368
1309
  if (precondition) return { ...envelope, ...precondition };
1369
1310
 
1370
- let findings;
1371
- if (Array.isArray(preParsedFindings)) {
1372
- // Pre-parsed seam (retro auto-filer): bypass the structured-comment
1373
- // read + parse entirely and file the supplied findings directly.
1374
- findings = preParsedFindings;
1375
- } else {
1376
- const loaded = await loadGraduateFindings({ epicId, provider, spec });
1377
- if (!loaded.findings) return { ...envelope, ...loaded };
1378
- findings = loaded.findings;
1311
+ // Pre-parsed findings are the only source (Story #5003). The
1312
+ // structured-comment read/parse limb went with the audit-results
1313
+ // graduator: it walked an Epic's `verification-results` comment, and v2
1314
+ // has no Epics, so it could only ever resolve zero findings.
1315
+ if (!Array.isArray(preParsedFindings)) {
1316
+ return {
1317
+ ...envelope,
1318
+ errors: [`${spec.fnName}: findings[] is required and must be an array`],
1319
+ };
1379
1320
  }
1321
+ const findings = preParsedFindings;
1380
1322
 
1381
1323
  // Blast-radius guard (Story #4837): decided ONCE per walk, before the
1382
1324
  // first spawn, so a refused context costs no child process at all.
@@ -138,6 +138,14 @@ function metaSourceLabel(source) {
138
138
  * a single source so the constant classifier routes every finding in the
139
139
  * bucket to the correct repo (and the label reflects that source).
140
140
  *
141
+ * The bundle carries **only** builders. It used to declare a
142
+ * `commentMarker` / `noCommentReason` / `parseFindings` trio purely to satisfy
143
+ * the shared walk's shape — this graduator supplies its findings pre-parsed,
144
+ * so the walk never consulted any of the three. Story #5003 deleted that limb
145
+ * from `graduate()` along with the Epic-era graduator that was its only real
146
+ * consumer, and the placeholder fields went with it: a declared-but-unread
147
+ * field is a standing invitation to write a parser nothing will call.
148
+ *
141
149
  * @param {'framework'|'consumer'} source
142
150
  * @returns {object}
143
151
  */
@@ -145,12 +153,6 @@ function makeSpec(source) {
145
153
  return {
146
154
  fnName: 'graduateRetroProposals',
147
155
  isAutoFileEnabled,
148
- // Pre-parsed seam: the comment marker / parser are never consulted
149
- // (findings are supplied directly), but the fields are declared for the
150
- // shared walk's shape.
151
- commentMarker: '<!-- structured-comment: retro -->',
152
- noCommentReason: 'no-retro-comment',
153
- parseFindings: () => [],
154
156
  buildContentMarker,
155
157
  buildMatchTokens,
156
158
  crossRepoCommentAttrs: { graduator: 'retro-proposals' },
@@ -210,8 +212,8 @@ function toFinding(item, source, index) {
210
212
  *
211
213
  * @param {object} opts
212
214
  * @param {number} opts.epicId
213
- * @param {object} opts.provider — ticketing provider (getTicketComments;
214
- * postComment for the cross-repo-deferred persistence).
215
+ * @param {object} opts.provider — ticketing provider (`postComment`, for the
216
+ * cross-repo-deferred persistence).
215
217
  * @param {object} [opts.config] — resolved agentrc.
216
218
  * @param {{owner: string, repo: string}} opts.currentRepo — the repo the
217
219
  * retro is running inside (the consumer's own repo); the cross-repo guard's
@@ -0,0 +1,52 @@
1
+ /**
2
+ * fs-walk.js — the one recursive file walker the lint scripts share.
3
+ *
4
+ * `check-lifecycle-lint.js` and `lint-label-vocabulary.js` each carried a
5
+ * private copy of this generator, differing only in the extension they
6
+ * matched — the second one's comment said so outright ("mirrors the walker
7
+ * shape in check-lifecycle-lint.js"). Story #5024 extracted it: shrinking
8
+ * check-lifecycle-lint.js pushed its duplication percentage up on an unchanged
9
+ * clone count, which is the ratchet correctly pointing at boilerplate that
10
+ * should not have been copied twice.
11
+ *
12
+ * A missing directory yields nothing rather than throwing. That is
13
+ * load-bearing, not lenient: the lint surfaces are directories a deletion can
14
+ * legitimately remove (Story #5024 deleted `lifecycle/listeners/`), and a scan
15
+ * of an absent path is vacuously clean, not an error. Every other `readdir`
16
+ * failure — a permission error, a file where a directory was expected —
17
+ * propagates, so a genuinely broken scan cannot read as clean.
18
+ */
19
+
20
+ import { readdirSync } from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ /**
24
+ * Walk a directory tree synchronously, yielding absolute paths of every file
25
+ * whose name ends with `extension`.
26
+ *
27
+ * @param {string} dir Absolute directory to walk.
28
+ * @param {string} extension Extension to match, leading dot included (`.js`).
29
+ * @param {{ readDir?: typeof readdirSync }} [ports] Injectable for tests.
30
+ * @returns {Generator<string, void, void>}
31
+ */
32
+ export function* walkFilesByExtension(
33
+ dir,
34
+ extension,
35
+ { readDir = readdirSync } = {},
36
+ ) {
37
+ let entries;
38
+ try {
39
+ entries = readDir(dir, { withFileTypes: true });
40
+ } catch (err) {
41
+ if (err.code === 'ENOENT') return;
42
+ throw err;
43
+ }
44
+ for (const entry of entries) {
45
+ const p = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ yield* walkFilesByExtension(p, extension, { readDir });
48
+ } else if (entry.isFile() && entry.name.endsWith(extension)) {
49
+ yield p;
50
+ }
51
+ }
52
+ }
@@ -129,8 +129,8 @@ export function classifyBranchSeed({ localHas, remoteHas }) {
129
129
  * throws (the v2 standalone path has no concurrent creator to race).
130
130
  *
131
131
  * The asymmetric surrounding wrappers (merged-sweep, fast-forward,
132
- * donor-prime, workspace-verify, phase-timer) are deliberately NOT folded
133
- * in — they stay in their respective callers.
132
+ * donor-prime, workspace-verify) are deliberately NOT folded in — they stay
133
+ * in their respective callers.
134
134
  *
135
135
  * Caller-specific log lines and error text are passed in as the `messages`
136
136
  * data bag so behaviour stays byte-identical to the pre-extraction switches.
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { execFileSync, spawnSync } from 'node:child_process';
31
+ import { execFileCapture, spawnCapture } from './child-exec.js';
31
32
 
32
33
  /**
33
34
  * Result of a `gitSpawn` invocation. Field semantics match `child_process.spawnSync`
@@ -40,23 +41,6 @@ import { execFileSync, spawnSync } from 'node:child_process';
40
41
  * @property {string} stderr - Trimmed stderr.
41
42
  */
42
43
 
43
- /**
44
- * Explicit stdout ceiling for every git invocation in this module.
45
- *
46
- * Both child-process runners default `maxBuffer` to 1 MB, at which point the
47
- * child is killed and the call fails with `ENOBUFS` for a reason unrelated to
48
- * git. The push path is the sharp edge: `git push` relays the whole `pre-push`
49
- * hook output, which is unbounded by design — this repo's hook emits a full
50
- * `check-baselines` envelope, measured at 2,166,643 bytes, so every Story
51
- * close failed at `phase: push` once hooks became reachable inside worktrees.
52
- *
53
- * 64 MB is the bound Story #4914 already set on `baselines/git-base.js` for
54
- * the identical failure, matching `run-test-profile.js`,
55
- * `audit-baselines/trend.js` and `audit-baselines/weights.js`. This module was
56
- * missed by that sweep.
57
- */
58
- const MAX_BUFFER_BYTES = 64 * 1024 * 1024;
59
-
60
44
  let _execFileSync = execFileSync;
61
45
  let _spawnSync = spawnSync;
62
46
 
@@ -129,26 +113,30 @@ export function __setGitRunners(exec, spawn) {
129
113
  * the interface returned by {@link createGitInterface} route through this —
130
114
  * they differ only in which `execFileSync` they hand it.
131
115
  *
116
+ * Buffer, shell and encoding policy belong to
117
+ * [`child-exec.js`](child-exec.js); this function contributes only the git
118
+ * specifics (argv prefix, cwd, scrubbed env).
119
+ *
132
120
  * @param {typeof execFileSync} exec
133
121
  * @param {string} cwd
134
122
  * @param {string[]} args
135
123
  * @returns {string} Trimmed stdout text.
136
124
  */
137
125
  function runGitSync(exec, cwd, args) {
138
- return exec('git', args, {
139
- cwd,
140
- encoding: 'utf8',
141
- stdio: ['pipe', 'pipe', 'pipe'],
142
- shell: false,
143
- env: cleanGitEnv(),
144
- maxBuffer: MAX_BUFFER_BYTES,
145
- }).trim();
126
+ return String(
127
+ execFileCapture('git', args, {
128
+ run: exec,
129
+ cwd,
130
+ stdio: ['pipe', 'pipe', 'pipe'],
131
+ env: cleanGitEnv(),
132
+ }),
133
+ ).trim();
146
134
  }
147
135
 
148
136
  /**
149
137
  * The **single** non-throwing git runner — the `spawnSync` counterpart of
150
138
  * {@link runGitSync}, normalising `status`/stdout/stderr into a
151
- * {@link GitResult}.
139
+ * {@link GitResult} via the shared exec surface.
152
140
  *
153
141
  * @param {typeof spawnSync} spawn
154
142
  * @param {string} cwd
@@ -156,19 +144,11 @@ function runGitSync(exec, cwd, args) {
156
144
  * @returns {GitResult}
157
145
  */
158
146
  function runGitSpawn(spawn, cwd, args) {
159
- const result = spawn('git', args, {
147
+ return spawnCapture('git', args, {
148
+ run: spawn,
160
149
  cwd,
161
- stdio: 'pipe',
162
- encoding: 'utf-8',
163
- shell: false,
164
150
  env: cleanGitEnv(),
165
- maxBuffer: MAX_BUFFER_BYTES,
166
151
  });
167
- return {
168
- status: result.status ?? 1,
169
- stdout: (result.stdout ?? '').trim(),
170
- stderr: (result.stderr ?? '').trim(),
171
- };
172
152
  }
173
153
 
174
154
  /**