mandrel 2.31.0 → 2.33.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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -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 +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  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 +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -726,19 +726,25 @@ function githubSubMutationsSucceeded(gh) {
726
726
  return true;
727
727
  }
728
728
 
729
- /** Phase groups whose mutations actually landed, for the install ledger. */
730
- function resolveAppliedGroups(approvedGroups, report) {
731
- const applied = new Set();
732
- for (const group of approvedGroups ?? []) {
733
- if (group === PHASE_GROUPS.GITHUB_ADMIN) {
734
- const gh = report?.github;
735
- if (gh && !gh.error && !gh.skipped && githubSubMutationsSucceeded(gh)) {
736
- applied.add(group);
737
- }
738
- continue;
739
- }
740
- applied.add(group);
741
- }
729
+ /**
730
+ * Phase groups whose mutations actually landed, for the install ledger.
731
+ *
732
+ * Every project-side group runs on every install (Story #3690 replaced the
733
+ * consent-first phased-approval install with a plain summary+confirm loop),
734
+ * so the only variable is whether the irreversible GitHub-admin mutations
735
+ * landed. Story #5007 collapsed the set-threading this used to carry down to
736
+ * that one boolean.
737
+ *
738
+ * @param {object|undefined} report — the live execution report.
739
+ * @returns {Set<string>}
740
+ */
741
+ function resolveAppliedGroups(report) {
742
+ const applied = new Set(Object.values(PHASE_GROUPS));
743
+ const gh = report?.github;
744
+ const githubApplied = Boolean(
745
+ gh && !gh.error && !gh.skipped && githubSubMutationsSucceeded(gh),
746
+ );
747
+ if (!githubApplied) applied.delete(PHASE_GROUPS.GITHUB_ADMIN);
742
748
  return applied;
743
749
  }
744
750
 
@@ -1210,23 +1216,22 @@ export async function provisionResources(state, deps = {}) {
1210
1216
  }
1211
1217
 
1212
1218
  /**
1213
- * Step 6a — Project-side bootstrap. With phased approval removed, all
1214
- * project-side phase groups are treated as approved.
1219
+ * Step 6a — Project-side bootstrap. Every project-side phase runs; the
1220
+ * install ledger decides afterwards which phase groups actually landed
1221
+ * ({@link resolveAppliedGroups}).
1215
1222
  */
1216
1223
  export async function executeBootstrap(state) {
1217
1224
  Logger.info(
1218
1225
  `[Bootstrap] Starting project bootstrap at ${state.projectRoot} (owner=${state.answers.owner} repo=${state.answers.repo} base=${state.answers.baseBranch})`,
1219
1226
  );
1220
- const approvedGroups = new Set(Object.values(PHASE_GROUPS));
1221
1227
  const report = await applyProjectBootstrap({
1222
1228
  projectRoot: state.projectRoot,
1223
1229
  agentRoot: state.agentRoot,
1224
1230
  answers: state.answers,
1225
- approvedGroups,
1226
1231
  withQuality: state.withQuality === true,
1227
1232
  withIssueForms: state.withIssueForms === true,
1228
1233
  });
1229
- return { ok: true, payload: { report, approvedGroups } };
1234
+ return { ok: true, payload: { report } };
1230
1235
  }
1231
1236
 
1232
1237
  /**
@@ -1293,22 +1298,19 @@ export async function executeGithubBootstrap(state) {
1293
1298
 
1294
1299
  /** Step 6c — Record the install ledger for a future uninstall. */
1295
1300
  export function recordLedger(state) {
1296
- const appliedGroups = resolveAppliedGroups(
1297
- state.approvedGroups,
1298
- state.report,
1299
- );
1301
+ const appliedGroups = resolveAppliedGroups(state.report);
1300
1302
  const manifestCtx = {
1301
1303
  answers: state.answers,
1302
1304
  skipGithub: Boolean(state.flags['skip-github']),
1303
1305
  withQuality: state.withQuality === true,
1304
1306
  };
1307
+ // The manifest always carries the unconditional ide-wiring / repo-config
1308
+ // entries and `appliedGroups` always contains those groups, so the filter
1309
+ // never empties. (Story #5007 removed the phased-approval gate that was the
1310
+ // only way to reach an empty set.)
1305
1311
  const entries = buildMutationManifest(manifestCtx).filter((e) =>
1306
1312
  appliedGroups.has(e.phaseGroup),
1307
1313
  );
1308
- if (entries.length === 0) {
1309
- state.report.ledger = { written: false, reason: 'no-mutations-applied' };
1310
- return { ok: true, payload: {} };
1311
- }
1312
1314
  const record = buildLedgerRecord({
1313
1315
  entries,
1314
1316
  approvedGroups: appliedGroups,
@@ -13,13 +13,23 @@
13
13
  // directory through the same scorer that writes the baseline, prints a per-row
14
14
  // before/after table for everything that moved beyond the gate's tolerance,
15
15
  // and exits non-zero when it finds any — so a consumer can wire it as a
16
- // scheduled CI job without wrapping it in verdict-parsing glue. Wiring it is
17
- // deliberately consumer-side work; nothing in this repo schedules it.
16
+ // scheduled CI job without wrapping it in verdict-parsing glue. This repo
17
+ // schedules the maintainability kind in `.github/workflows/baseline-drift.yml`;
18
+ // a consumer materializing `.agents/` still owns its own schedule.
19
+ //
20
+ // `--require-scored` exists because the default skip-is-green contract below
21
+ // is a fail-open trap for exactly that scheduled use. Measured on this repo:
22
+ // `check-baseline-drift.js --gate crap` with no `coverage/coverage-final.json`
23
+ // present prints "✅ No baseline drift detected" and exits 0 — a nightly job
24
+ // wired that way reports green while having measured nothing. The flag turns
25
+ // every skip into exit 2, so a job that asked for a kind and did not get it
26
+ // reds instead.
18
27
  //
19
28
  // Exit codes:
20
29
  // 0 — no drift (or every kind skipped: disabled gate, no baseline, no scorer)
21
30
  // 1 — drift detected in at least one kind
22
- // 2 — the check itself could not run
31
+ // 2 — the check itself could not run (including: a requested kind was
32
+ // skipped and `--require-scored` was passed)
23
33
 
24
34
  // Fail-fast if the framework's runtime deps are not installed — must be the
25
35
  // first import so the check runs before any third-party-importing sibling
@@ -42,6 +52,10 @@ diff-scoped gates structurally cannot see.
42
52
  Options:
43
53
  --gate <kind> Restrict to one kind (repeatable). Default: ${DRIFT_KINDS.join(', ')}.
44
54
  --tolerance <n> Override the per-gate absolute tolerance.
55
+ --require-scored Treat a skipped kind (gate disabled, no baseline, no
56
+ scorer, nothing scored) as a failure rather than a pass.
57
+ Use this in scheduled jobs: without it a kind that could
58
+ not be scored at all reports green.
45
59
  --json Emit the machine-readable report instead of the table.
46
60
  -h, --help Show this help.
47
61
 
@@ -55,16 +69,21 @@ commit the result with a \`baseline-refresh:\` tagged subject (non-empty body).`
55
69
  * cannot silently widen a scheduled job's scope.
56
70
  *
57
71
  * @param {string[]} argv
58
- * @returns {{ kinds: string[], tolerance: number|null, json: boolean }}
72
+ * @returns {{ kinds: string[], tolerance: number|null, json: boolean, requireScored: boolean }}
59
73
  */
60
74
  export function parseArgs(argv = []) {
61
75
  const kinds = [];
62
76
  let tolerance = null;
63
77
  let json = false;
64
- for (let i = 0; i < argv.length; i += 1) {
65
- const arg = argv[i];
66
- if (arg === '--gate' && argv[i + 1]) {
67
- const kind = argv[i + 1];
78
+ // Lifted out of the loop rather than added to its else-if chain: the chain
79
+ // is the file's most complex method already, and a valueless boolean flag
80
+ // needs no positional handling to be recognised.
81
+ const requireScored = argv.includes('--require-scored');
82
+ const rest = argv.filter((a) => a !== '--require-scored');
83
+ for (let i = 0; i < rest.length; i += 1) {
84
+ const arg = rest[i];
85
+ if (arg === '--gate' && rest[i + 1]) {
86
+ const kind = rest[i + 1];
68
87
  if (!DRIFT_KINDS.includes(kind)) {
69
88
  throw new Error(
70
89
  `[drift] unknown --gate "${kind}"; expected one of ${DRIFT_KINDS.join(', ')}`,
@@ -72,11 +91,11 @@ export function parseArgs(argv = []) {
72
91
  }
73
92
  kinds.push(kind);
74
93
  i += 1;
75
- } else if (arg === '--tolerance' && argv[i + 1]) {
76
- const value = Number(argv[i + 1]);
94
+ } else if (arg === '--tolerance' && rest[i + 1]) {
95
+ const value = Number(rest[i + 1]);
77
96
  if (!Number.isFinite(value)) {
78
97
  throw new Error(
79
- `[drift] --tolerance must be a number (got ${argv[i + 1]})`,
98
+ `[drift] --tolerance must be a number (got ${rest[i + 1]})`,
80
99
  );
81
100
  }
82
101
  tolerance = value;
@@ -91,9 +110,44 @@ export function parseArgs(argv = []) {
91
110
  kinds: kinds.length > 0 ? kinds : [...DRIFT_KINDS],
92
111
  tolerance,
93
112
  json,
113
+ requireScored,
94
114
  };
95
115
  }
96
116
 
117
+ /**
118
+ * Render the `--require-scored` verdict for a completed run.
119
+ *
120
+ * A skip is the detector's honest answer to "I could not score this kind" —
121
+ * `{ ok: true, skipped: '<reason>' }` — and the default contract maps that to
122
+ * a pass so an ad-hoc run does not red because coverage happened to be absent.
123
+ * Under `--require-scored` the same answer is a failure: the caller named the
124
+ * kinds it wanted measured, and a kind that was not measured is a gap, not a
125
+ * clean bill of health.
126
+ *
127
+ * Returns `null` when the flag is off or nothing was skipped, so the caller
128
+ * keeps the run's own 0/1 verdict untouched. Both "off" and "nothing skipped"
129
+ * are answered here rather than at the call site: the caller is the file's
130
+ * ratcheted entry point, and this is the branch's natural home anyway.
131
+ *
132
+ * @param {{ results?: Array<{ kind: string, skipped?: string }> }} run
133
+ * @param {boolean} requireScored Whether `--require-scored` was passed.
134
+ * @returns {string|null} the operator-facing failure note, or null.
135
+ */
136
+ function requireScoredFailure(run, requireScored) {
137
+ if (!requireScored) return null;
138
+ const skipped = (run?.results ?? []).filter((r) => r?.skipped);
139
+ if (skipped.length === 0) return null;
140
+ const detail = skipped.map((r) => `${r.kind} (${r.skipped})`).join(', ');
141
+ return (
142
+ `[drift] ❌ --require-scored: ${skipped.length} requested kind(s) were ` +
143
+ `not scored — ${detail}. The verdict above covers only the kinds that ` +
144
+ 'DID score, which is why it can read clean. A scheduled run that cannot ' +
145
+ 'measure a kind is not a clean run; fix the cause (a missing coverage ' +
146
+ 'artifact, a missing baseline, a disabled gate) or drop the kind from ' +
147
+ 'the invocation.'
148
+ );
149
+ }
150
+
97
151
  /**
98
152
  * Run the drift check and render it. Returns the process exit code rather
99
153
  * than exiting, so the whole path is unit-testable.
@@ -112,10 +166,16 @@ export async function runCheckBaselineDrift({
112
166
  cwd,
113
167
  tolerance: args.tolerance,
114
168
  });
115
- const output = args.json
169
+ const base = args.json
116
170
  ? JSON.stringify({ schemaVersion: '1', ...run }, null, 2)
117
171
  : formatDriftReport(run);
118
- return { exitCode: run.ok ? 0 : 1, output };
172
+ // The note is appended rather than substituted: the drift table is still the
173
+ // useful half of the report for whichever kinds DID score. A note present at
174
+ // all means a requested kind went unmeasured, which is exit 2 regardless of
175
+ // what the measured kinds reported.
176
+ const unscored = requireScoredFailure(run, args.requireScored);
177
+ if (unscored) return { exitCode: 2, output: `${base}\n${unscored}` };
178
+ return { exitCode: run.ok ? 0 : 1, output: base };
119
179
  }
120
180
 
121
181
  /**
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+
3
+ // .agents/scripts/check-baseline-scope.js — Story #5012.
4
+ //
5
+ // Assert that every committed baseline's ROW SET still describes the tree.
6
+ //
7
+ // `check-baselines.js` answers "did a measured value regress?" and answers it
8
+ // well. Nothing answered the prior question: does this baseline still measure
9
+ // the right files? A row can point at a file deleted months ago, and an
10
+ // in-scope file can carry no row at all, while every gate stays green — a
11
+ // ratchet is perfectly capable of being green over almost nothing.
12
+ //
13
+ // This gate is measurement-free by construction. It runs no scorer, needs no
14
+ // coverage artifact and no test run: it walks the directories the gate's own
15
+ // config names (`lib/baselines/scope-inventory.js`) and compares that set
16
+ // against the committed rows in both directions
17
+ // (`lib/baselines/scope-assert.js`). That is what makes it cheap enough to be
18
+ // a required check, and what makes its companion —
19
+ // `prune-baseline-orphans.js` — a one-command remedy rather than a full
20
+ // re-score.
21
+ //
22
+ // Exit codes:
23
+ // 0 no fatal divergence (inherited divergence may still be warned about)
24
+ // 1 fatal divergence — attributable to this change set, or strict mode
25
+ // 2 the check could not run (unreadable config, unusable repository)
26
+
27
+ // Fail-fast if the framework's runtime deps are not installed — must be the
28
+ // first import so the check runs before any third-party-importing sibling
29
+ // module is evaluated (Story #3432).
30
+ import './lib/runtime-deps/ensure-installed.js';
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ import { _internals as readerInternals } from './lib/baselines/reader.js';
34
+ import {
35
+ assertScope,
36
+ attributeDivergence,
37
+ resolveStrictness,
38
+ } from './lib/baselines/scope-assert.js';
39
+ import {
40
+ buildScopeInventory,
41
+ SCOPE_KINDS,
42
+ } from './lib/baselines/scope-inventory.js';
43
+ import { runAsCli } from './lib/cli-utils.js';
44
+ import { getQuality } from './lib/config/quality.js';
45
+ import { resolveConfig } from './lib/config-resolver.js';
46
+ import { gitSpawn } from './lib/git-utils.js';
47
+
48
+ const EXIT_PASS = 0;
49
+ const EXIT_DIVERGED = 1;
50
+ const EXIT_CANNOT_RUN = 2;
51
+
52
+ /**
53
+ * Files whose edit makes merge-base attribution untrustworthy: once a branch
54
+ * has rewritten the scope rules themselves, "which side of the merge-base
55
+ * introduced this row" is no longer a question the diff can answer.
56
+ */
57
+ const SCOPE_CONFIG_PATHS = Object.freeze([
58
+ '.c8rc.cjs',
59
+ '.agentrc.json',
60
+ '.agentrc.local.json',
61
+ ]);
62
+
63
+ const HELP = {
64
+ invocation:
65
+ 'node .agents/scripts/check-baseline-scope.js [--kind <kind>] [--base <ref>] [--strict] [--json]',
66
+ summary:
67
+ "Assert each committed baseline's row set still describes the tree — in-scope files with no row, and rows whose file is gone or out of scope.",
68
+ flags: [
69
+ ['--kind <kind>', 'Check one kind only (repeatable). Default: all.'],
70
+ ['--base <ref>', 'Attribution base ref. Default: origin/<baseBranch>.'],
71
+ ['--strict', 'Treat every divergence as fatal, skipping attribution.'],
72
+ ['--json', 'Emit the report as JSON instead of text.'],
73
+ ['--cwd <dir>', 'Repository root to check. Default: process.cwd().'],
74
+ ],
75
+ notes: [
76
+ 'Exit codes:\n 0 no fatal divergence\n 1 fatal divergence\n 2 the check could not run',
77
+ 'Remedy for a stale row: node .agents/scripts/prune-baseline-orphans.js',
78
+ ],
79
+ };
80
+
81
+ /**
82
+ * Parse argv into an options bag. Unknown flags are a config error rather than
83
+ * a silent no-op — a typo'd `--kinds` must not read as "check everything".
84
+ *
85
+ * @param {string[]} argv
86
+ * @returns {{ kinds: string[], base: string | null, strict: boolean, json: boolean, cwd: string }}
87
+ */
88
+ export function parseArgs(argv = []) {
89
+ const out = { kinds: [], base: null, strict: false, json: false, cwd: null };
90
+ let i = 0;
91
+ while (i < argv.length) {
92
+ const arg = argv[i];
93
+ const value = argv[i + 1];
94
+ i += 1;
95
+ if (arg === '--strict') out.strict = true;
96
+ else if (arg === '--json') out.json = true;
97
+ else if (arg === '--kind') {
98
+ out.kinds.push(value);
99
+ i += 1;
100
+ } else if (arg === '--base') {
101
+ out.base = value;
102
+ i += 1;
103
+ } else if (arg === '--cwd') {
104
+ out.cwd = value;
105
+ i += 1;
106
+ } else throw new Error(`unknown flag "${arg}" (try --help)`);
107
+ }
108
+ const unknown = out.kinds.filter((k) => !SCOPE_KINDS.includes(k));
109
+ if (unknown.length > 0) {
110
+ throw new Error(
111
+ `unknown --kind ${unknown.join(', ')}; expected one of ${SCOPE_KINDS.join(', ')}`,
112
+ );
113
+ }
114
+ if (out.kinds.length === 0) out.kinds = [...SCOPE_KINDS];
115
+ out.cwd = out.cwd ?? process.cwd();
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Resolve the merge base between HEAD and the requested base ref, preferring
121
+ * the remote-tracking ref so a stale local `main` cannot widen attribution.
122
+ *
123
+ * Returns `{ base: null }` when no candidate resolves — the strictness
124
+ * resolver treats that as a reason to fail towards strict, never as a licence
125
+ * to skip.
126
+ *
127
+ * @param {{ cwd: string, baseRef: string }} params
128
+ * @returns {{ base: string | null, aheadOfBase: boolean }}
129
+ */
130
+ function resolveMergeBase({ cwd, baseRef }) {
131
+ for (const candidate of [`origin/${baseRef}`, baseRef]) {
132
+ const merged = gitSpawn(cwd, 'merge-base', candidate, 'HEAD');
133
+ if (merged.status !== 0 || merged.stdout.length === 0) continue;
134
+ const head = gitSpawn(cwd, 'rev-parse', 'HEAD');
135
+ return {
136
+ base: merged.stdout,
137
+ aheadOfBase: head.status === 0 && head.stdout !== merged.stdout,
138
+ };
139
+ }
140
+ return { base: null, aheadOfBase: false };
141
+ }
142
+
143
+ /**
144
+ * Enumerate what `<base>..HEAD` did to the tree, split into the two sets
145
+ * attribution needs. A rename contributes its old path to `removed` and its
146
+ * new path to `added`, which is exactly how a rename strands a row.
147
+ *
148
+ * @param {{ cwd: string, base: string | null }} params
149
+ * @returns {{ added: string[], removed: string[], changed: string[] }}
150
+ */
151
+ function readChangeSet({ cwd, base }) {
152
+ const empty = { added: [], removed: [], changed: [] };
153
+ if (base === null) return empty;
154
+ const diff = gitSpawn(cwd, 'diff', '--name-status', '-M', `${base}..HEAD`);
155
+ if (diff.status !== 0) return empty;
156
+ const added = [];
157
+ const removed = [];
158
+ const changed = [];
159
+ for (const line of diff.stdout.split('\n').filter(Boolean)) {
160
+ const [code, first, second] = line.split('\t');
161
+ const letter = code[0];
162
+ if (letter === 'R' || letter === 'C') {
163
+ removed.push(first);
164
+ added.push(second);
165
+ changed.push(first, second);
166
+ continue;
167
+ }
168
+ if (letter === 'A') added.push(first);
169
+ if (letter === 'D') removed.push(first);
170
+ changed.push(first);
171
+ }
172
+ return { added, removed, changed };
173
+ }
174
+
175
+ /**
176
+ * Repo-relative paths of every baseline file in play — the "did this change
177
+ * set edit a baseline?" input to the strictness resolver.
178
+ *
179
+ * @param {{ cwd: string, kinds: string[] }} params
180
+ * @returns {string[]}
181
+ */
182
+ function baselinePathsFor({ cwd, kinds }) {
183
+ return kinds.map((kind) =>
184
+ path
185
+ .relative(cwd, readerInternals.resolveBaselinePath(kind, { cwd }))
186
+ .split(path.sep)
187
+ .join('/'),
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Read a baseline's rows, or `null` when the kind ships no baseline in this
193
+ * repository. Deliberately does NOT go through `reader.load`: an envelope too
194
+ * stale to satisfy its schema is exactly the state this gate exists to report,
195
+ * and refusing to read it would turn the report into a crash.
196
+ *
197
+ * @param {{ cwd: string, kind: string }} params
198
+ * @returns {Array<object> | null}
199
+ */
200
+ function readRows({ cwd, kind }) {
201
+ const abs = readerInternals.resolveBaselinePath(kind, { cwd });
202
+ try {
203
+ const parsed = JSON.parse(fs.readFileSync(abs, 'utf8'));
204
+ return Array.isArray(parsed?.rows) ? parsed.rows : [];
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Assess one kind: inventory → both-directions assertion → attribution.
212
+ *
213
+ * @param {object} params
214
+ * @returns {object} One entry of the report's `kinds` array.
215
+ */
216
+ function assessKind({ kind, cwd, quality, strict, changeSet }) {
217
+ const rows = readRows({ cwd, kind });
218
+ if (rows === null) {
219
+ return { kind, present: false, skipped: true, reason: 'no baseline file' };
220
+ }
221
+ const inventory = buildScopeInventory({ kind, cwd, quality });
222
+ const found = assertScope({
223
+ inventory,
224
+ rows,
225
+ existsOnDisk: (rel) => fs.existsSync(path.resolve(cwd, rel)),
226
+ });
227
+ if (found.skipped) return { kind, present: true, ...found };
228
+ const attributed = attributeDivergence({
229
+ missing: found.missing,
230
+ extra: found.extra,
231
+ added: changeSet.added,
232
+ removed: changeSet.removed,
233
+ strict,
234
+ });
235
+ return {
236
+ kind,
237
+ present: true,
238
+ skipped: false,
239
+ reason: null,
240
+ inScopeCount: inventory.files.length,
241
+ rowCount: rows.length,
242
+ ...attributed,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Run the whole assessment and return a report plus its exit code.
248
+ *
249
+ * @param {{ argv?: string[] }} [params]
250
+ * @returns {{ report: object, exitCode: number }}
251
+ */
252
+ export function runScopeCheck({ argv = [] } = {}) {
253
+ const opts = parseArgs(argv);
254
+ const cwd = opts.cwd;
255
+ const config = resolveConfig({ cwd });
256
+ const quality = getQuality(config) ?? { gates: {} };
257
+ const baseRef = opts.base ?? config?.project?.baseBranch ?? 'main';
258
+ const { base, aheadOfBase } = resolveMergeBase({ cwd, baseRef });
259
+ const changeSet = readChangeSet({ cwd, base });
260
+ const strictness = opts.strict
261
+ ? { strict: true, reason: 'operator requested --strict' }
262
+ : resolveStrictness({
263
+ base,
264
+ aheadOfBase,
265
+ changedFiles: changeSet.changed,
266
+ baselinePaths: baselinePathsFor({ cwd, kinds: opts.kinds }),
267
+ scopeConfigPaths: SCOPE_CONFIG_PATHS,
268
+ });
269
+ const kinds = opts.kinds.map((kind) =>
270
+ assessKind({ kind, cwd, quality, strict: strictness.strict, changeSet }),
271
+ );
272
+ const fatalCount = kinds.reduce((sum, k) => sum + (k.fatalCount ?? 0), 0);
273
+ const report = {
274
+ schemaVersion: '1',
275
+ base,
276
+ strict: strictness.strict,
277
+ strictReason: strictness.reason,
278
+ fatalCount,
279
+ warningCount: kinds.reduce((sum, k) => sum + (k.warningCount ?? 0), 0),
280
+ kinds,
281
+ };
282
+ return { report, exitCode: fatalCount > 0 ? EXIT_DIVERGED : EXIT_PASS };
283
+ }
284
+
285
+ /**
286
+ * Render one kind's findings as indented text lines.
287
+ *
288
+ * @param {object} entry
289
+ * @returns {string[]}
290
+ */
291
+ function renderKind(entry) {
292
+ if (entry.skipped) return [` - ${entry.kind}: skipped (${entry.reason})`];
293
+ const head =
294
+ ` - ${entry.kind}: ${entry.rowCount} row(s) over ${entry.inScopeCount} ` +
295
+ `in-scope file(s) — ${entry.fatalCount} fatal, ${entry.warningCount} inherited`;
296
+ const lines = [head];
297
+ for (const file of entry.fatal.missing) {
298
+ lines.push(` ✗ missing row: ${file}`);
299
+ }
300
+ for (const row of entry.fatal.extra) {
301
+ lines.push(` ✗ stale row (${row.reason}): ${row.path}`);
302
+ }
303
+ return lines;
304
+ }
305
+
306
+ /**
307
+ * Render the whole report as text.
308
+ *
309
+ * @param {object} report
310
+ * @returns {string}
311
+ */
312
+ export function formatReport(report) {
313
+ const mode = report.strict ? 'strict' : 'attributed';
314
+ const lines = [
315
+ `[check-baseline-scope] ${report.fatalCount} fatal, ${report.warningCount} inherited ` +
316
+ `(${mode}: ${report.strictReason})`,
317
+ ...report.kinds.flatMap(renderKind),
318
+ ];
319
+ if (report.fatalCount > 0) {
320
+ lines.push(
321
+ '',
322
+ 'A baseline no longer describes the tree. Prune provably-inert rows with:',
323
+ ' node .agents/scripts/prune-baseline-orphans.js',
324
+ 'A missing row means a file was added without being measured — run that',
325
+ "kind's producer (npm run coverage:update / maintainability:update).",
326
+ );
327
+ }
328
+ return lines.join('\n');
329
+ }
330
+
331
+ /**
332
+ * CLI entry point. Returns its exit code rather than calling `process.exit()`,
333
+ * so `runAsCli`'s `propagateExitCode` path settles it through `flushStdio` and
334
+ * an unbounded report is not truncated at a pipe boundary (Story #4783).
335
+ *
336
+ * @returns {Promise<number>}
337
+ */
338
+ async function main() {
339
+ let result;
340
+ try {
341
+ result = runScopeCheck({ argv: process.argv.slice(2) });
342
+ } catch (err) {
343
+ const message = err?.message ?? String(err);
344
+ process.stdout.write(
345
+ `${JSON.stringify({ schemaVersion: '1', error: message }, null, 2)}\n`,
346
+ );
347
+ return EXIT_CANNOT_RUN;
348
+ }
349
+ const opts = { json: process.argv.includes('--json') };
350
+ process.stdout.write(
351
+ opts.json
352
+ ? `${JSON.stringify(result.report, null, 2)}\n`
353
+ : `${formatReport(result.report)}\n`,
354
+ );
355
+ return result.exitCode;
356
+ }
357
+
358
+ runAsCli(import.meta.url, main, {
359
+ source: 'check-baseline-scope',
360
+ usage: HELP,
361
+ propagateExitCode: true,
362
+ });
@@ -14,13 +14,21 @@
14
14
  * production-dead code hides behind its own tests. See
15
15
  * `lib/dead-exports-mode.js` for why the two passes carry separate baselines.
16
16
  *
17
+ * Story #5001 widens what the ratchet can see: knip's `files` category (a
18
+ * module nothing imports) now maps to a `{ file, symbol: '*' }` row alongside
19
+ * the per-export rows, and `knip.json` lists `.agents/scripts/*.js` entry
20
+ * points explicitly instead of by blanket glob — so a top-level CLI that
21
+ * nothing invokes surfaces as dead instead of being declared live by the glob.
22
+ *
17
23
  * Contract:
18
24
  * - Reads the committed baseline at `baselines/dead-exports.json` — or
19
25
  * `baselines/dead-exports-production.json` under `--production`
20
26
  * (override either with `--baseline <path>`). Envelope shape:
21
27
  * { $schema, kernelVersion, generatedAt, rows: [{ file, symbol }] }
22
28
  * - Spawns `npx knip --reporter json --no-progress` (plus `--production`),
23
- * parses stdout, extracts `{ file, symbol }` rows from `issues[].exports[]`.
29
+ * parses stdout, extracts `{ file, symbol }` rows from `issues[].exports[]`
30
+ * **and** `issues[].files[]` — the latter as one `{ file, symbol: '*' }`
31
+ * whole-file row per module nothing imports (Story #5001).
24
32
  * - Diffs current vs. baseline by `(file, symbol)` identity.
25
33
  * - Prints `+ <file>: <symbol>` for each added dead export and
26
34
  * `- <file>: <symbol>` for each removed one, then a summary line.