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
@@ -40,46 +40,26 @@
40
40
  * @module lib/baselines/envelope
41
41
  */
42
42
 
43
- import fs from 'node:fs';
44
- import path from 'node:path';
45
- import { fileURLToPath } from 'node:url';
46
- import Ajv from 'ajv';
47
- import addFormats from 'ajv-formats';
48
-
49
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
43
+ import {
44
+ BASELINE_KIND_SCHEMA_FILES,
45
+ buildBaselineSchemaAjv,
46
+ } from '../baseline-schema-registry.js';
50
47
 
51
48
  /**
52
- * Repo-relative `.agents/schemas/baselines/` directory. Resolved off the
53
- * module URL so the envelope works the same in the main checkout, in a
54
- * worktree, and inside CI's bare clone — none of those have a stable
55
- * `process.cwd()` relative to the schemas.
49
+ * Canonical list of kinds the shared envelope supports, **derived** from the
50
+ * schema registry rather than restated (Story #5002).
51
+ *
52
+ * It used to be a hand-kept literal that happened to mirror
53
+ * `BASELINE_KIND_SCHEMA_FILES` name for name. Deriving it makes
54
+ * "`kind` is known" and "`kind`'s schema is registered" the same fact, so
55
+ * `getValidator` below cannot be handed a kind the AJV instance never
56
+ * compiled — the not-registered branch is gone because the state is
57
+ * unreachable, not because it was hidden.
56
58
  */
