mandrel 2.28.0 → 2.30.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.
- package/.agents/docs/configuration.md +3 -0
- package/.agents/schemas/agentrc.schema.json +14 -0
- package/.agents/scripts/coverage-capture.js +31 -62
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +41 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +135 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +13 -0
- package/.agents/scripts/lib/baselines/preview-gates.js +7 -87
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +26 -0
- package/.agents/scripts/lib/config/gates/crap.schema.js +2 -0
- package/.agents/scripts/lib/config/quality.js +41 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +6 -0
- package/.agents/scripts/lib/coverage-capture-fullscope.js +105 -0
- package/.agents/scripts/lib/coverage-capture-incremental.js +116 -0
- package/.agents/scripts/lib/coverage-capture.js +101 -17
- package/.agents/scripts/lib/crap-baseline-index.js +46 -0
- package/.agents/scripts/lib/crap-baseline-join.js +153 -0
- package/.agents/scripts/lib/crap-coordinates.js +43 -0
- package/.agents/scripts/lib/crap-engine.js +26 -51
- package/.agents/scripts/lib/crap-utils-incremental.js +113 -0
- package/.agents/scripts/lib/crap-utils.js +118 -13
- package/docs/CHANGELOG.md +14 -0
- package/package.json +1 -1
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import escomplex from 'typhonjs-escomplex';
|
|
2
2
|
import { coverageForMethodInEntry } from './coverage-utils.js';
|
|
3
|
+
// `finalizeMethodRowsWithBaseline` (Story #4981) lives in
|
|
4
|
+
// crap-baseline-join.js — `resolveRawRow`, the per-row policy it shares with
|
|
5
|
+
// `finalizeMethodRows` below, is imported from there so the two stay a
|
|
6
|
+
// single implementation.
|
|
7
|
+
import { resolveRawRow } from './crap-baseline-join.js';
|
|
8
|
+
// `COORDINATE_ORIGINAL` / `COORDINATE_TRANSPILED` / `crapFormula` (Story
|
|
9
|
+
// #4866 / this module's original home) now live in crap-coordinates.js —
|
|
10
|
+
// re-exported here so every existing importer of this module is unaffected.
|
|
11
|
+
import {
|
|
12
|
+
COORDINATE_ORIGINAL,
|
|
13
|
+
COORDINATE_TRANSPILED,
|
|
14
|
+
crapFormula,
|
|
15
|
+
} from './crap-coordinates.js';
|
|
3
16
|
import { deriveMethodIdentities } from './crap-method-identity.js';
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
* The two line coordinate systems a CRAP row's `startLine` can be expressed
|
|
7
|
-
* in (Story #4866).
|
|
8
|
-
*
|
|
9
|
-
* `original` — the coordinates of the file a reader can open, and the ones
|
|
10
|
-
* istanbul's `fnMap` is keyed against. A JavaScript source is already in this
|
|
11
|
-
* system; a TS/TSX source reaches it only through a successful sourcemap
|
|
12
|
-
* lookup.
|
|
13
|
-
*
|
|
14
|
-
* `transpiled` — escomplex's own coordinates over the emitted JavaScript,
|
|
15
|
-
* kept only when the sourcemap has no entry originating on the method's
|
|
16
|
-
* generated line. Such a row is NOT an original-source coordinate and must
|
|
17
|
-
* never be presented as one: it cannot be joined to coverage, and it cannot
|
|
18
|
-
* be compared against a baseline row carrying the other provenance.
|
|
19
|
-
*/
|
|
20
|
-
export const COORDINATE_ORIGINAL = 'original';
|
|
21
|
-
export const COORDINATE_TRANSPILED = 'transpiled';
|
|
18
|
+
export { COORDINATE_ORIGINAL, COORDINATE_TRANSPILED, crapFormula };
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
21
|
* Derive the raw per-method CRAP rows from an escomplex report.
|
|
@@ -170,33 +167,25 @@ export function finalizeMethodRows(
|
|
|
170
167
|
let totalMethods = 0;
|
|
171
168
|
for (const mr of rawRows ?? []) {
|
|
172
169
|
totalMethods += 1;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) {
|
|
170
|
+
const resolution = resolveRawRow(mr, {
|
|
171
|
+
requireCoverage,
|
|
172
|
+
coverageAvailable,
|
|
173
|
+
});
|
|
174
|
+
if (resolution.resolved) resolvedMethods += 1;
|
|
175
|
+
if (resolution.row === null) {
|
|
180
176
|
skippedMethodsNoCoverage += 1;
|
|
181
177
|
continue;
|
|
182
178
|
}
|
|
183
|
-
|
|
184
|
-
const crap = unresolved ? crapFormula(mr.cyclomatic, 0) : mr.crap;
|
|
185
|
-
// Everything the scan decided is carried forward; this step overrides only
|
|
186
|
-
// what its own policy resolves. Spreading rather than re-listing each field
|
|
187
|
-
// is why the row's identity marker (Story #4969) and its provenance
|
|
188
|
-
// (Story #4866) survive the step without a line each to remember them —
|
|
189
|
-
// a hand-rebuilt row is how a marker silently stops reaching the baseline.
|
|
190
|
-
rows.push({
|
|
191
|
-
...mr,
|
|
192
|
-
coverage,
|
|
193
|
-
crap,
|
|
194
|
-
coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
|
|
195
|
-
});
|
|
179
|
+
rows.push(resolution.row);
|
|
196
180
|
}
|
|
197
181
|
return { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods };
|
|
198
182
|
}
|
|
199
183
|
|
|
184
|
+
// The incremental-mode join (Story #4981) lives in crap-baseline-join.js;
|
|
185
|
+
// re-exported here so it stays reachable from the scoring kernel's existing
|
|
186
|
+
// public surface.
|
|
187
|
+
export { finalizeMethodRowsWithBaseline } from './crap-baseline-join.js';
|
|
188
|
+
|
|
200
189
|
/**
|
|
201
190
|
* Score each method in a JavaScript source for Change Risk Anti-Patterns
|
|
202
191
|
* (CRAP): `c² · (1 − cov)³ + c`, where `c` is cyclomatic complexity and `cov`
|
|
@@ -243,20 +232,6 @@ export function calculateCrapForSource(
|
|
|
243
232
|
return methodRowsFromReport(report, coverageForFile, mapLine);
|
|
244
233
|
}
|
|
245
234
|
|
|
246
|
-
/**
|
|
247
|
-
* CRAP formula, exported for callers that need to derive target scores or
|
|
248
|
-
* `fixGuidance` values without re-scoring source.
|
|
249
|
-
*
|
|
250
|
-
* @param {number} cyclomatic
|
|
251
|
-
* @param {number} coverage In [0, 1].
|
|
252
|
-
* @returns {number}
|
|
253
|
-
*/
|
|
254
|
-
export function crapFormula(cyclomatic, coverage) {
|
|
255
|
-
const c = Number(cyclomatic) || 0;
|
|
256
|
-
const cov = Math.max(0, Math.min(1, Number(coverage) || 0));
|
|
257
|
-
return c * c * (1 - cov) ** 3 + c;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
235
|
/**
|
|
261
236
|
* Derive the deterministic single-axis fixes that would bring a method at
|
|
262
237
|
* cyclomatic complexity `c` at or under the `target` CRAP score.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crap-utils-incremental.js — small pure helpers that wire the incremental
|
|
3
|
+
* CRAP join (Story #4981) into `crap-utils.js#scanAndScore`'s per-file work
|
|
4
|
+
* queue. Split into their own file so the wiring lands as new code rather
|
|
5
|
+
* than a same-file expansion of `scanAndScore` / `scoreFileSerial`.
|
|
6
|
+
*/
|
|
7
|
+
import { indexBaselineRowsByFile } from './crap-baseline-index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `scanAndScore`'s `incremental` option into the two lookup
|
|
11
|
+
* structures the per-file queue build needs. Both are `null` when
|
|
12
|
+
* `incremental` is absent (full-scope, the default) — every downstream
|
|
13
|
+
* consumer treats a `null` context as "not incremental".
|
|
14
|
+
*
|
|
15
|
+
* @param {{ touchedFiles?: Set<string>|string[], baselineRows?: Array<object> } | null} incremental
|
|
16
|
+
* @returns {{ touchedFiles: Set<string>|null, baselineByFile: Map<string, Map<string, object>>|null }}
|
|
17
|
+
*/
|
|
18
|
+
export function resolveIncrementalContext(incremental) {
|
|
19
|
+
const touchedFiles = incremental?.touchedFiles
|
|
20
|
+
? incremental.touchedFiles instanceof Set
|
|
21
|
+
? incremental.touchedFiles
|
|
22
|
+
: new Set(incremental.touchedFiles)
|
|
23
|
+
: null;
|
|
24
|
+
const baselineByFile = incremental
|
|
25
|
+
? indexBaselineRowsByFile(incremental.baselineRows)
|
|
26
|
+
: null;
|
|
27
|
+
return { touchedFiles, baselineByFile };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge one queued file's `touched` flag and per-file `baselineByKey` map
|
|
32
|
+
* (resolved from the `resolveIncrementalContext` output) onto its base queue
|
|
33
|
+
* item. `touched` defaults to `true` (every file is "touched" outside
|
|
34
|
+
* incremental mode, matching
|
|
35
|
+
* `crap-baseline-join.js#finalizeMethodRowsWithBaseline`'s own default).
|
|
36
|
+
*
|
|
37
|
+
* @param {object} item Base queue item (`{ abs, relPath, requireCoverage, coverageAvailable }`).
|
|
38
|
+
* @param {{ touchedFiles: Set<string>|null, baselineByFile: Map<string, Map<string, object>>|null }} ctx
|
|
39
|
+
* @returns {object} `item` plus `{ touched, baselineByKey }`.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveQueueIncrementalFields(
|
|
42
|
+
item,
|
|
43
|
+
{ touchedFiles, baselineByFile },
|
|
44
|
+
) {
|
|
45
|
+
const touched = touchedFiles ? touchedFiles.has(item.relPath) : true;
|
|
46
|
+
const baselineByKey = baselineByFile
|
|
47
|
+
? (baselineByFile.get(item.relPath) ?? new Map())
|
|
48
|
+
: null;
|
|
49
|
+
return { ...item, touched, baselineByKey };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* True when `scoreFileSerial` should resolve a file's methods from the
|
|
54
|
+
* baseline rather than requiring a fresh coverage entry — an untouched file
|
|
55
|
+
* with at least one indexed baseline row.
|
|
56
|
+
*
|
|
57
|
+
* @param {boolean} touched
|
|
58
|
+
* @param {Map<string, object>|null} baselineByKey
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isIncrementalJoinActive(touched, baselineByKey) {
|
|
62
|
+
return !touched && baselineByKey != null && baselineByKey.size > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `scoreFileSerial`'s file-level skip decision, factored out whole so the
|
|
67
|
+
* incremental exception lives with the rest of this Story's branching
|
|
68
|
+
* rather than inflating the cyclomatic complexity of the pre-#4981 caller.
|
|
69
|
+
*
|
|
70
|
+
* @param {boolean} requireCoverage
|
|
71
|
+
* @param {object|null} entry Istanbul coverage entry for this file.
|
|
72
|
+
* @param {boolean} touched
|
|
73
|
+
* @param {Map<string, object>|null} baselineByKey
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function shouldSkipFileForNoCoverage(
|
|
77
|
+
requireCoverage,
|
|
78
|
+
entry,
|
|
79
|
+
touched,
|
|
80
|
+
baselineByKey,
|
|
81
|
+
) {
|
|
82
|
+
return (
|
|
83
|
+
requireCoverage &&
|
|
84
|
+
entry === null &&
|
|
85
|
+
!isIncrementalJoinActive(touched, baselineByKey)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `scanAndScore`'s serial-vs-pool routing decision. Incremental mode always
|
|
91
|
+
* routes serial (see `isIncrementalJoinActive`'s doc for why the baseline
|
|
92
|
+
* lookup Maps don't cross the worker boundary); otherwise unchanged from the
|
|
93
|
+
* pre-#4981 queue-length cutover.
|
|
94
|
+
*
|
|
95
|
+
* @param {number} queueLength
|
|
96
|
+
* @param {unknown} incremental
|
|
97
|
+
* @param {number} serialThreshold
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
export function shouldRunSerial(queueLength, incremental, serialThreshold) {
|
|
101
|
+
return queueLength < serialThreshold || Boolean(incremental);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Project the `resolvedFromBaseline` marker onto a `scanAndScore` result row
|
|
106
|
+
* — present only when true, so a full-scope scan's rows are unaffected.
|
|
107
|
+
*
|
|
108
|
+
* @param {{ resolvedFromBaseline?: boolean }} mr
|
|
109
|
+
* @returns {{ resolvedFromBaseline: true } | {}}
|
|
110
|
+
*/
|
|
111
|
+
export function resolvedFromBaselineFlag(mr) {
|
|
112
|
+
return mr.resolvedFromBaseline === true ? { resolvedFromBaseline: true } : {};
|
|
113
|
+
}
|
|
@@ -4,11 +4,19 @@ 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';
|
|
7
8
|
import {
|
|
8
9
|
COORDINATE_ORIGINAL,
|
|
9
10
|
finalizeMethodRows,
|
|
10
11
|
methodRowsFromReport,
|
|
11
12
|
} from './crap-engine.js';
|
|
13
|
+
import {
|
|
14
|
+
resolvedFromBaselineFlag,
|
|
15
|
+
resolveIncrementalContext,
|
|
16
|
+
resolveQueueIncrementalFields,
|
|
17
|
+
shouldRunSerial,
|
|
18
|
+
shouldSkipFileForNoCoverage,
|
|
19
|
+
} from './crap-utils-incremental.js';
|
|
12
20
|
import { Logger } from './Logger.js';
|
|
13
21
|
import { scanDirectory } from './maintainability-utils.js';
|
|
14
22
|
import {
|
|
@@ -98,6 +106,73 @@ function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
|
|
|
98
106
|
* rows: Array<{file: string, method: string, startLine: number, crap: number}>,
|
|
99
107
|
* }|null}
|
|
100
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
|
+
|
|
101
176
|
/**
|
|
102
177
|
* Story #1895: shipped baseline switched to the canonical envelope shape
|
|
103
178
|
* (`$schema`, `kernelVersion`, `generatedAt`, `rollup`, `rows` keyed on
|
|
@@ -114,6 +189,16 @@ function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
|
|
|
114
189
|
* They are carried verbatim now, `null` when the envelope never stamped them,
|
|
115
190
|
* so an axis can tell "written by a different transpiler" apart from "written
|
|
116
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.
|
|
117
202
|
*/
|
|
118
203
|
function projectCrapEnvelopeToLegacy(parsed) {
|
|
119
204
|
if (
|
|
@@ -126,11 +211,7 @@ function projectCrapEnvelopeToLegacy(parsed) {
|
|
|
126
211
|
return {
|
|
127
212
|
kernelVersion: parsed.kernelVersion,
|
|
128
213
|
escomplexVersion: resolveEscomplexVersion(),
|
|
129
|
-
|
|
130
|
-
typeof parsed.tsTranspilerVersion === 'string'
|
|
131
|
-
? parsed.tsTranspilerVersion
|
|
132
|
-
: null,
|
|
133
|
-
scoringSemantics: parsed.scoringSemantics ?? null,
|
|
214
|
+
...projectCompatStamps(parsed),
|
|
134
215
|
rows: parsed.rows.map((row) => ({
|
|
135
216
|
crap: row.crap,
|
|
136
217
|
file: row.path,
|
|
@@ -409,6 +490,10 @@ export function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
|
409
490
|
* `regenerateMainFromTree`) SHOULD pass the MI scan's file list here so the
|
|
410
491
|
* tree is walked only once per run.
|
|
411
492
|
*
|
|
493
|
+
* `incremental` (Story #4981) resolves an untouched file's methods from
|
|
494
|
+
* `crap-baseline-join.js#finalizeMethodRowsWithBaseline` instead of
|
|
495
|
+
* requiring fresh coverage; omitted (the default), behaviour is unchanged.
|
|
496
|
+
*
|
|
412
497
|
* @param {{
|
|
413
498
|
* targetDirs: string[],
|
|
414
499
|
* coverage: object|null,
|
|
@@ -416,6 +501,7 @@ export function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
|
416
501
|
* cwd?: string,
|
|
417
502
|
* scopeFiles?: Set<string>|string[]|null,
|
|
418
503
|
* preScannedFiles?: string[]|null,
|
|
504
|
+
* incremental?: { touchedFiles: Set<string>|string[], baselineRows: Array<object> } | null,
|
|
419
505
|
* }} params
|
|
420
506
|
* @returns {{
|
|
421
507
|
* rows: Array<{
|
|
@@ -439,6 +525,7 @@ export async function scanAndScore({
|
|
|
439
525
|
scopeFiles = null,
|
|
440
526
|
ignoreGlobs = [],
|
|
441
527
|
preScannedFiles = null,
|
|
528
|
+
incremental = null,
|
|
442
529
|
}) {
|
|
443
530
|
if (!Array.isArray(targetDirs)) {
|
|
444
531
|
throw new TypeError('scanAndScore: targetDirs must be an array');
|
|
@@ -460,6 +547,8 @@ export async function scanAndScore({
|
|
|
460
547
|
}
|
|
461
548
|
files.sort();
|
|
462
549
|
|
|
550
|
+
const incrementalCtx = resolveIncrementalContext(incremental);
|
|
551
|
+
|
|
463
552
|
// Build the work-queue first so scopeFile filtering happens before
|
|
464
553
|
// any I/O / IPC. `scannedFiles` is the in-scope count.
|
|
465
554
|
// Story #2079: route every relPath through path-canon so a scan from
|
|
@@ -472,14 +561,18 @@ export async function scanAndScore({
|
|
|
472
561
|
const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
473
562
|
const relPath = canonicalisePath(rawRel);
|
|
474
563
|
if (scopeSet && !scopeSet.has(relPath)) continue;
|
|
475
|
-
queue.push(
|
|
564
|
+
queue.push(
|
|
565
|
+
resolveQueueIncrementalFields(
|
|
566
|
+
{ abs, relPath, requireCoverage, coverageAvailable },
|
|
567
|
+
incrementalCtx,
|
|
568
|
+
),
|
|
569
|
+
);
|
|
476
570
|
}
|
|
477
571
|
const scannedFiles = queue.length;
|
|
478
572
|
|
|
479
|
-
const perFile =
|
|
480
|
-
queue.
|
|
481
|
-
|
|
482
|
-
: await scoreFilesViaPool(queue, coverage);
|
|
573
|
+
const perFile = shouldRunSerial(queue.length, incremental, SERIAL_THRESHOLD)
|
|
574
|
+
? queue.map((item) => ({ item, result: scoreFileSerial(item, coverage) }))
|
|
575
|
+
: await scoreFilesViaPool(queue, coverage);
|
|
483
576
|
|
|
484
577
|
const rows = [];
|
|
485
578
|
let skippedFilesNoCoverage = 0;
|
|
@@ -516,6 +609,7 @@ export async function scanAndScore({
|
|
|
516
609
|
coverage: mr.coverage,
|
|
517
610
|
crap: mr.crap,
|
|
518
611
|
coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
|
|
612
|
+
...resolvedFromBaselineFlag(mr),
|
|
519
613
|
});
|
|
520
614
|
}
|
|
521
615
|
}
|
|
@@ -545,11 +639,20 @@ export async function scanAndScore({
|
|
|
545
639
|
* CRAP rows and the MI score are derived from the same escomplex report.
|
|
546
640
|
*/
|
|
547
641
|
function scoreFileSerial(
|
|
548
|
-
{
|
|
642
|
+
{
|
|
643
|
+
abs,
|
|
644
|
+
relPath,
|
|
645
|
+
requireCoverage,
|
|
646
|
+
coverageAvailable = true,
|
|
647
|
+
touched = true,
|
|
648
|
+
baselineByKey = null,
|
|
649
|
+
},
|
|
549
650
|
coverage,
|
|
550
651
|
) {
|
|
551
652
|
const entry = findCoverageEntry(coverage, relPath);
|
|
552
|
-
if (
|
|
653
|
+
if (
|
|
654
|
+
shouldSkipFileForNoCoverage(requireCoverage, entry, touched, baselineByKey)
|
|
655
|
+
) {
|
|
553
656
|
return {
|
|
554
657
|
skippedFileNoCoverage: true,
|
|
555
658
|
rows: [],
|
|
@@ -575,9 +678,11 @@ function scoreFileSerial(
|
|
|
575
678
|
prepared.mapLine,
|
|
576
679
|
);
|
|
577
680
|
if (parseError) return dropped;
|
|
578
|
-
const finalized =
|
|
681
|
+
const finalized = finalizeMethodRowsWithBaseline(crapRows, {
|
|
579
682
|
requireCoverage,
|
|
580
683
|
coverageAvailable,
|
|
684
|
+
touched,
|
|
685
|
+
baselineByKey,
|
|
581
686
|
});
|
|
582
687
|
return {
|
|
583
688
|
skippedFileNoCoverage: false,
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,20 @@ All notable changes to this project will be documented in this file.
|
|
|
15
15
|
-->
|
|
16
16
|
<!-- markdownlint-disable-file MD004 MD012 MD037 -->
|
|
17
17
|
|
|
18
|
+
## [2.30.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.29.0...mandrel-v2.30.0) (2026-08-04)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
* **baselines:** carry provenanceStamped through the legacy CRAP projection ([#4986](https://github.com/dsj1984/mandrel/issues/4986)) ([#4987](https://github.com/dsj1984/mandrel/issues/4987)) ([921b521](https://github.com/dsj1984/mandrel/commit/921b521f2094c961fec84acca1f90e068cb77798))
|
|
24
|
+
|
|
25
|
+
## [2.29.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.28.0...mandrel-v2.29.0) (2026-08-03)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
* incremental coverage-capture: scope the close-time coverage run to the Story diff and join unchanged files from the committed CRAP baseline ([#4981](https://github.com/dsj1984/mandrel/issues/4981)) ([#4982](https://github.com/dsj1984/mandrel/issues/4982)) ([3d73875](https://github.com/dsj1984/mandrel/commit/3d73875deb170edf69f6f480d5d1abfffebcb47d))
|
|
31
|
+
|
|
18
32
|
## [2.28.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.27.0...mandrel-v2.28.0) (2026-08-03)
|
|
19
33
|
|
|
20
34
|
|
package/package.json
CHANGED