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
@@ -1,169 +0,0 @@
1
- /**
2
- * lib/workers/combined-mi-crap-worker.js — CPU-pool worker entry for the
3
- * combined MI + CRAP single-pass scan (`scanAndScoreCombined`).
4
- *
5
- * One file in, BOTH the maintainability score and the per-method CRAP rows
6
- * out — derived from a SINGLE `escomplex.analyzeModule` parse via
7
- * `analyzeOnce`. This collapses the two independent worker-pool passes the
8
- * full-tree baseline regenerator used to run (the MI worker parsed the AST
9
- * once for the module score, the CRAP worker parsed the same file's AST
10
- * again for the method rows) into one parse per file.
11
- *
12
- * The MI score and the CRAP rows have independent skip policies, mirroring
13
- * the two separate passes this worker replaces:
14
- * - **MI** never requires coverage. The module score is emitted for every
15
- * file that reads + transpiles + parses. A read failure yields
16
- * `miScore: null` (the host drops the file from the MI map, matching
17
- * `calculateAll`'s `score === null` filter). A transpile failure or a
18
- * parse error yields `miScore: 0` (matching `calculateForFile` /
19
- * `calculateForSource`, which return 0 on transpile-null / parse-error).
20
- * - **CRAP** honours `requireCoverage`. A file with no coverage entry is
21
- * reported as `skippedFileNoCoverage: true` (the host increments its own
22
- * counter and emits no CRAP rows for it) — but the MI score is STILL
23
- * computed and returned, because the MI pass would have scored it.
24
- *
25
- * Message contract — see lib/cpu-pool.js:
26
- * IN : { item: { abs: string, relPath: string, requireCoverage: boolean,
27
- * coverageAvailable?: boolean, coverageEntry: object | null } }
28
- * { exit: true }
29
- * OUT : { ok: true, result: {
30
- * relPath,
31
- * miScore: number | null,
32
- * skippedFileNoCoverage: boolean,
33
- * crapRows: Array<{ method, startLine, cyclomatic, coverage, crap }> | null,
34
- * skippedMethodsNoCoverage: number,
35
- * hasCoverageEntry: boolean,
36
- * resolvedMethods: number,
37
- * totalMethods: number,
38
- * } }
39
- *
40
- * A read/transpile/parse failure surfaces as `crapRows: null` so the host
41
- * loop drops the file's CRAP contribution (matching the crap-worker's
42
- * `rows: null` contract) — never aborts the whole scan. On a read failure
43
- * `miScore` is `null`; on a transpile/parse failure `miScore` is `0`.
44
- */
45
-
46
- import { parentPort } from 'node:worker_threads';
47
- import { finalizeMethodRows } from '../crap-engine.js';
48
- import { analyzeOnce } from '../crap-utils.js';
49
- import { prepareSourceForScoring } from '../transpile.js';
50
- import { serveWorkerMessages } from './serve-worker-messages.js';
51
-
52
- /**
53
- * Pure handler for a single inbound worker message. Exported so unit tests
54
- * can exercise every branch (bad-shape rejection, coverage gate, read /
55
- * transpile / parse failures, success rows, skipped methods, and the
56
- * MI-computed-even-when-coverage-skipped invariant) without spawning a real
57
- * `Worker` thread.
58
- *
59
- * Side effects (fs, transpile, analyzeOnce) are wired through `deps` so
60
- * tests pass deterministic stubs.
61
- *
62
- * @param {unknown} msg
63
- * @param {{
64
- * readFile?: (abs: string) => string,
65
- * transpile?: (abs: string, source: string, opts?: object) => unknown,
66
- * prepare?: (abs: string, deps: object) => object,
67
- * analyze?: (source: string, entry: object|null, mapLine: Function|null) => {
68
- * miScore: number,
69
- * crapRows: Array<object>,
70
- * parseError: boolean,
71
- * },
72
- * }} [deps]
73
- * @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
74
- */
75
- export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
76
- if (msg && msg.exit === true) return { kind: 'exit' };
77
-
78
- const item = msg?.item;
79
- if (
80
- !item ||
81
- typeof item.abs !== 'string' ||
82
- typeof item.relPath !== 'string'
83
- ) {
84
- return {
85
- kind: 'reply',
86
- message: {
87
- ok: false,
88
- error: `bad worker message: ${JSON.stringify(msg)}`,
89
- },
90
- };
91
- }
92
- const { abs, relPath, requireCoverage, coverageAvailable = true } = item;
93
- const prepare = deps.prepare ?? prepareSourceForScoring;
94
- const analyze = deps.analyze ?? analyzeOnce;
95
-
96
- // Coverage entry is pre-resolved on the host and attached to the item.
97
- // `item.coverageEntry` may be explicitly `null` when the file has no
98
- // coverage, or `undefined` when the caller did not supply it (treat as null).
99
- const entry = item.coverageEntry ?? null;
100
-
101
- const reply = (result) => ({
102
- kind: 'reply',
103
- message: {
104
- ok: true,
105
- result: {
106
- relPath,
107
- skippedFileNoCoverage: false,
108
- skippedMethodsNoCoverage: 0,
109
- hasCoverageEntry: entry !== null,
110
- resolvedMethods: 0,
111
- totalMethods: 0,
112
- ...result,
113
- },
114
- },
115
- });
116
-
117
- // Read + transpile once, carrying the transpiled -> original-source line
118
- // map the per-method coverage join needs (Story #4775). A read failure
119
- // means neither MI nor CRAP can be computed (MI drops as null, matching
120
- // `calculateAll`'s filter); a transpile failure scores MI 0 (matching
121
- // `calculateForFile`). CRAP drops the file either way.
122
- const prepared = prepare(abs, deps);
123
- if (prepared.error) {
124
- return reply({
125
- miScore: prepared.error === 'read' ? null : 0,
126
- crapRows: null,
127
- });
128
- }
129
-
130
- // ONE parse: analyzeOnce derives both the module MI score and the raw
131
- // per-method CRAP rows from a single escomplex report. On a parse error it
132
- // returns miScore 0 and an empty crapRows with parseError true.
133
- const {
134
- miScore,
135
- crapRows: rawCrapRows,
136
- parseError,
137
- } = analyze(prepared.code, entry, prepared.mapLine);
138
- if (parseError) {
139
- // Parse error: MI scores 0 (parity with calculateForSource's catch ->
140
- // returns 0), CRAP drops the file (rows null, parity with the crap
141
- // worker's calculateCrap-throw branch).
142
- return reply({ miScore: 0, crapRows: null });
143
- }
144
-
145
- // CRAP coverage gate runs AFTER the parse so the MI score is always
146
- // available. When the file has no coverage under requireCoverage, the CRAP
147
- // pass would have skipped it at the file level (no rows, counted) — but the
148
- // MI pass would still have scored it, so miScore is returned regardless.
149
- if (requireCoverage && entry === null) {
150
- return reply({ miScore, skippedFileNoCoverage: true, crapRows: [] });
151
- }
152
-
153
- const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
154
- finalizeMethodRows(rawCrapRows, {
155
- requireCoverage,
156
- coverageAvailable,
157
- });
158
- return reply({
159
- miScore,
160
- crapRows: rows,
161
- skippedMethodsNoCoverage,
162
- resolvedMethods,
163
- totalMethods,
164
- });
165
- }
166
-
167
- serveWorkerMessages(parentPort, (msg) =>
168
- handleCombinedMiCrapWorkerMessage(msg),
169
- );
@@ -1,507 +0,0 @@
1
- /* node:coverage ignore file -- top-level CLI gate; tested logic lives in lib/gates/baseline-store.js */
2
-
3
- import { spawnSync } from 'node:child_process';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { parseArgsStringToArgv } from 'string-argv';
7
- import { runAsCli } from './lib/cli-utils.js';
8
- import {
9
- getBaselines,
10
- getCommands,
11
- getLimits,
12
- resolveConfig,
13
- } from './lib/config-resolver.js';
14
- import { isDegraded, softFailOrThrow } from './lib/degraded-mode.js';
15
- import {
16
- BaselineNotFoundError,
17
- loadBaseline,
18
- writeBaseline,
19
- } from './lib/gates/baseline-store.js';
20
- import { Logger } from './lib/Logger.js';
21
-
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
- const PROJECT_ROOT = path.resolve(__dirname, '../..');
24
-
25
- /**
26
- * Hardcoded Node `spawnSync` buffer ceiling (Epic #1720 Story #1739 —
27
- * `limits.executionMaxBuffer` moved from `.agentrc.json` to a
28
- * framework-internal constant). 10 MiB matches the legacy default;
29
- * lint output that exceeds this much JSON is a symptom of misconfigured
30
- * scopes, not a tuning case.
31
- */
32
- const EXECUTION_MAX_BUFFER = 10485760;
33
-
34
- /**
35
- * Allowlist of command names that resolve via shell-launcher shims on
36
- * Windows (`.cmd` / `.bat`). Node's `spawnSync({ shell: false })` cannot
37
- * execute `.cmd` files (post CVE-2024-27980), so commands beginning with
38
- * one of these names are invoked through the platform shell. Restricting
39
- * `shell: true` to this fixed set keeps the command-injection surface
40
- * minimal while making the default `npx eslint …` invocation portable.
41
- */
42
- const SHIM_LAUNCHERS = new Set(['npx', 'npm', 'pnpm', 'pnpx', 'yarn']);
43
-
44
- /**
45
- * Decide how `spawnSync` should be invoked for `cmdConfig`. Returns the
46
- * full argument tuple — exported for unit testing the shim-detection
47
- * logic without a real process spawn.
48
- *
49
- * @param {string} cmdConfig — operator-configured command string.
50
- * @returns {{ shell: boolean, command: string, args: string[] }}
51
- */
52
- export function pickSpawnShape(cmdConfig) {
53
- const parsedArgs = parseArgsStringToArgv(cmdConfig);
54
- if (parsedArgs.length === 0) {
55
- return { shell: false, command: '', args: [] };
56
- }
57
- const head = parsedArgs[0];
58
- if (SHIM_LAUNCHERS.has(head)) {
59
- return { shell: true, command: cmdConfig, args: [] };
60
- }
61
- return { shell: false, command: head, args: parsedArgs.slice(1) };
62
- }
63
-
64
- // Shared core: extract the JSON-array tail from shell output, then tally
65
- // errors/warnings. `{ detailed: true }` also returns per-file counts + rule
66
- // histogram (used by `diff` + `captureBaseline` to attribute regressions).
67
- function parseLintShared(jsonStr, { detailed = false } = {}) {
68
- const empty = detailed
69
- ? { errorCount: 0, warningCount: 0, byFile: {} }
70
- : { errorCount: 0, warningCount: 0 };
71
- const startIndex = jsonStr.indexOf('[');
72
- const endIndex = jsonStr.lastIndexOf(']');
73
- if (startIndex === -1 || endIndex === -1) {
74
- if (jsonStr === '') return empty;
75
- throw new Error(
76
- `Could not find JSON array in output. Output: ${jsonStr.substring(0, 100)}`,
77
- );
78
- }
79
- const output = JSON.parse(jsonStr.substring(startIndex, endIndex + 1));
80
- let totalErrors = 0;
81
- let totalWarnings = 0;
82
- const byFile = detailed ? {} : null;
83
- for (const file of output) {
84
- const errorCount = file.errorCount || 0;
85
- const warningCount = file.warningCount || 0;
86
- totalErrors += errorCount;
87
- totalWarnings += warningCount;
88
- if (!detailed) continue;
89
- if (errorCount === 0 && warningCount === 0) continue;
90
- const filePath = file.filePath || file.file || '<unknown>';
91
- const rules = {};
92
- if (Array.isArray(file.messages)) {
93
- for (const msg of file.messages) {
94
- const ruleId = msg.ruleId || msg.rule || '<unknown>';
95
- rules[ruleId] = (rules[ruleId] || 0) + 1;
96
- }
97
- }
98
- byFile[filePath] = { errorCount, warningCount, rules };
99
- }
100
- return detailed
101
- ? { errorCount: totalErrors, warningCount: totalWarnings, byFile }
102
- : { errorCount: totalErrors, warningCount: totalWarnings };
103
- }
104
-
105
- export function parseLintOutput(jsonStr, _cmdConfig) {
106
- return parseLintShared(jsonStr, { detailed: false });
107
- }
108
-
109
- /**
110
- * Detailed twin of `parseLintOutput`: returns `byFile` with per-file
111
- * counts and a rule histogram keyed by ruleId (or `<unknown>`).
112
- */
113
- export function parseLintOutputDetailed(jsonStr, _cmdConfig) {
114
- return parseLintShared(jsonStr, { detailed: true });
115
- }
116
-
117
- // Soft-fail contract (Tech Spec #819): a JSON-parse failure emits the
118
- // degraded envelope (or hard-fails under `--gate-mode`) so callers see
119
- // the explicit signal rather than a silent zero-error fallback.
120
- function runLintShared(
121
- cmdConfig,
122
- executionTimeoutMs,
123
- executionMaxBuffer,
124
- gateModeOpts,
125
- { detailed = false } = {},
126
- ) {
127
- const shape = pickSpawnShape(cmdConfig);
128
- if (shape.command === '') {
129
- Logger.warn(`āš ļø [lint-baseline] Empty command configuration provided.`);
130
- return detailed
131
- ? { errorCount: 0, warningCount: 0, byFile: {} }
132
- : { errorCount: 0, warningCount: 0 };
133
- }
134
- const result = spawnSync(shape.command, shape.args, {
135
- cwd: PROJECT_ROOT,
136
- encoding: 'utf-8',
137
- timeout: executionTimeoutMs,
138
- maxBuffer: executionMaxBuffer,
139
- shell: shape.shell,
140
- });
141
- try {
142
- return parseLintShared(result.stdout.trim(), { detailed });
143
- } catch (err) {
144
- return softFailOrThrow(
145
- 'LINT_OUTPUT_PARSE_FAILED',
146
- `lint-baseline: failed to parse JSON from \`${cmdConfig}\`: ${err.message}`,
147
- gateModeOpts,
148
- );
149
- }
150
- }
151
-
152
- export function runLintCommand(
153
- cmdConfig,
154
- executionTimeoutMs,
155
- executionMaxBuffer,
156
- gateModeOpts,
157
- ) {
158
- return runLintShared(
159
- cmdConfig,
160
- executionTimeoutMs,
161
- executionMaxBuffer,
162
- gateModeOpts,
163
- {
164
- detailed: false,
165
- },
166
- );
167
- }
168
-
169
- function runLintCommandDetailed(
170
- cmdConfig,
171
- executionTimeoutMs,
172
- executionMaxBuffer,
173
- gateModeOpts,
174
- ) {
175
- return runLintShared(
176
- cmdConfig,
177
- executionTimeoutMs,
178
- executionMaxBuffer,
179
- gateModeOpts,
180
- {
181
- detailed: true,
182
- },
183
- );
184
- }
185
-
186
- export function captureBaseline(
187
- cmdConfig,
188
- executionTimeoutMs,
189
- executionMaxBuffer,
190
- baselinePath,
191
- baselinePathRel,
192
- gateModeOpts,
193
- ) {
194
- Logger.info(`ā–¶ [lint-baseline] Capturing lint baseline...`);
195
- const detailed = runLintCommandDetailed(
196
- cmdConfig,
197
- executionTimeoutMs,
198
- executionMaxBuffer,
199
- gateModeOpts,
200
- );
201
- if (isDegraded(detailed)) return detailed;
202
- writeBaseline({ baselinePath, data: detailed });
203
- Logger.info(
204
- `āœ… Baseline captured: ${detailed.errorCount} errors, ${detailed.warningCount} warnings.`,
205
- );
206
- Logger.info(` Saved to: ${baselinePathRel}`);
207
- return detailed;
208
- }
209
-
210
- /**
211
- * Pure: compute per-file regressions between a baseline and a current
212
- * detailed snapshot. Returns rows for files where current warnings or errors
213
- * exceed baseline. Each row carries the delta and the rules contributing the
214
- * most issues in the current snapshot. Sorted by descending warning delta
215
- * then descending error delta.
216
- *
217
- * Exported for testing.
218
- *
219
- * @param {object} baseline Object loaded from baseline.json (may lack `byFile`).
220
- * @param {object} current Detailed snapshot from `runLintCommandDetailed`.
221
- * @returns {{ file: string, errorDelta: number, warningDelta: number, rules: string[] }[]}
222
- */
223
- export function diffPerFile(baseline, current) {
224
- const baseFiles = baseline?.byFile ?? {};
225
- const curFiles = current?.byFile ?? {};
226
- const rows = [];
227
- for (const [filePath, cur] of Object.entries(curFiles)) {
228
- const base = baseFiles[filePath] ?? { errorCount: 0, warningCount: 0 };
229
- const errorDelta = cur.errorCount - (base.errorCount || 0);
230
- const warningDelta = cur.warningCount - (base.warningCount || 0);
231
- if (errorDelta <= 0 && warningDelta <= 0) continue;
232
- const rules = Object.entries(cur.rules || {})
233
- .sort((a, b) => b[1] - a[1])
234
- .map(([rule]) => rule);
235
- rows.push({ file: filePath, errorDelta, warningDelta, rules });
236
- }
237
- rows.sort((a, b) => {
238
- if (b.warningDelta !== a.warningDelta) {
239
- return b.warningDelta - a.warningDelta;
240
- }
241
- return b.errorDelta - a.errorDelta;
242
- });
243
- return rows;
244
- }
245
-
246
- /**
247
- * Pure: render the diff rows as a fixed-width table suitable for terminal
248
- * output. When there are no regressions, emits a single line. When the
249
- * baseline lacks `byFile`, prepends a one-line note so operators understand
250
- * why every currently-warning file shows as "new".
251
- *
252
- * Exported for testing.
253
- */
254
- export function formatDiffTable(rows, { baselineHasByFile } = {}) {
255
- if (rows.length === 0) {
256
- return 'āœ… No per-file regressions detected.';
257
- }
258
- const FILE_HEADER = 'File';
259
- const DELTA_HEADER = 'Ī” warn/err';
260
- const RULES_HEADER = 'rules';
261
- const fileWidth = Math.max(
262
- FILE_HEADER.length,
263
- ...rows.map((r) => r.file.length),
264
- );
265
- const deltaCells = rows.map((r) => `+${r.warningDelta}w / +${r.errorDelta}e`);
266
- const deltaWidth = Math.max(
267
- DELTA_HEADER.length,
268
- ...deltaCells.map((c) => c.length),
269
- );
270
- const lines = [];
271
- if (!baselineHasByFile) {
272
- lines.push(
273
- 'ā„¹ļø Baseline has no per-file data; treating every regression as "new since baseline".',
274
- );
275
- }
276
- lines.push(
277
- `${FILE_HEADER.padEnd(fileWidth)} ${DELTA_HEADER.padEnd(deltaWidth)} ${RULES_HEADER}`,
278
- );
279
- lines.push(
280
- `${'-'.repeat(fileWidth)} ${'-'.repeat(deltaWidth)} ${'-'.repeat(RULES_HEADER.length)}`,
281
- );
282
- for (let i = 0; i < rows.length; i += 1) {
283
- const r = rows[i];
284
- const ruleStr = r.rules.length > 0 ? r.rules.join(', ') : '(no ruleId)';
285
- lines.push(
286
- `${r.file.padEnd(fileWidth)} ${deltaCells[i].padEnd(deltaWidth)} ${ruleStr}`,
287
- );
288
- }
289
- return lines.join('\n');
290
- }
291
-
292
- /**
293
- * Load the baseline at `baselinePath` and return an empty-shape fallback when
294
- * it is missing. `BaselineNotFoundError` is the only error class we tolerate
295
- * — anything else propagates. Pure-ish (warns via Logger).
296
- *
297
- * @param {{ baselinePath: string, baselinePathRel: string, includeByFile: boolean }} args
298
- * @returns {{ baseline: object, baselineHasByFile: boolean }}
299
- */
300
- export function loadBaselineWithFallback({
301
- baselinePath,
302
- baselinePathRel,
303
- includeByFile,
304
- }) {
305
- const emptyBaseline = includeByFile
306
- ? { errorCount: 0, warningCount: 0, byFile: {} }
307
- : { errorCount: 0, warningCount: 0 };
308
- try {
309
- const baseline = loadBaseline({ baselinePath });
310
- const baselineHasByFile =
311
- includeByFile &&
312
- baseline &&
313
- typeof baseline.byFile === 'object' &&
314
- baseline.byFile !== null;
315
- return { baseline, baselineHasByFile };
316
- } catch (err) {
317
- if (!(err instanceof BaselineNotFoundError)) throw err;
318
- const msg = includeByFile
319
- ? `āš ļø No baseline found at ${baselinePathRel}. Treating baseline as empty.`
320
- : `āš ļø No baseline found at ${baselinePathRel}. Assuming 0 baseline.`;
321
- Logger.warn(msg);
322
- return { baseline: emptyBaseline, baselineHasByFile: false };
323
- }
324
- }
325
-
326
- export function diffBaseline(
327
- cmdConfig,
328
- executionTimeoutMs,
329
- executionMaxBuffer,
330
- baselinePath,
331
- baselinePathRel,
332
- gateModeOpts,
333
- ) {
334
- Logger.info(`ā–¶ [lint-baseline] Computing per-file regressions...`);
335
- const current = runLintCommandDetailed(
336
- cmdConfig,
337
- executionTimeoutMs,
338
- executionMaxBuffer,
339
- gateModeOpts,
340
- );
341
- if (isDegraded(current)) return current;
342
-
343
- const { baseline, baselineHasByFile } = loadBaselineWithFallback({
344
- baselinePath,
345
- baselinePathRel,
346
- includeByFile: true,
347
- });
348
-
349
- Logger.info(
350
- ` Baseline: ${baseline.errorCount ?? 0} errors, ${baseline.warningCount ?? 0} warnings`,
351
- );
352
- Logger.info(
353
- ` Current: ${current.errorCount} errors, ${current.warningCount} warnings`,
354
- );
355
- Logger.info('');
356
-
357
- const rows = diffPerFile(baseline, current);
358
- Logger.info(formatDiffTable(rows, { baselineHasByFile }));
359
- return { ...current, regressions: rows };
360
- }
361
-
362
- /** Pure predicate: current snapshot has more errors or warnings than baseline. */
363
- export function hasDegraded(current, baseline) {
364
- return (
365
- current.errorCount > baseline.errorCount ||
366
- current.warningCount > baseline.warningCount
367
- );
368
- }
369
-
370
- /** Pure predicate: current snapshot has strictly fewer of either count. */
371
- export function hasImproved(current, baseline) {
372
- return (
373
- current.errorCount < baseline.errorCount ||
374
- current.warningCount < baseline.warningCount
375
- );
376
- }
377
-
378
- export function checkBaseline(
379
- cmdConfig,
380
- executionTimeoutMs,
381
- executionMaxBuffer,
382
- baselinePath,
383
- baselinePathRel,
384
- gateModeOpts,
385
- ) {
386
- Logger.info(`ā–¶ [lint-baseline] Checking lint against baseline...`);
387
- const current = runLintCommand(
388
- cmdConfig,
389
- executionTimeoutMs,
390
- executionMaxBuffer,
391
- gateModeOpts,
392
- );
393
- if (isDegraded(current)) return current;
394
-
395
- const { baseline } = loadBaselineWithFallback({
396
- baselinePath,
397
- baselinePathRel,
398
- includeByFile: false,
399
- });
400
-
401
- Logger.info(
402
- ` Baseline: ${baseline.errorCount} errors, ${baseline.warningCount} warnings`,
403
- );
404
- Logger.info(
405
- ` Current: ${current.errorCount} errors, ${current.warningCount} warnings`,
406
- );
407
-
408
- if (hasDegraded(current, baseline)) {
409
- throw new Error(
410
- '\n🚨 LINT DEGRADATION DETECTED! You have introduced new lint issues compared to the baseline.',
411
- );
412
- }
413
- if (hasImproved(current, baseline)) {
414
- writeBaseline({ baselinePath, data: current });
415
- Logger.info(
416
- `šŸŽ‰ Lint health improved! Ratcheted baseline down to current levels.`,
417
- );
418
- }
419
-
420
- Logger.info(`āœ… Lint check passed.`);
421
- return current;
422
- }
423
-
424
- /**
425
- * Orchestration body of `main` extracted as a sibling exported function so
426
- * the validate / dispatch / classify-degraded ladder is unit-testable
427
- * without spawning a process. `main` becomes a thin shell: parse → call
428
- * this → render → exit. CLI surface unchanged (same modes, same exit
429
- * codes, same stdout JSON schema for degraded envelopes).
430
- *
431
- * Note: `checkBaseline` itself still throws on real lint degradation
432
- * (preserved CLI behaviour — `runAsCli` maps the throw to exit 1). Tests
433
- * for the validation-error branch use the explicit `'invalid'` mode which
434
- * never reaches the runner.
435
- *
436
- * @param {{ mode: string, gateModeArgv?: string[] }} values
437
- * @param {{
438
- * resolveConfig?: typeof resolveConfig,
439
- * runners?: { capture?: Function, check?: Function, diff?: Function },
440
- * env?: Record<string, string|undefined>,
441
- * projectRoot?: string,
442
- * }} [deps]
443
- * @returns {Promise<{ exitCode: number, result: object }>}
444
- * `result.kind` is one of: `'validation-error'`, `'envelope'`. Envelopes
445
- * carry the raw runner output; `exitCode === 1` iff `isDegraded(envelope)`.
446
- */
447
- export async function runLintBaselineCli(values, deps = {}) {
448
- const { mode, gateModeArgv = [] } = values;
449
- if (mode !== 'capture' && mode !== 'check' && mode !== 'diff') {
450
- return {
451
- exitCode: 1,
452
- result: {
453
- kind: 'validation-error',
454
- message:
455
- 'Usage: node lint-baseline.js <capture|check|diff> [--gate-mode]',
456
- },
457
- };
458
- }
459
-
460
- const cfg = deps.resolveConfig ? deps.resolveConfig() : resolveConfig();
461
- const cmdConfig = getCommands(cfg).lintBaseline;
462
- const baselinePathRel = getBaselines(cfg).lint.path;
463
- const projectRoot = deps.projectRoot ?? PROJECT_ROOT;
464
- const baselinePath = path.resolve(projectRoot, baselinePathRel);
465
- const limits = getLimits(cfg);
466
- const env = deps.env ?? process.env;
467
-
468
- const gateModeOpts = { argv: gateModeArgv, env };
469
-
470
- const runners = deps.runners ?? {
471
- capture: captureBaseline,
472
- check: checkBaseline,
473
- diff: diffBaseline,
474
- };
475
- const runner = runners[mode];
476
- // Hardcoded post-reshape (Epic #1720 Story #1739) — see EXECUTION_MAX_BUFFER.
477
- const envelope = runner(
478
- cmdConfig,
479
- limits.executionTimeoutMs,
480
- EXECUTION_MAX_BUFFER,
481
- baselinePath,
482
- baselinePathRel,
483
- gateModeOpts,
484
- );
485
-
486
- return {
487
- exitCode: isDegraded(envelope) ? 1 : 0,
488
- result: { kind: 'envelope', envelope },
489
- };
490
- }
491
-
492
- export async function main(args = process.argv) {
493
- const values = { mode: args[2], gateModeArgv: args.slice(3) };
494
- const { exitCode, result } = await runLintBaselineCli(values);
495
-
496
- if (result.kind === 'validation-error') {
497
- throw new Error(result.message);
498
- }
499
- // kind === 'envelope': only print on degraded soft-fail (preserves
500
- // pre-refactor stdout contract — happy paths stay quiet on stdout).
501
- if (exitCode === 1 && isDegraded(result.envelope)) {
502
- process.stdout.write(`${JSON.stringify(result.envelope)}\n`);
503
- }
504
- process.exit(exitCode);
505
- }
506
-
507
- runAsCli(import.meta.url, main, { source: 'LintBaseline' });