57
- const SCHEMAS_DIR = path.resolve(
58
- __dirname,
59
- '..',
60
- '..',
61
- '..',
62
- 'schemas',
63
- 'baselines',
59
+ export const KNOWN_KINDS = Object.freeze(
60
+ BASELINE_KIND_SCHEMA_FILES.map((file) => file.replace(/\.schema\.json$/, '')),
64
61
  );
65
62
 
66
- /**
67
- * Canonical list of kinds the shared envelope supports. The writer's
68
- * per-kind module list lives in `kinds/index.js`; this constant is the
69
- * envelope's view of the same set, kept here to break the import cycle
70
- * (`kinds/<kind>` imports envelope; envelope only needs the names).
71
- */
72
- export const KNOWN_KINDS = Object.freeze([
73
- 'lint',
74
- 'coverage',
75
- 'crap',
76
- 'maintainability',
77
- 'mutation',
78
- 'lighthouse',
79
- 'bundle-size',
80
- 'duplication',
81
- ]);
82
-
83
63
  function schemaRefFor(kind) {
84
64
  return `.agents/schemas/baselines/${kind}.schema.json`;
85
65
  }
@@ -193,36 +173,37 @@ export function buildEnvelope({
193
173
  }
194
174
 
195
175
  /**
196
- * Lazy AJV instance compiled schemas are memoised so successive writes
197
- * during a single Node process don't re-compile the seven schemas.
176
+ * Lazy AJV instance, built by the canonical
177
+ * [`baseline-schema-registry`](../baseline-schema-registry.js) the ONE
178
+ * place a baselines-directory schema is read off disk and compiled
179
+ * (Story #5002). This module used to keep its own `SCHEMAS_DIR` + AJV +
180
+ * per-kind compile cache alongside the registry's, so the writer and
181
+ * `baselines/reader.js` validated the same envelope through two independently
182
+ * configured instances; a registration added to one was invisible to the
183
+ * other. One builder means the writer and the reader cannot disagree about
184
+ * what a valid envelope is.
185
+ *
186
+ * Memoised: building it reads eleven schema files, and a single baseline
187
+ * regeneration calls `assertEnvelope` once per write plus once per re-read.
198
188
  */
199
189
  let _ajv = null;
200
- const _validators = new Map();
201
-
202
- function getAjv() {
203
- if (_ajv) return _ajv;
204
- const ajv = new Ajv({ allErrors: true, strict: false });
205
- addFormats(ajv);
206
- const envelopeSchema = JSON.parse(
207
- fs.readFileSync(
208
- path.join(SCHEMAS_DIR, 'baseline-envelope.schema.json'),
209
- 'utf8',
210
- ),
211
- );
212
- ajv.addSchema(envelopeSchema, 'baseline-envelope.schema.json');
213
- _ajv = ajv;
214
- return ajv;
190
+ function ajv() {
191
+ if (_ajv === null) {
192
+ _ajv = buildBaselineSchemaAjv();
193
+ }
194
+ return _ajv;
215
195
  }
216
196
 
197
+ /**
198
+ * Look up the pre-registered validator for a per-kind schema. Total over
199
+ * `KNOWN_KINDS` — that list is derived from the registry's own filenames, so
200
+ * every kind reaching here has a compiled schema.
201
+ *
202
+ * @param {string} kind
203
+ * @returns {import('ajv').ValidateFunction}
204
+ */
217
205
  function getValidator(kind) {
218
- if (_validators.has(kind)) return _validators.get(kind);
219
- const ajv = getAjv();
220
- const schema = JSON.parse(
221
- fs.readFileSync(path.join(SCHEMAS_DIR, `${kind}.schema.json`), 'utf8'),
222
- );
223
- const validate = ajv.compile(schema);
224
- _validators.set(kind, validate);
225
- return validate;
206
+ return ajv().getSchema(`${kind}.schema.json`);
226
207
  }
227
208
 
228
209
  /**
@@ -22,6 +22,13 @@
22
22
  // `ref` and `file` are passed as separate argv tokens, never
23
23
  // interpolated into a shell command.
24
24
  //
25
+ // Story #5009 moved the spawn itself, the stdout ceiling and the
26
+ // failure-message shape out to `lib/child-exec.js` — the one child-process
27
+ // surface — so this module now owns only what is genuinely its own: the
28
+ // `(ref, file)` LRU, the exit-code semantics below, and the test seam. The
29
+ // `__setSpawnRunner` seam still injects a mock runner; it is threaded through
30
+ // the shared wrapper as `run` rather than calling `spawnSync` directly.
31
+ //
25
32
  // Exit semantics are intentionally narrow:
26
33
  //
27
34
  // - Exit 0: stdout is the file contents, returned as a UTF-8 string.
@@ -39,7 +46,7 @@
39
46
  // means "path absent at ref". Callers may therefore treat a throw as a
40
47
  // hard failure without having to re-diagnose it.
41
48
 
42
- import { spawnSync } from 'node:child_process';
49
+ import { formatChildFailure, spawnChild } from '../child-exec.js';
43
50
 
44
51
  // ---------------------------------------------------------------------------
45
52
  // LRU cache. Small footprint — even a fan-out across all seven baseline
@@ -53,21 +60,13 @@ import { spawnSync } from 'node:child_process';
53
60
  const DEFAULT_MAX_ENTRIES = 64;
54
61
 
55
62
  /**
56
- * Story #4914 explicit stdout ceiling for every git read in this module.
57
- *
58
- * `child_process.spawnSync` defaults `maxBuffer` to 1 MB. A committed
59
- * baseline legitimately grows past that (a real consumer's crap baseline
60
- * measured 1,178,910 bytes), at which point the child is killed —
61
- * `status: null`, `signal: 'SIGTERM'`, `error.code: 'ENOBUFS'` — and the
62
- * read fails for a reason that has nothing to do with the repository.
63
+ * Injected spawn runner, or `undefined` to use the shared surface's real one.
64
+ * Story #4914's explicit 64 MB ceiling still applies to both git reads below —
65
+ * it is now `child-exec.js`'s `MAX_BUFFER_BYTES`, applied by `spawnChild`.
63
66
  *
64
- * 64 MB is not a new number: it is the bound already used at
65
- * `run-test-profile.js:87`, `audit-baselines/trend.js:61` and
66
- * `audit-baselines/weights.js:65`. This module was the outlier.
67
+ * @type {Function | undefined}
67
68
  */
68
- const MAX_BUFFER_BYTES = 64 * 1024 * 1024;
69
-
70
- let _spawnSync = spawnSync;
69
+ let _spawnRunner;
71
70
  let _cache = new Map();
72
71
  let _maxEntries = DEFAULT_MAX_ENTRIES;
73
72
 
@@ -80,12 +79,12 @@ let _maxEntries = DEFAULT_MAX_ENTRIES;
80
79
  * the contract.
81
80
  *
82
81
  * @param {object} [opts]
83
- * @param {typeof spawnSync} [opts.spawn] - Mock spawnSync.
84
- * @param {number} [opts.maxEntries] - Override LRU capacity.
82
+ * @param {Function} [opts.spawn] - Mock spawnSync.
83
+ * @param {number} [opts.maxEntries] - Override LRU capacity.
85
84
  */
86
85
  export function __setSpawnRunner(opts = {}) {
87
86
  if (opts.spawn) {
88
- _spawnSync = opts.spawn;
87
+ _spawnRunner = opts.spawn;
89
88
  }
90
89
  if (typeof opts.maxEntries === 'number') {
91
90
  _maxEntries = opts.maxEntries;
@@ -98,7 +97,7 @@ export function __setSpawnRunner(opts = {}) {
98
97
  * to keep state from leaking across files.
99
98
  */
100
99
  export function __resetForTests() {
101
- _spawnSync = spawnSync;
100
+ _spawnRunner = undefined;
102
101
  _maxEntries = DEFAULT_MAX_ENTRIES;
103
102
  _cache = new Map();
104
103
  }
@@ -179,13 +178,10 @@ export function readBaseFromGit(ref, file, opts = {}) {
179
178
 
180
179
  const cwd = opts.cwd ?? process.cwd();
181
180
  const spec = `${ref}:${file}`;
182
- const result = _spawnSync('git', ['show', spec], {
181
+ const result = spawnChild('git', ['show', spec], {
182
+ run: _spawnRunner,
183
183
  cwd,
184
- stdio: 'pipe',
185
- encoding: 'utf-8',
186
- shell: false,
187
184
  env: cleanGitEnv(),
188
- maxBuffer: MAX_BUFFER_BYTES,
189
185
  });
190
186
 
191
187
  // `child_process.spawnSync` returns `status: null` when the child died
@@ -206,9 +202,12 @@ export function readBaseFromGit(ref, file, opts = {}) {
206
202
  return null;
207
203
  }
208
204
 
209
- const stderr = (result.stderr || '').toString().trim();
210
205
  throw new Error(
211
- `readBaseFromGit: git show ${spec} failed (status=${status}): ${stderr}`,
206
+ formatChildFailure({
207
+ label: `readBaseFromGit: git show ${spec}`,
208
+ status,
209
+ stderr: result.stderr,
210
+ }),
212
211
  );
213
212
  }
214
213
 
@@ -222,9 +221,10 @@ export function readBaseFromGit(ref, file, opts = {}) {
222
221
  * Restricting the log to `-- <file>` means the returned subjects already
223
222
  * satisfy the "commit whose diff touches the baseline file" half of the
224
223
  * predicate; the caller only has to match the tag substring against each
225
- * subject. Runs through the same `spawnSync` seam as `readBaseFromGit`, so
226
- * it inherits the identical env scrubbing (drop inherited `GIT_*`) and the
227
- * spawn-not-exec security posture (argv tokens, `shell: false`).
224
+ * subject. Runs through the same shared `spawnChild` surface and injected
225
+ * runner as `readBaseFromGit`, so it inherits the identical env scrubbing
226
+ * (drop inherited `GIT_*`), the same stdout ceiling, and the spawn-not-exec
227
+ * security posture (argv tokens, `shell: false`).
228
228
  *
229
229
  * Returns an empty array whenever the range cannot be walked (missing base
230
230
  * ref, git failure, empty range) — the acknowledgment path treats "no
@@ -244,17 +244,10 @@ export function readRangeSubjectsTouchingFile(baseRef, file, opts = {}) {
244
244
  const cwd = opts.cwd ?? process.cwd();
245
245
  let result;
246
246
  try {
247
- result = _spawnSync(
247
+ result = spawnChild(
248
248
  'git',
249
249
  ['log', `${baseRef}..HEAD`, '--format=%s', '--', file],
250
- {
251
- cwd,
252
- stdio: 'pipe',
253
- encoding: 'utf-8',
254
- shell: false,
255
- env: cleanGitEnv(),
256
- maxBuffer: MAX_BUFFER_BYTES,
257
- },
250
+ { run: _spawnRunner, cwd, env: cleanGitEnv() },
258
251
  );
259
252
  } catch {
260
253
  return [];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * _crap-new-method-gate.js — the policy `kinds/crap.js#compareCrap` applies to
3
+ * a method that has no baseline row (Story #5002).
4
+ *
5
+ * Underscore-prefixed like `_shared-metric.js`: this is a helper for the
6
+ * per-kind modules in this directory, not a kind of its own.
7
+ *
8
+ * A leaf — it imports nothing, so `kinds/crap.js` can consume it without
9
+ * adding an edge to anything the comparator does not already reach.
10
+ */
11
+
12
+ /**
13
+ * The set of scanned files in which **every** measured method resolved 0%
14
+ * coverage — the observable signature of a source no test loads at all.
15
+ *
16
+ * Deliberately derived from the scan rows rather than plumbed down from the
17
+ * coverage artifact: the rows are the only thing both the preview gate and the
18
+ * close-validation projection hand the comparator, and "the whole file is at
19
+ * zero" is exactly what an all-zero istanbul entry produces here.
20
+ *
21
+ * A row carrying `coverage: null` is skipped, not counted as a zero — an
22
+ * absent observation is not a measured one, and letting it vote would hand the
23
+ * relief below to a file nothing measured at all.
24
+ *
25
+ * @param {Array<{file: string, coverage?: number|null}>} currentRows
26
+ * @returns {Set<string>}
27
+ */
28
+ export function deriveUncoveredFiles(currentRows) {
29
+ const allZeroByFile = new Map();
30
+ for (const row of currentRows ?? []) {
31
+ if (typeof row?.coverage !== 'number') continue;
32
+ const soFar = allZeroByFile.get(row.file);
33
+ allZeroByFile.set(row.file, (soFar ?? true) && row.coverage === 0);
34
+ }
35
+ const uncovered = new Set();
36
+ for (const [file, allZero] of allZeroByFile) {
37
+ if (allZero) uncovered.add(file);
38
+ }
39
+ return uncovered;
40
+ }
41
+
42
+ /**
43
+ * The score a **new** method is judged against `newMethodCeiling` with.
44
+ *
45
+ * Normally the measured CRAP. In a file whose every method scored 0%, the
46
+ * complexity term `c` alone.
47
+ *
48
+ * **Why.** `crap = c²·(1 − cov)³ + c` collapses to `c² + c` at zero coverage,
49
+ * so the default ceiling of 30 caps a *brand-new* method in an untested file
50
+ * at `c ≈ 5`. For the code that is untestable by construction — argv parsing,
51
+ * `spawn` wiring, a CLI's top-level `main()` — that is not a quality signal
52
+ * but a fragmentation tax: the only way past it is to shatter cohesive wiring
53
+ * into sub-five-branch fragments, which raises the file's method count and its
54
+ * reader's cost while measuring nothing. The coverage term still governs every
55
+ * method in a file the tests *do* reach, and a genuinely sprawling function is
56
+ * still refused — `c` alone must clear the ceiling.
57
+ *
58
+ * This is a **gate** decision, not a scoring one: the persisted baseline row
59
+ * keeps its measured `c² + c`, so the regression arm on the next run compares
60
+ * like with like.
61
+ *
62
+ * @param {{file: string, crap: number, cyclomatic?: number}} row
63
+ * @param {Set<string>} uncoveredFiles
64
+ * @returns {number}
65
+ */
66
+ export function newMethodGateScore(row, uncoveredFiles) {
67
+ if (!uncoveredFiles.has(row.file)) return row.crap;
68
+ return Number.isFinite(row.cyclomatic) ? row.cyclomatic : row.crap;
69
+ }
70
+
71
+ /**
72
+ * Assemble a new-method violation.
73
+ *
74
+ * `gateScore` is written **only** when the ceiling was judged against
75
+ * something other than the measured `crap` — the same write-only-when-
76
+ * non-default idiom `projectRow` uses for its row markers — so the printer can
77
+ * name the number that actually failed instead of one nothing tested.
78
+ *
79
+ * @param {object} row
80
+ * @param {number} ceiling
81
+ * @param {number} gateScore
82
+ * @returns {object}
83
+ */
84
+ export function buildNewViolation(row, ceiling, gateScore) {
85
+ return {
86
+ ...row,
87
+ kind: 'new',
88
+ baseline: null,
89
+ ceiling,
90
+ ...(gateScore === row.crap ? {} : { gateScore }),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Render the measured-vs-gated half of a new-method violation line.
96
+ *
97
+ * @param {{crap: number, gateScore?: number}} v
98
+ * @returns {string}
99
+ */
100
+ export function formatNewViolationMeasure(v) {
101
+ if (v.gateScore === undefined) return `crap=${v.crap.toFixed(2)}`;
102
+ return `complexity=${v.gateScore} (file has no coverage; crap=${v.crap.toFixed(2)})`;
103
+ }
@@ -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
  }