mandrel 2.30.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 (253) 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/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  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 +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  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 +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. 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' });