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
@@ -4,19 +4,13 @@ import escomplex from 'typhonjs-escomplex';
4
4
  import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
5
5
  import { findCoverageEntry } from './coverage-utils.js';
6
6
  import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
7
- import { finalizeMethodRowsWithBaseline } from './crap-baseline-join.js';
8
7
  import {
9
- COORDINATE_ORIGINAL,
10
- finalizeMethodRows,
11
- methodRowsFromReport,
12
- } from './crap-engine.js';
13
- import {
14
- resolvedFromBaselineFlag,
8
+ finalizeMethodRowsWithBaseline,
15
9
  resolveIncrementalContext,
16
10
  resolveQueueIncrementalFields,
17
- shouldRunSerial,
18
11
  shouldSkipFileForNoCoverage,
19
- } from './crap-utils-incremental.js';
12
+ } from './crap-baseline-join.js';
13
+ import { COORDINATE_ORIGINAL, methodRowsFromReport } from './crap-engine.js';
20
14
  import { Logger } from './Logger.js';
21
15
  import { scanDirectory } from './maintainability-utils.js';
22
16
  import {
@@ -25,10 +19,6 @@ import {
25
19
  } from './transpile.js';
26
20
 
27
21
  const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
28
- const COMBINED_MI_CRAP_WORKER_URL = new URL(
29
- './workers/combined-mi-crap-worker.js',
30
- import.meta.url,
31
- );
32
22
 
33
23
  // Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
34
24
  // POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
@@ -79,192 +69,6 @@ export function resolveEscomplexVersion(cwd = process.cwd()) {
79
69
  return '0.0.0';
80
70
  }
81
71
 
82
- function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
83
- if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
84
- throw new TypeError(
85
- 'crap-utils: opts.baselinePath is required (Epic #730 Story 5.5 — ' +
86
- 'callers resolve the path via getBaselines(config).crap.path).',
87
- );
88
- }
89
- return path.isAbsolute(baselinePath)
90
- ? baselinePath
91
- : path.join(cwd, baselinePath);
92
- }
93
-
94
- /**
95
- * Load the CRAP baseline envelope from disk.
96
- *
97
- * Returns the parsed envelope on success, or `null` when the file is missing,
98
- * unreadable, or structurally unusable. Version-mismatch detection is a
99
- * caller concern — this loader never silently rescores or mutates the
100
- * envelope.
101
- *
102
- * @param {{cwd?: string, baselinePath?: string}} [opts]
103
- * @returns {{
104
- * kernelVersion: string,
105
- * escomplexVersion: string,
106
- * rows: Array<{file: string, method: string, startLine: number, crap: number}>,
107
- * }|null}
108
- */
109
- /**
110
- * The envelope-level fields the CRAP compat axes read off a *loaded* baseline
111
- * — the set every read path owes `assertBaselineCompatible`.
112
- *
113
- * It exists because there are TWO read paths and they have now diverged three
114
- * times. `check-baselines` loads through `baselines/reader.js`; the
115
- * `quality-preview` pre-commit arm loads through `projectCrapEnvelopeToLegacy`
116
- * below. Both are ALLOW-LISTS, both feed the same axes, and a stamp added to
117
- * one and not the other yields two opposite verdicts on one file: Story #4866
118
- * (`scoringSemantics`, `tsTranspilerVersion`), Story #4969 (`rows[].anonymous`)
119
- * and Story #4986 (`provenanceStamped`, half-fixed by #4973) were each that
120
- * same half-landing.
121
- *
122
- * Every one of those axes keys on a POSITIVE marker, so a dropped field reads
123
- * `undefined` and fails the baseline closed with a remedy that cannot work —
124
- * re-deriving it writes the stamp the read path then discards. The projection
125
- * below is DERIVED from this list rather than repeating it, so a new stamp is
126
- * carried here the moment it is named; a parity test holds the reader to the
127
- * same set and names whichever path forgot one.
128
- *
129
- * Row-level markers are deliberately out: they are projected per-row, not as
130
- * envelope stamps.
131
- *
132
- * Deliberately module-local, mirroring `SCORING_SEMANTICS` in
133
- * `baselines/kinds/crap.js`: the writer's `envelopeExtras()` is the single
134
- * production door to the stamp set, and exporting this list would add a second
135
- * one that only a test reaches. The parity test holds this projection to
136
- * `Object.keys(envelopeExtras())` instead, so a stamp the writer starts
137
- * emitting fails the test here until it is named — which is the enforcement
138
- * this list needs, not an export.
139
- */
140
- const COMPAT_STAMP_FIELDS = Object.freeze([
141
- 'scoringSemantics',
142
- 'tsTranspilerVersion',
143
- 'provenanceStamped',
144
- ]);
145
-
146
- /**
147
- * Per-stamp coercion applied on the way through the legacy projection. A field
148
- * with no entry is carried VERBATIM, which is the correct default: the axes
149
- * distinguish "stamped" from "absent", so inventing a value for a stamp the
150
- * envelope never wrote is the one thing a read path must not do.
151
- */
152
- const COMPAT_STAMP_NORMALIZERS = {
153
- // `null` (not the running value) when unstamped, so `ts-transpiler-drift`
154
- // can tell "written by a different transpiler" from "written before the
155
- // stamp existed" instead of comparing a value against itself.
156
- tsTranspilerVersion: (value) => (typeof value === 'string' ? value : null),
157
- scoringSemantics: (value) => value ?? null,
158
- };
159
-
160
- /**
161
- * Project the compat stamps off a v2 envelope, driven by
162
- * `COMPAT_STAMP_FIELDS`.
163
- *
164
- * @param {Record<string, unknown>} parsed
165
- * @returns {Record<string, unknown>}
166
- */
167
- function projectCompatStamps(parsed) {
168
- const stamps = {};
169
- for (const field of COMPAT_STAMP_FIELDS) {
170
- const normalize = COMPAT_STAMP_NORMALIZERS[field];
171
- stamps[field] = normalize ? normalize(parsed[field]) : parsed[field];
172
- }
173
- return stamps;
174
- }
175
-
176
- /**
177
- * Story #1895: shipped baseline switched to the canonical envelope shape
178
- * (`$schema`, `kernelVersion`, `generatedAt`, `rollup`, `rows` keyed on
179
- * `path`). Backfill the legacy `escomplexVersion` field from the running
180
- * scorer and re-key rows by `file` so existing comparators keep working
181
- * until Story #1912 lands the unified gate. Detection probes the first row
182
- * for the new `path` key — the legacy envelope also carries `$schema` but
183
- * keys rows by `file`.
184
- *
185
- * **Compat stamps survive the projection (Story #4866).** `scoringSemantics`
186
- * and `tsTranspilerVersion` are *baseline* facts, and the projection used to
187
- * drop the first and overwrite the second with the RUNNING value — which made
188
- * every compat axis reading them either unstamped or vacuously self-equal.
189
- * They are carried verbatim now, `null` when the envelope never stamped them,
190
- * so an axis can tell "written by a different transpiler" apart from "written
191
- * before the stamp existed" instead of guessing.
192
- *
193
- * **This projection is one of TWO (Story #4986).** `check-baselines` reads a
194
- * baseline through `baselines/reader.js`; `quality-preview` reads the same file
195
- * through here. Both feed `assertBaselineCompatible`, so a stamp added to one
196
- * allow-list and not the other produces two opposite verdicts on one envelope —
197
- * which is what happened to `provenanceStamped`: #4973 added it to the reader
198
- * and left this projection dropping it, so the pre-commit CRAP arm rejected
199
- * every stamped baseline with an un-satisfiable "re-seed" remedy while the
200
- * authoritative gate passed. The stamp block is derived from
201
- * `COMPAT_STAMP_FIELDS` above so this projection cannot fall behind again.
202
- */
203
- function projectCrapEnvelopeToLegacy(parsed) {
204
- if (
205
- !Array.isArray(parsed.rows) ||
206
- parsed.rows.length === 0 ||
207
- typeof parsed.rows[0]?.path !== 'string'
208
- ) {
209
- return null;
210
- }
211
- return {
212
- kernelVersion: parsed.kernelVersion,
213
- escomplexVersion: resolveEscomplexVersion(),
214
- ...projectCompatStamps(parsed),
215
- rows: parsed.rows.map((row) => ({
216
- crap: row.crap,
217
- file: row.path,
218
- method: row.method,
219
- startLine: row.startLine,
220
- ...(row.coordinateSystem === undefined
221
- ? {}
222
- : { coordinateSystem: row.coordinateSystem }),
223
- // Story #4969, same reason as the stamps above: `anonymous` is a
224
- // BASELINE fact. Dropping it here left every re-keyed row looking like
225
- // an unmarked anonymous one, which is precisely the shape the
226
- // `anon-identity-unstamped` axis fails closed — the projection would
227
- // have manufactured the very defect the axis exists to catch.
228
- ...(row.anonymous === undefined ? {} : { anonymous: row.anonymous }),
229
- })),
230
- };
231
- }
232
-
233
- export function getCrapBaseline(opts = {}) {
234
- const filePath = resolveBaselinePath(opts);
235
- if (!fs.existsSync(filePath)) return null;
236
- let raw;
237
- try {
238
- raw = fs.readFileSync(filePath, 'utf-8');
239
- } catch (err) {
240
- Logger.warn(`[crap-utils] unable to read baseline: ${err.message}`);
241
- return null;
242
- }
243
- let parsed;
244
- try {
245
- parsed = JSON.parse(raw);
246
- } catch (err) {
247
- Logger.warn(`[crap-utils] baseline is not valid JSON: ${err.message}`);
248
- return null;
249
- }
250
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
251
- return null;
252
- }
253
- const projected = projectCrapEnvelopeToLegacy(parsed);
254
- if (projected) return projected;
255
- if (typeof parsed.kernelVersion !== 'string') return null;
256
- if (typeof parsed.escomplexVersion !== 'string') return null;
257
- if (!Array.isArray(parsed.rows)) return null;
258
- // tsTranspilerVersion landed in kernel 1.1.0. Older envelopes (1.0.0)
259
- // do not carry it; we surface that as the sentinel '0.0.0' so the
260
- // version-drift detector can warn on first 1.1.0 check without
261
- // crashing on a missing field.
262
- if (typeof parsed.tsTranspilerVersion !== 'string') {
263
- parsed.tsTranspilerVersion = '0.0.0';
264
- }
265
- return parsed;
266
- }
267
-
268
72
  /**
269
73
  * Project rich scan rows onto the minimal baseline row shape and assemble an
270
74
  * envelope ready for the shared V2 writer.
@@ -428,9 +232,9 @@ export function checkResolutionFloor(resolution, floor) {
428
232
  * Parse `source` exactly once with escomplex and derive both the
429
233
  * maintainability score and the raw CRAP method rows from that single report.
430
234
  *
431
- * Callers that need both scores for the same source string (e.g. a combined
432
- * CRAP + MI scan) MUST use this helper rather than calling `calculateCrapForSource`
433
- * and `calculateForSource` separately — doing so would parse the AST twice.
235
+ * Callers that need both scores for the same source string MUST use this
236
+ * helper rather than calling `calculateCrapForSource` and `calculateForSource`
237
+ * separately — doing so would parse the AST twice.
434
238
  *
435
239
  * Coverage-dependent CRAP values require a `coverageForFile` entry (the value
436
240
  * from `coverage-final.json` for this file). Pass `null` when no coverage is
@@ -456,7 +260,7 @@ export function checkResolutionFloor(resolution, floor) {
456
260
  * parseError: boolean,
457
261
  * }}
458
262
  */
