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
@@ -0,0 +1,150 @@
1
+ /**
2
+ * _crap-read.js — the CRAP baseline read path (Story #5002).
3
+ *
4
+ * Underscore-prefixed like `_shared-metric.js`: a helper for the per-kind
5
+ * modules in this directory, not a kind of its own. `kinds/crap.js` re-exports
6
+ * `loadCrapBaseline` so existing importers keep one door.
7
+ *
8
+ * **This module is the whole read path, and there is only one.** `crap-utils.js`
9
+ * used to carry a second one — `projectCrapEnvelopeToLegacy` plus its
10
+ * `COMPAT_STAMP_*` allow-lists — so `check-baselines` (through
11
+ * `baselines/reader.js`) and the `quality-preview` pre-commit arm (through that
12
+ * projection) fed the same compat axes from two hand-maintained field lists. A
13
+ * stamp added to one and not the other yielded two opposite verdicts on one
14
+ * envelope, three times over: Story #4866 (`scoringSemantics`,
15
+ * `tsTranspilerVersion`), #4969 (`rows[].anonymous`), #4986
16
+ * (`provenanceStamped`). Every one of those axes keys on a POSITIVE marker, so
17
+ * a dropped field read `undefined` and failed the baseline closed with a remedy
18
+ * that could not work — re-deriving it wrote the stamp the read path then
19
+ * discarded. With one reader that drift class is structurally impossible, so no
20
+ * allow-list needs maintaining.
21
+ */
22
+
23
+ import path from 'node:path';
24
+ import { readBaselineAtRef } from '../../baseline-loader.js';
25
+ import { resolveEscomplexVersion } from '../../crap-utils.js';
26
+ import { loadBaseline } from '../../gates/baseline-store.js';
27
+ import {
28
+ loadFile as loadBaselineFile,
29
+ load as loadBaselineKind,
30
+ } from '../reader.js';
31
+
32
+ /**
33
+ * Read the committed CRAP baseline off the working tree through
34
+ * `baselines/reader.js` and project it onto the `file`-keyed shape the
35
+ * comparator and the compat axes consume.
36
+ *
37
+ * `baselinePath` selects the explicit-path reader variant (the worktree /
38
+ * epic-ref callers always know their own path); without one the reader
39
+ * resolves the configured location for the `crap` kind itself.
40
+ *
41
+ * `escomplexVersion` is back-filled from the running scorer exactly as the
42
+ * deleted projection stamped it — the v2 envelope does not carry the field, and
43
+ * `escomplex-mismatch` is a fatal axis, so omitting it would fail every
44
+ * baseline closed on a value that was never on disk.
45
+ *
46
+ * Returns `null` on any read/parse/schema failure; the preview gate maps that
47
+ * to "no baseline" and fails open, as it always did.
48
+ *
49
+ * Deliberately module-local: `loadCrapBaseline`'s `readFromTree` default is the
50
+ * single production door to it, and tests inject their own loader.
51
+ *
52
+ * @param {{baselinePath?: string, projectRoot?: string}} [opts]
53
+ * @returns {object|null}
54
+ */
55
+ function readCrapBaselineFromTree({ baselinePath, projectRoot } = {}) {
56
+ const cwd = projectRoot ?? process.cwd();
57
+ let envelope;
58
+ try {
59
+ envelope = baselinePath
60
+ ? loadBaselineFile(
61
+ path.isAbsolute(baselinePath)
62
+ ? baselinePath
63
+ : path.resolve(cwd, baselinePath),
64
+ { kind: 'crap' },
65
+ )
66
+ : loadBaselineKind('crap', { cwd });
67
+ } catch {
68
+ return null;
69
+ }
70
+ return {
71
+ kernelVersion: envelope.kernelVersion,
72
+ escomplexVersion: resolveEscomplexVersion(),
73
+ scoringSemantics: envelope.scoringSemantics ?? null,
74
+ tsTranspilerVersion:
75
+ typeof envelope.tsTranspilerVersion === 'string'
76
+ ? envelope.tsTranspilerVersion
77
+ : null,
78
+ provenanceStamped: envelope.provenanceStamped,
79
+ rows: (envelope.rows ?? []).map(projectBaselineRow),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Re-key one on-disk row (`path`) onto the `file` field `compareCrap` matches
85
+ * on, carrying the two write-only-when-non-default row markers verbatim.
86
+ *
87
+ * Both markers are BASELINE facts. Dropping `anonymous` would leave every
88
+ * re-keyed row looking like an unmarked anonymous one — precisely the shape the
89
+ * `anon-identity-unstamped` axis fails closed (Story #4969) — and dropping
90
+ * `coordinateSystem` would let the comparator drift-resolve across two
91
+ * coordinate systems (Story #4866).
92
+ *
93
+ * @param {{path: string, method: string, startLine: number, crap: number,
94
+ * coordinateSystem?: string, anonymous?: boolean}} row
95
+ * @returns {object}
96
+ */
97
+ function projectBaselineRow(row) {
98
+ return {
99
+ crap: row.crap,
100
+ file: row.path,
101
+ method: row.method,
102
+ startLine: row.startLine,
103
+ ...(row.coordinateSystem === undefined
104
+ ? {}
105
+ : { coordinateSystem: row.coordinateSystem }),
106
+ ...(row.anonymous === undefined ? {} : { anonymous: row.anonymous }),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Pure helper: resolve the CRAP baseline either from the working tree (via
112
+ * `readCrapBaselineFromTree`) or, when `epicRef` is supplied, from
113
+ * `git show <epicRef>:<baselinePath>` via `readBaselineAtRef`.
114
+ *
115
+ * Story #1120 threads `epic/<id>` into close-validation so the comparison runs
116
+ * against the Epic-branch HEAD's committed baseline. This helper delegates the
117
+ * read to baseline-store and applies the CRAP shape-check +
118
+ * `tsTranspilerVersion` back-fill on top.
119
+ */
120
+ export function loadCrapBaseline({
121
+ baselinePath,
122
+ epicRef,
123
+ readAtRef = readBaselineAtRef,
124
+ readFromTree = readCrapBaselineFromTree,
125
+ logger = console,
126
+ }) {
127
+ const parsed = loadBaseline({
128
+ baselinePath,
129
+ epicRef,
130
+ readAtRef,
131
+ readFromTree,
132
+ logger,
133
+ label: 'CRAP',
134
+ });
135
+ // No-epicRef path delegates to readFromTree which already applies the
136
+ // shape-check + tsTranspilerVersion back-fill, so a tree read returns either
137
+ // a valid envelope or null. Epic-ref path bypasses that helper — shape-check
138
+ // + back-fill happens here.
139
+ if (!epicRef) return parsed;
140
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
141
+ return null;
142
+ }
143
+ if (typeof parsed.kernelVersion !== 'string') return null;
144
+ if (typeof parsed.escomplexVersion !== 'string') return null;
145
+ if (!Array.isArray(parsed.rows)) return null;
146
+ if (typeof parsed.tsTranspilerVersion !== 'string') {
147
+ parsed.tsTranspilerVersion = '0.0.0';
148
+ }
149
+ return parsed;
150
+ }
@@ -13,15 +13,12 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { readBaselineAtRef } from '../../baseline-loader.js';
17
16
  import {
18
17
  COORDINATE_ORIGINAL,
19
18
  COORDINATE_TRANSPILED,
20
19
  deriveFixGuidance,
21
20
  } from '../../crap-engine.js';
22
21
  import { isAnonymousMethodLabel } from '../../crap-method-identity.js';
23
- import { getCrapBaseline } from '../../crap-utils.js';
24
- import { loadBaseline } from '../../gates/baseline-store.js';
25
22
  import { Logger } from '../../Logger.js';
26
23
  import { resolveTsTranspilerVersion } from '../../transpile.js';
27
24
  import {
@@ -31,6 +28,12 @@ import {
31
28
  } from '../envelope.js';
32
29
  import { canonicalise } from '../path-canon.js';
33
30
  import { mergeRowsByScope } from '../scope.js';
31
+ import {
32
+ buildNewViolation,
33
+ deriveUncoveredFiles,
34
+ formatNewViolationMeasure,
35
+ newMethodGateScore,
36
+ } from './_crap-new-method-gate.js';
34
37
  import {
35
38
  makeAggregate,
36
39
  makeCompare,
@@ -42,6 +45,11 @@ import {
42
45
  export const name = 'crap';
43
46
  export const keyField = 'path';
44
47
 
48
+ // The CRAP baseline read path (Story #5002) lives in `_crap-read.js` — the ONE
49
+ // door, re-exported here so `preview-gates.js` and every existing importer keep
50
+ // reaching it through this module.
51
+ export { loadCrapBaseline } from './_crap-read.js';
52
+
45
53
  const __filename = fileURLToPath(import.meta.url);
46
54
 
47
55
  /**
@@ -245,18 +253,12 @@ function crapRowKey(row) {
245
253
  return `${row.path}::${row.method}@${row.startLine}`;
246
254
  }
247
255
 
248
- // `methodIdentityKey` / `indexBaselineRowsByFile` (Story #4981) live in
249
- // crap-baseline-index.js, not here `crap-utils.js#scanAndScore` needs them
250
- // to build the incremental join's per-file baseline lookup, and crap-utils.js
251
- // already imports `getCrapBaseline` FROM this module. Defining them here and
252
- // importing them into crap-utils.js would close that edge into a cycle
253
- // (kinds/crap.js crap-utils.js kinds/crap.js). Re-exported here so
254
- // existing importers of this module keep a single door to the identity key
255
- // `crapRowKey` composes with the file path.
256
- export {
257
- indexBaselineRowsByFile,
258
- methodIdentityKey,
259
- } from '../../crap-baseline-index.js';
256
+ // `methodIdentityKey` / `indexBaselineRowsByFile` (Story #4981) are now
257
+ // module-local to `crap-baseline-join.js` (Story #5002): after that module
258
+ // absorbed the per-file queue wiring, both callers are inside it, so the
259
+ // exports and the re-export that used to live here — were reachable from
260
+ // tests alone. `resolveIncrementalContext` is the production door to the
261
+ // index; `crapRowKey` above is the composite key it halves.
260
262
 
261
263
  /**
262
264
  * Pure stabilizer for s-stability-epsilon (Story #1964). CRAP rows match
@@ -395,6 +397,9 @@ export function checkCrapRegression(row, baseline, tolerance, kind) {
395
397
  * (`crap: null` / `coverage: null`, or an explicit `unscorable: true`) carries
396
398
  * no measurement to compare. It is bucketed and counted, and it is excluded
397
399
  * from `comparable` so it cannot dilute any ratio derived from this result.
400
+ *
401
+ * **A new method in a wholly-uncovered file is gated on complexity alone**
402
+ * (Story #5002) — see `newMethodGateScore`.
398
403
  */
399
404
  export function compareCrap({
400
405
  currentRows,
@@ -402,6 +407,7 @@ export function compareCrap({
402
407
  newMethodCeiling,
403
408
  tolerance,
404
409
  }) {
410
+ const uncoveredFiles = deriveUncoveredFiles(currentRows);
405
411
  const exactIndex = new Map();
406
412
  const methodIndex = new Map();
407
413
  for (const b of baselineRows ?? []) {
@@ -475,14 +481,10 @@ export function compareCrap({
475
481
  continue;
476
482
  }
477
483
 
478
- if (row.crap > newMethodCeiling + tolerance) {
484
+ const gateScore = newMethodGateScore(row, uncoveredFiles);
485
+ if (gateScore > newMethodCeiling + tolerance) {
479
486
  newViolations += 1;
480
- violations.push({
481
- ...row,
482
- kind: 'new',
483
- baseline: null,
484
- ceiling: newMethodCeiling,
485
- });
487
+ violations.push(buildNewViolation(row, newMethodCeiling, gateScore));
486
488
  }
487
489
  }
488
490
 
@@ -892,48 +894,6 @@ export function assertBaselineCompatible(baseline, ctx = {}) {
892
894
  return null;
893
895
  }
894
896
 
895
- /**
896
- * Pure helper: resolve the CRAP baseline either from the working tree
897
- * (via `getCrapBaseline`) or, when `epicRef` is supplied, from
898
- * `git show <epicRef>:<baselinePath>` via `readBaselineAtRef`.
899
- *
900
- * Story #1120 threads `epic/<id>` into close-validation so the
901
- * comparison runs against the Epic-branch HEAD's committed baseline.
902
- * This helper delegates the read to baseline-store and applies the CRAP
903
- * shape-check + `tsTranspilerVersion` back-fill on top.
904
- */
905
- export function loadCrapBaseline({
906
- baselinePath,
907
- epicRef,
908
- readAtRef = readBaselineAtRef,
909
- readFromTree = getCrapBaseline,
910
- logger = console,
911
- }) {
912
- const parsed = loadBaseline({
913
- baselinePath,
914
- epicRef,
915
- readAtRef,
916
- readFromTree,
917
- logger,
918
- label: 'CRAP',
919
- });
920
- // No-epicRef path delegates to readFromTree which already applies the
921
- // shape-check + tsTranspilerVersion back-fill, so a tree read returns
922
- // either a valid envelope or null. Epic-ref path bypasses that helper
923
- // — shape-check + back-fill happens here.
924
- if (!epicRef) return parsed;
925
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
926
- return null;
927
- }
928
- if (typeof parsed.kernelVersion !== 'string') return null;
929
- if (typeof parsed.escomplexVersion !== 'string') return null;
930
- if (!Array.isArray(parsed.rows)) return null;
931
- if (typeof parsed.tsTranspilerVersion !== 'string') {
932
- parsed.tsTranspilerVersion = '0.0.0';
933
- }
934
- return parsed;
935
- }
936
-
937
897
  /**
938
898
  * Build the structured `--json` report envelope.
939
899
  *
@@ -1061,7 +1021,7 @@ export function printViolation(v) {
1061
1021
  `[CRAP] ❌ NEW-METHOD over ceiling: ${v.file}::${v.method} (line ${v.startLine})`,
1062
1022
  );
1063
1023
  Logger.error(
1064
- ` crap=${v.crap.toFixed(2)} > ceiling=${v.ceiling} (c=${v.cyclomatic}, cov=${v.coverage.toFixed(2)})`,
1024
+ ` ${formatNewViolationMeasure(v)} > ceiling=${v.ceiling} (c=${v.cyclomatic}, cov=${v.coverage.toFixed(2)})`,
1065
1025
  );
1066
1026
  return;
1067
1027
  }
@@ -0,0 +1,233 @@
1
+ // .agents/scripts/lib/baselines/orphan-pruner.js
2
+ //
3
+ // Story #5012 — the measurement-free remedy that makes hard-failing on a stale
4
+ // baseline row defensible.
5
+ //
6
+ // The scope gate (`check-baseline-scope.js`) blocks a PR that leaves a row
7
+ // pointing at a file it deleted. A hard gate is only fair while its remedy is
8
+ // cheap, and the existing remedy — re-run the scorer, re-derive the baseline —
9
+ // costs a full coverage run or a full-tree MI re-score for what is arithmetic
10
+ // on a row set. This pruner is that arithmetic.
11
+ //
12
+ // ## Measurement-free by contract
13
+ //
14
+ // Three prohibitions, each guarding a way the file could start lying:
15
+ //
16
+ // 1. **Never add a row.** Adding one means claiming a measurement nobody
17
+ // took. Producing rows stays the producers' exclusive job.
18
+ // 2. **Never restamp `generatedAt`.** A fresh stamp over rows nobody
19
+ // re-measured is precisely the failure an age check exists to catch — the
20
+ // envelope would claim to describe today's tree on the strength of a
21
+ // deletion. The pruner carries the original stamp through untouched.
22
+ // 3. **Never delete a row it cannot prove inert.** Only two classes qualify:
23
+ // the file is absent from disk, or the file is no longer matched by the
24
+ // gate's own `targetDirs` / `ignoreGlobs`. Everything else survives.
25
+ //
26
+ // ## Degrade, never guess
27
+ //
28
+ // A degraded inventory (`files: null` — unreadable `.c8rc.cjs`, absent
29
+ // `targetDirs`) means scope is unknown, not empty. Treating it as empty would
30
+ // make every row look out-of-scope and hand the pruner the whole baseline. So
31
+ // an unreadable scope config falls back to **orphan-only** pruning: the
32
+ // absent-from-disk class still resolves off the filesystem and is still safe,
33
+ // while the out-of-scope class is suspended until scope can be read again.
34
+
35
+ import fs from 'node:fs';
36
+ import path from 'node:path';
37
+
38
+ import { getKindModule } from './kernel.js';
39
+ import { _internals as readerInternals } from './reader.js';
40
+ import { EXTRA_REASONS } from './scope-assert.js';
41
+ import {
42
+ buildScopeInventory,
43
+ isFileKeyed,
44
+ SCOPE_KINDS,
45
+ } from './scope-inventory.js';
46
+ import { writeFile } from './writer.js';
47
+
48
+ /** Every kind whose rows key on a repo-relative file path. */
49
+ export const PRUNABLE_KINDS = Object.freeze(SCOPE_KINDS.filter(isFileKeyed));
50
+
51
+ /**
52
+ * Partition a row set into the rows to keep and the rows to drop.
53
+ *
54
+ * Pure given its injected `existsOnDisk` predicate. `inScope` of `null` is the
55
+ * degraded case: only the absent-from-disk class is prunable.
56
+ *
57
+ * @param {{
58
+ * rows: Array<Record<string, unknown>>,
59
+ * inScope: Set<string> | null,
60
+ * existsOnDisk: (relPath: string) => boolean,
61
+ * }} params
62
+ * @returns {{ keep: Array<object>, removed: Array<{ path: string, reason: string }> }}
63
+ */
64
+ export function planPrune({ rows, inScope, existsOnDisk } = {}) {
65
+ const keep = [];
66
+ const removed = [];
67
+ for (const row of Array.isArray(rows) ? rows : []) {
68
+ const key = row?.path;
69
+ if (typeof key !== 'string' || key.length === 0) {
70
+ keep.push(row);
71
+ continue;
72
+ }
73
+ if (!existsOnDisk(key)) {
74
+ removed.push({ path: key, reason: EXTRA_REASONS.ABSENT });
75
+ continue;
76
+ }
77
+ if (inScope !== null && !inScope.has(key)) {
78
+ removed.push({ path: key, reason: EXTRA_REASONS.OUT_OF_SCOPE });
79
+ continue;
80
+ }
81
+ keep.push(row);
82
+ }
83
+ return { keep, removed };
84
+ }
85
+
86
+ /**
87
+ * Prune one parsed envelope and recompute its `rollup` through the kind's own
88
+ * arithmetic (`kinds/<kind>.js#rollup`), so the pruned file stays internally
89
+ * consistent and still validates against its schema. Recomputing by a private
90
+ * formula here would let the rollup and the rows describe different trees.
91
+ *
92
+ * A rollup carrying component buckets beyond `*` is refused rather than
93
+ * recomputed: the component globs that produced those buckets live in gate
94
+ * config this module is not handed, and emitting a `*`-only rollup would
95
+ * silently delete them.
96
+ *
97
+ * @param {{
98
+ * kind: string,
99
+ * envelope: object,
100
+ * inventory: { files: string[] | null },
101
+ * existsOnDisk?: (relPath: string) => boolean,
102
+ * }} params
103
+ * @returns {{
104
+ * envelope: object | null,
105
+ * removed: Array<{ path: string, reason: string }>,
106
+ * skipped: boolean,
107
+ * reason: string | null,
108
+ * }}
109
+ */
110
+ export function pruneEnvelope({
111
+ kind,
112
+ envelope,
113
+ inventory,
114
+ existsOnDisk,
115
+ } = {}) {
116
+ const rollupKeys = Object.keys(envelope?.rollup ?? {});
117
+ if (rollupKeys.some((key) => key !== '*')) {
118
+ return {
119
+ envelope: null,
120
+ removed: [],
121
+ skipped: true,
122
+ reason: `rollup carries component buckets (${rollupKeys.join(', ')}); prune them with the producer`,
123
+ };
124
+ }
125
+ const inScope = inventory?.files === null ? null : new Set(inventory.files);
126
+ const { keep, removed } = planPrune({
127
+ rows: envelope?.rows ?? [],
128
+ inScope,
129
+ existsOnDisk: existsOnDisk ?? (() => true),
130
+ });
131
+ if (removed.length === 0) {
132
+ return { envelope: null, removed: [], skipped: false, reason: null };
133
+ }
134
+ return {
135
+ envelope: {
136
+ ...envelope,
137
+ // `generatedAt` is carried by the spread, deliberately unmodified.
138
+ rollup: getKindModule(kind).rollup(keep, []),
139
+ rows: keep,
140
+ },
141
+ removed,
142
+ skipped: false,
143
+ reason: null,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Read and JSON-parse a baseline file. Returns `null` when the file is absent
149
+ * (a kind this repository does not use) and throws only on malformed JSON,
150
+ * which is a real defect the caller should surface rather than swallow.
151
+ *
152
+ * @param {string} absPath
153
+ * @param {typeof fs} fsImpl
154
+ * @returns {object | null}
155
+ */
156
+ function readEnvelope(absPath, fsImpl) {
157
+ let raw;
158
+ try {
159
+ raw = fsImpl.readFileSync(absPath, 'utf8');
160
+ } catch {
161
+ return null;
162
+ }
163
+ return JSON.parse(raw);
164
+ }
165
+
166
+ /**
167
+ * Prune one kind end to end: resolve its baseline path, read it, build the
168
+ * scope inventory, plan, and (unless `check`) write.
169
+ *
170
+ * @param {object} params
171
+ * @returns {object} One entry of the report's `kinds` array.
172
+ */
173
+ function pruneKind({ kind, cwd, quality, check, fsImpl, requireFn }) {
174
+ const absPath = readerInternals.resolveBaselinePath(kind, { cwd });
175
+ const relPath = path.relative(cwd, absPath).split(path.sep).join('/');
176
+ const envelope = readEnvelope(absPath, fsImpl);
177
+ if (envelope === null) {
178
+ return { kind, path: relPath, present: false, removed: [], written: false };
179
+ }
180
+ const inventory = buildScopeInventory({ kind, cwd, quality, requireFn });
181
+ const result = pruneEnvelope({
182
+ kind,
183
+ envelope,
184
+ inventory,
185
+ existsOnDisk: (rel) => fsImpl.existsSync(path.resolve(cwd, rel)),
186
+ });
187
+ const base = {
188
+ kind,
189
+ path: relPath,
190
+ present: true,
191
+ degraded: inventory.degraded,
192
+ degradedReason: inventory.degraded ? inventory.reason : null,
193
+ skipped: result.skipped,
194
+ skipReason: result.reason,
195
+ removed: result.removed,
196
+ written: false,
197
+ };
198
+ if (result.envelope === null || check) return base;
199
+ writeFile(absPath, result.envelope, { fsImpl });
200
+ return { ...base, written: true };
201
+ }
202
+
203
+ /**
204
+ * Prune every file-keyed baseline under `cwd`.
205
+ *
206
+ * @param {{
207
+ * cwd?: string,
208
+ * kinds?: string[],
209
+ * check?: boolean,
210
+ * quality?: object,
211
+ * fsImpl?: typeof fs,
212
+ * requireFn?: (id: string) => object,
213
+ * }} params
214
+ * @returns {{ kinds: Array<object>, removedCount: number, writtenCount: number, check: boolean }}
215
+ */
216
+ export function runPrune({
217
+ cwd = process.cwd(),
218
+ kinds = PRUNABLE_KINDS,
219
+ check = false,
220
+ quality,
221
+ fsImpl = fs,
222
+ requireFn,
223
+ } = {}) {
224
+ const results = kinds.map((kind) =>
225
+ pruneKind({ kind, cwd, quality, check, fsImpl, requireFn }),
226
+ );
227
+ return {
228
+ check,
229
+ kinds: results,
230
+ removedCount: results.reduce((sum, r) => sum + r.removed.length, 0),
231
+ writtenCount: results.filter((r) => r.written).length,
232
+ };
233
+ }
@@ -76,11 +76,10 @@
76
76
  * @module .agents/scripts/lib/baselines/refresh-service
77
77
  */
78
78
 
79
- import { execFile as nodeExecFile } from 'node:child_process';
80
79
  import nodeFs from 'node:fs';
81
80
  import { createRequire } from 'node:module';
82
81
  import path from 'node:path';
83
- import { promisify } from 'node:util';
82
+ import { execFileCaptureAsync } from '../child-exec.js';
84
83
  import { getQuality, resolveConfig } from '../config-resolver.js';
85
84
  import {
86
85
  buildScopePredicate,
@@ -107,8 +106,6 @@ import {
107
106
 
108
107
  const nodeRequire = createRequire(import.meta.url);
109
108
 
110
- const execFileAsync = promisify(nodeExecFile);
111
-
112
109
  /**
113
110
  * Kinds the refresh service knows how to dispatch. Stays in lockstep with
114
111
  * the per-kind modules under `.agents/scripts/lib/baselines/kinds/`.
@@ -574,8 +571,9 @@ function assertRequiredScopeRows({
574
571
  }
575
572
 
576
573
  /**
577
- * Default git-diff derivation for the diff-scope path. Uses `execFile`
578
- * (no shell) and only the canonical two-dot range `baseRef..headRef` —
574
+ * Default git-diff derivation for the diff-scope path. Runs through the shared
575
+ * child-process surface ([`child-exec.js`](../child-exec.js)) `execFile`,
576
+ * never a shell — and only the canonical two-dot range `baseRef..headRef`:
579
577
  * triple-dot is intentionally avoided so the result reflects exactly the
580
578
  * files that differ between the two refs at the time of the call.
581
579
  *
@@ -585,10 +583,10 @@ function assertRequiredScopeRows({
585
583
  async function defaultGitDiff({ baseRef, headRef, cwd }) {
586
584
  const range = `${baseRef}..${headRef}`;
587
585
  try {
588
- const { stdout } = await execFileAsync(
586
+ const { stdout } = await execFileCaptureAsync(
589
587
  'git',
590
588
  ['diff', '--name-only', range],
591
- { cwd, maxBuffer: 16 * 1024 * 1024 },
589
+ { cwd },
592
590
  );
593
591
  return stdout
594
592
  .split(/\r?\n/)