459
- export function analyzeOnce(source, coverageForFile, mapLine = null) {
263
+ function analyzeOnce(source, coverageForFile, mapLine = null) {
460
264
  let report;
461
265
  try {
462
266
  report = escomplex.analyzeModule(source);
@@ -469,6 +273,69 @@ export function analyzeOnce(source, coverageForFile, mapLine = null) {
469
273
  return { report, miScore, crapRows, parseError: false };
470
274
  }
471
275
 
276
+ /**
277
+ * Build `scanAndScore`'s per-file work queue: canonicalise each discovered
278
+ * absolute path, drop everything outside `scopeSet`, and merge the
279
+ * incremental-join fields onto the surviving items.
280
+ *
281
+ * Story #2079: every relPath goes through path-canon so a scan from inside
282
+ * `.worktrees/<workspace>/` (with `cwd` pointing at the main checkout) cannot
283
+ * leak the worktree prefix into the on-disk baseline's `file` / `path` keys.
284
+ *
285
+ * @param {string[]} files Absolute paths, already sorted.
286
+ * @param {{
287
+ * cwd: string,
288
+ * scopeSet: Set<string>|null,
289
+ * requireCoverage: boolean,
290
+ * coverageAvailable: boolean,
291
+ * incrementalCtx: object,
292
+ * }} opts
293
+ * @returns {Array<object>}
294
+ */
295
+ function buildScanQueue(
296
+ files,
297
+ { cwd, scopeSet, requireCoverage, coverageAvailable, incrementalCtx },
298
+ ) {
299
+ const queue = [];
300
+ for (const abs of files) {
301
+ const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
302
+ const relPath = canonicalisePath(rawRel);
303
+ if (scopeSet && !scopeSet.has(relPath)) continue;
304
+ queue.push(
305
+ resolveQueueIncrementalFields(
306
+ { abs, relPath, requireCoverage, coverageAvailable },
307
+ incrementalCtx,
308
+ ),
309
+ );
310
+ }
311
+ return queue;
312
+ }
313
+
314
+ /**
315
+ * Project one finalized method row onto the enriched scan-row shape
316
+ * `compareCrap` and the baseline writer consume.
317
+ *
318
+ * @param {string} relPath Canonical repo-relative path of the scanned file.
319
+ * @param {object} mr A row from `finalizeMethodRowsWithBaseline`.
320
+ * @returns {object}
321
+ */
322
+ function projectScanRow(relPath, mr) {
323
+ return {
324
+ file: relPath,
325
+ method: mr.method,
326
+ // Story #4969: `method` may be a derived anonymous identity; the flag is
327
+ // what lets the persisted row say so.
328
+ anonymous: mr.anonymous === true,
329
+ startLine: mr.startLine,
330
+ cyclomatic: mr.cyclomatic,
331
+ coverage: mr.coverage,
332
+ crap: mr.crap,
333
+ coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
334
+ // Present only when true, so a full-scope scan's rows are unaffected.
335
+ ...(mr.resolvedFromBaseline === true ? { resolvedFromBaseline: true } : {}),
336
+ };
337
+ }
338
+
472
339
  /**
473
340
  * Scan `targetDirs` for JS files, score each method via the CRAP kernel, and
474
341
  * return enriched rows plus skip counters. Does not write to disk.
@@ -551,26 +418,20 @@ export async function scanAndScore({
551
418
 
552
419
  // Build the work-queue first so scopeFile filtering happens before
553
420
  // any I/O / IPC. `scannedFiles` is the in-scope count.
554
- // Story #2079: route every relPath through path-canon so a scan from
555
- // inside `.worktrees/<workspace>/` (with cwd pointing at the main
556
- // checkout) cannot leak the worktree prefix into the on-disk baseline's
557
- // `file` / `path` keys downstream.
558
- const coverageAvailable = isCoverageArtifactPresent(coverage);
559
- const queue = [];
560
- for (const abs of files) {
561
- const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
562
- const relPath = canonicalisePath(rawRel);
563
- if (scopeSet && !scopeSet.has(relPath)) continue;
564
- queue.push(
565
- resolveQueueIncrementalFields(
566
- { abs, relPath, requireCoverage, coverageAvailable },
567
- incrementalCtx,
568
- ),
569
- );
570
- }
421
+ const queue = buildScanQueue(files, {
422
+ cwd,
423
+ scopeSet,
424
+ requireCoverage,
425
+ coverageAvailable: isCoverageArtifactPresent(coverage),
426
+ incrementalCtx,
427
+ });
571
428
  const scannedFiles = queue.length;
572
429
 
573
- const perFile = shouldRunSerial(queue.length, incremental, SERIAL_THRESHOLD)
430
+ // Serial below the pool cutover, and ALWAYS in incremental mode: the
431
+ // per-file baseline lookup Maps the join needs do not cross the worker
432
+ // boundary (Story #4981).
433
+ const runSerial = queue.length < SERIAL_THRESHOLD || Boolean(incremental);
434
+ const perFile = runSerial
574
435
  ? queue.map((item) => ({ item, result: scoreFileSerial(item, coverage) }))
575
436
  : await scoreFilesViaPool(queue, coverage);
576
437
 
@@ -598,19 +459,7 @@ export async function scanAndScore({
598
459
  skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
599
460
  accumulateResolution(resolution, item.relPath, result);
600
461
  for (const mr of result.rows) {
601
- rows.push({
602
- file: item.relPath,
603
- method: mr.method,
604
- // Story #4969: `method` may be a derived anonymous identity; the flag
605
- // is what lets the persisted row say so.
606
- anonymous: mr.anonymous === true,
607
- startLine: mr.startLine,
608
- cyclomatic: mr.cyclomatic,
609
- coverage: mr.coverage,
610
- crap: mr.crap,
611
- coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
612
- ...resolvedFromBaselineFlag(mr),
613
- });
462
+ rows.push(projectScanRow(item.relPath, mr));
614
463
  }
615
464
  }
616
465
 
@@ -635,8 +484,7 @@ export async function scanAndScore({
635
484
  * reference implementation against which the worker output is asserted
636
485
  * byte-for-byte in the cpu-pool tests.
637
486
  *
638
- * Uses `analyzeOnce` so the source is parsed a single time and both the
639
- * CRAP rows and the MI score are derived from the same escomplex report.
487
+ * Uses `analyzeOnce` so the source is parsed a single time.
640
488
  */
641
489
  function scoreFileSerial(
642
490
  {
@@ -713,287 +561,3 @@ async function scoreFilesViaPool(queue, coverage) {
713
561
  return { item, result: r };
714
562
  });
715
563
  }
716
-
717
- /**
718
- * In-process combined scorer: parse `abs` exactly once via `analyzeOnce` and
719
- * derive BOTH the module MI score and the per-method CRAP rows. The reference
720
- * implementation for the combined worker, used directly below
721
- * `SERIAL_THRESHOLD` (matching the serial fast paths of `calculateAll` and
722
- * `scanAndScore`).
723
- *
724
- * Return shape mirrors `combined-mi-crap-worker.js`:
725
- * - `miScore` — `null` on read failure (MI dropped by the host), `0` on
726
- * transpile-null / parse-error (parity with `calculateForFile` /
727
- * `calculateForSource`), otherwise the module maintainability index.
728
- * - `crapRows` — `null` on read/transpile/parse failure (CRAP drops the
729
- * file), `[]` when coverage-skipped, otherwise the scored method rows.
730
- * - `skippedFileNoCoverage` / `skippedMethodsNoCoverage` — CRAP counters.
731
- */
732
- function scoreFileCombinedSerial(
733
- { abs, relPath, requireCoverage, coverageAvailable = true },
734
- coverage,
735
- ) {
736
- const entry = findCoverageEntry(coverage, relPath);
737
- const prepared = prepareSourceForScoring(abs);
738
- if (prepared.error) {
739
- return {
740
- relPath,
741
- miScore: prepared.error === 'read' ? null : 0,
742
- skippedFileNoCoverage: false,
743
- crapRows: null,
744
- skippedMethodsNoCoverage: 0,
745
- hasCoverageEntry: entry !== null,
746
- resolvedMethods: 0,
747
- totalMethods: 0,
748
- };
749
- }
750
- const {
751
- miScore,
752
- crapRows: rawCrapRows,
753
- parseError,
754
- } = analyzeOnce(prepared.code, entry, prepared.mapLine);
755
- if (parseError) {
756
- return {
757
- relPath,
758
- miScore: 0,
759
- skippedFileNoCoverage: false,
760
- crapRows: null,
761
- skippedMethodsNoCoverage: 0,
762
- hasCoverageEntry: entry !== null,
763
- resolvedMethods: 0,
764
- totalMethods: 0,
765
- };
766
- }
767
- if (requireCoverage && entry === null) {
768
- return {
769
- relPath,
770
- miScore,
771
- skippedFileNoCoverage: true,
772
- crapRows: [],
773
- skippedMethodsNoCoverage: 0,
774
- hasCoverageEntry: false,
775
- resolvedMethods: 0,
776
- totalMethods: 0,
777
- };
778
- }
779
- const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
780
- finalizeMethodRows(rawCrapRows, {
781
- requireCoverage,
782
- coverageAvailable,
783
- });
784
- return {
785
- relPath,
786
- miScore,
787
- skippedFileNoCoverage: false,
788
- crapRows: rows,
789
- skippedMethodsNoCoverage,
790
- hasCoverageEntry: entry !== null,
791
- resolvedMethods,
792
- totalMethods,
793
- };
794
- }
795
-
796
- async function scoreFilesCombinedViaPool(queue, coverage) {
797
- const enrichedQueue = queue.map((item) => ({
798
- ...item,
799
- coverageEntry: findCoverageEntry(coverage, item.relPath),
800
- }));
801
- const results = await runOnPool(COMBINED_MI_CRAP_WORKER_URL, enrichedQueue, {
802
- workerData: {},
803
- });
804
- return results.map((r, i) => {
805
- const item = queue[i];
806
- if (!r || r.__cpuPoolError) {
807
- Logger.warn(
808
- `[crap-utils] combined worker pool error for ${item.relPath}: ${r?.message ?? 'unknown'}`,
809
- );
810
- return { item, result: null };
811
- }
812
- return { item, result: r };
813
- });
814
- }
815
-
816
- /**
817
- * Combined MI + CRAP single-pass scan. Walks the shared `targetDirs` once (or
818
- * reuses `preScannedFiles`), dispatches every file through ONE worker that
819
- * calls `analyzeOnce` a single time, and returns BOTH the maintainability
820
- * score map and the CRAP scan result.
821
- *
822
- * This collapses the two independent escomplex passes the full-tree baseline
823
- * regenerator used to run (`calculateAll` → maintainability worker, then
824
- * `scanAndScore` → CRAP worker) into one parse per file. The outputs are
825
- * shaped to be drop-in equivalents of the two passes they replace, so the
826
- * downstream envelope projection + writer logic stays byte-identical:
827
- *
828
- * - `miScores` — `Record<relPath, number>` keyed exactly as `calculateAll`
829
- * keys its result (`path.relative(cwd, abs)`, POSIX-normalised), with
830
- * read-failure files (`miScore === null`) dropped. Parity target:
831
- * `calculateAll(files)`.
832
- * - `crap` — `{ rows, scannedFiles, skippedFilesNoCoverage,
833
- * skippedMethodsNoCoverage }`, identical in shape and content to
834
- * `scanAndScore({ targetDirs, coverage, ... })`. The rows are
835
- * CRAP-sorted (file → startLine → method) so the result matches
836
- * `scanAndScore` even before the writer re-sorts.
837
- *
838
- * The `requireCoverage`, `scopeFiles`, `ignoreGlobs`, and `preScannedFiles`
839
- * semantics match `scanAndScore` exactly — coverage gating, scope filtering,
840
- * and the single-walk reuse path behave the same. Files dropped from CRAP by
841
- * the coverage gate STILL contribute their MI score (the MI pass never
842
- * required coverage), preserving the two-pass behaviour where MI scores every
843
- * file in the target dirs.
844
- *
845
- * @param {{
846
- * targetDirs: string[],
847
- * coverage: object|null,
848
- * requireCoverage?: boolean,
849
- * cwd?: string,
850
- * scopeFiles?: Set<string>|string[]|null,
851
- * ignoreGlobs?: string[],
852
- * preScannedFiles?: string[]|null,
853
- * }} params
854
- * @returns {Promise<{
855
- * miScores: Record<string, number>,
856
- * crap: {
857
- * rows: Array<{
858
- * file: string, method: string, startLine: number,
859
- * cyclomatic: number, coverage: number, crap: number,
860
- * }>,
861
- * scannedFiles: number,
862
- * skippedFilesNoCoverage: number,
863
- * skippedMethodsNoCoverage: number,
864
- * },
865
- * }>}
866
- */
867
- export async function scanAndScoreCombined({
868
- targetDirs,
869
- coverage,
870
- requireCoverage = true,
871
- cwd = process.cwd(),
872
- scopeFiles = null,
873
- ignoreGlobs = [],
874
- preScannedFiles = null,
875
- }) {
876
- if (!Array.isArray(targetDirs)) {
877
- throw new TypeError('scanAndScoreCombined: targetDirs must be an array');
878
- }
879
- const scopeSet =
880
- scopeFiles == null
881
- ? null
882
- : scopeFiles instanceof Set
883
- ? scopeFiles
884
- : new Set(scopeFiles);
885
-
886
- // Single directory walk (or reuse the caller's pre-walked list), mirroring
887
- // scanAndScore so the file discovery is byte-identical between paths.
888
- const files = preScannedFiles != null ? [...preScannedFiles] : [];
889
- if (preScannedFiles == null) {
890
- for (const dir of targetDirs) {
891
- const abs = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
892
- scanDirectory(abs, files, { cwd, ignoreGlobs });
893
- }
894
- }
895
- files.sort();
896
-
897
- // Build the work queue. Each item carries both the canonicalised relPath
898
- // (CRAP's key + scope filter, matching scanAndScore) and the raw relPath
899
- // (MI's key, matching calculateAll's `path.relative(cwd, p)` shape).
900
- const coverageAvailable = isCoverageArtifactPresent(coverage);
901
- const queue = [];
902
- for (const abs of files) {
903
- const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
904
- const relPath = canonicalisePath(rawRel);
905
- if (scopeSet && !scopeSet.has(relPath)) continue;
906
- queue.push({
907
- abs,
908
- relPath,
909
- miRel: rawRel,
910
- requireCoverage,
911
- coverageAvailable,
912
- });
913
- }
914
- const scannedFiles = queue.length;
915
-
916
- const perFile =
917
- queue.length < SERIAL_THRESHOLD
918
- ? queue.map((item) => ({
919
- item,
920
- result: scoreFileCombinedSerial(item, coverage),
921
- }))
922
- : await scoreFilesCombinedViaPool(queue, coverage);
923
-
924
- // MI assembly — mirror calculateAll: drop read-failure files (miScore
925
- // null), key by the raw relative path, then sort ascending so the returned
926
- // object is insertion-order-stable.
927
- const miEntries = [];
928
- // CRAP assembly — mirror scanAndScore: file-level skip counter, drop
929
- // read/transpile/parse failures, accumulate method rows.
930
- const crapRows = [];
931
- let skippedFilesNoCoverage = 0;
932
- let skippedMethodsNoCoverage = 0;
933
- const resolution = newResolutionAccumulator();
934
-
935
- for (const { item, result } of perFile) {
936
- if (!result) continue; // unrecoverable per-file failure: drop silently
937
-
938
- // MI side.
939
- if (result.miScore !== null) {
940
- miEntries.push({ relPath: item.miRel, score: result.miScore });
941
- }
942
-
943
- // CRAP side.
944
- if (result.skippedFileNoCoverage) {
945
- skippedFilesNoCoverage += 1;
946
- continue;
947
- }
948
- if (result.crapRows === null) {
949
- if (result.error) {
950
- Logger.warn(
951
- `[crap-utils] failed to score ${item.relPath}: ${result.error}`,
952
- );
953
- }
954
- continue;
955
- }
956
- skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
957
- accumulateResolution(resolution, item.relPath, result);
958
- for (const mr of result.crapRows) {
959
- crapRows.push({
960
- file: item.relPath,
961
- method: mr.method,
962
- // Story #4969: `method` may be a derived anonymous identity; the flag
963
- // is what lets the persisted row say so.
964
- anonymous: mr.anonymous === true,
965
- startLine: mr.startLine,
966
- cyclomatic: mr.cyclomatic,
967
- coverage: mr.coverage,
968
- crap: mr.crap,
969
- coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
970
- });
971
- }
972
- }
973
-
974
- miEntries.sort((a, b) =>
975
- a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0,
976
- );
977
- const miScores = {};
978
- for (const { relPath, score } of miEntries) {
979
- miScores[relPath] = score;
980
- }
981
-
982
- crapRows.sort((a, b) => {
983
- if (a.file !== b.file) return a.file < b.file ? -1 : 1;
984
- if (a.startLine !== b.startLine) return a.startLine - b.startLine;
985
- if (a.method !== b.method) return a.method < b.method ? -1 : 1;
986
- return 0;
987
- });
988
-
989
- return {
990
- miScores,
991
- crap: {
992
- rows: crapRows,
993
- scannedFiles,
994
- skippedFilesNoCoverage,
995
- skippedMethodsNoCoverage,
996
- resolution: summarizeResolution(resolution),
997
- },
998
- };
999
- }