mandrel 2.16.0 → 2.17.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 +1 -0
- package/.agents/docs/quality-gates.md +137 -0
- package/.agents/schemas/agentrc.schema.json +6 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +52 -12
- package/.agents/scripts/audit-to-stories.js +92 -25
- package/.agents/scripts/boot-sweep.js +28 -6
- package/.agents/scripts/check-baseline-drift.js +138 -0
- package/.agents/scripts/coverage-capture.js +74 -25
- package/.agents/scripts/deliver-recover.js +45 -18
- package/.agents/scripts/drain-pending-cleanup.js +67 -23
- package/.agents/scripts/generate-lens-checklists.js +81 -30
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +88 -17
- package/.agents/scripts/lib/baselines/drift-detector.js +351 -0
- package/.agents/scripts/lib/baselines/envelope.js +7 -0
- package/.agents/scripts/lib/baselines/kernel.js +31 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +76 -0
- package/.agents/scripts/lib/baselines/reader.js +12 -1
- package/.agents/scripts/lib/baselines/refresh-service.js +7 -1
- package/.agents/scripts/lib/baselines/writer.js +10 -0
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -8
- package/.agents/scripts/lib/cli-utils.js +48 -13
- package/.agents/scripts/lib/close-validation/projections/advisories.js +184 -0
- package/.agents/scripts/lib/close-validation/projections/crap.js +303 -0
- package/.agents/scripts/lib/close-validation/runner.js +68 -0
- package/.agents/scripts/lib/config/gates/crap.schema.js +7 -0
- package/.agents/scripts/lib/config/quality.js +40 -0
- package/.agents/scripts/lib/coverage-utils.js +92 -9
- package/.agents/scripts/lib/crap-engine.js +113 -23
- package/.agents/scripts/lib/crap-utils.js +159 -93
- package/.agents/scripts/lib/dynamic-workflow/audit-orchestrator.js +97 -10
- package/.agents/scripts/lib/dynamic-workflow/degraded-coverage.js +81 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +15 -8
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +35 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +13 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +16 -1
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +122 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +14 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal-schema.js +166 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +21 -50
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +26 -12
- package/.agents/scripts/lib/stdio-flush.js +71 -0
- package/.agents/scripts/lib/transpile.js +133 -6
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +47 -101
- package/.agents/scripts/lib/workers/crap-worker.js +49 -76
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +81 -8
- package/.agents/scripts/nav-registry-diff.js +30 -8
- package/.agents/scripts/plan-run-epilogue.js +27 -11
- package/.agents/scripts/resolve-doc-tiers.js +18 -8
- package/.agents/scripts/single-story-close.js +9 -92
- package/.agents/scripts/update-crap-baseline.js +13 -0
- package/README.md +14 -6
- package/docs/CHANGELOG.md +24 -0
- package/lib/cli/version-helpers.js +7 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +15 -8
- package/package.json +5 -1
|
@@ -1,6 +1,108 @@
|
|
|
1
1
|
import escomplex from 'typhonjs-escomplex';
|
|
2
2
|
import { coverageForMethodInEntry } from './coverage-utils.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Derive the raw per-method CRAP rows from an escomplex report.
|
|
6
|
+
*
|
|
7
|
+
* Single-sourced between `calculateCrapForSource` (CRAP-only path) and
|
|
8
|
+
* `analyzeOnce` (combined MI + CRAP path) so the two cannot drift on how a
|
|
9
|
+
* method's line is remapped or its coverage joined — the parity the
|
|
10
|
+
* combined-parity suite asserts.
|
|
11
|
+
*
|
|
12
|
+
* **Coordinates (Story #4775).** `mapLine` translates escomplex's
|
|
13
|
+
* `lineStart` — which is in *transpiled* coordinates for a TS/TSX source —
|
|
14
|
+
* into the *original source* coordinates istanbul's `fnMap` uses. Without it
|
|
15
|
+
* the join compares two different coordinate systems and either misses or,
|
|
16
|
+
* worse, collides with an unrelated function. A `null` mapper means the two
|
|
17
|
+
* coordinate systems already coincide (plain JavaScript), and a line the map
|
|
18
|
+
* cannot resolve falls back to the un-remapped value rather than dropping the
|
|
19
|
+
* method outright. The remapped line is also what the row reports, so a
|
|
20
|
+
* persisted row points at a line the reader can actually open.
|
|
21
|
+
*
|
|
22
|
+
* @param {object|null} report An `escomplex.analyzeModule` report.
|
|
23
|
+
* @param {object|null} coverageForFile Istanbul coverage entry for this file.
|
|
24
|
+
* @param {((line: number) => number|null)|null} [mapLine]
|
|
25
|
+
* @returns {Array<{
|
|
26
|
+
* method: string,
|
|
27
|
+
* startLine: number,
|
|
28
|
+
* cyclomatic: number,
|
|
29
|
+
* coverage: number|null,
|
|
30
|
+
* crap: number|null,
|
|
31
|
+
* }>}
|
|
32
|
+
*/
|
|
33
|
+
export function methodRowsFromReport(report, coverageForFile, mapLine = null) {
|
|
34
|
+
const methods = report?.methods ?? [];
|
|
35
|
+
const rows = [];
|
|
36
|
+
for (const m of methods) {
|
|
37
|
+
const rawStartLine = m?.lineStart;
|
|
38
|
+
if (typeof rawStartLine !== 'number') continue;
|
|
39
|
+
const mapped = typeof mapLine === 'function' ? mapLine(rawStartLine) : null;
|
|
40
|
+
const startLine = typeof mapped === 'number' ? mapped : rawStartLine;
|
|
41
|
+
const cyclomatic = m?.cyclomatic ?? 0;
|
|
42
|
+
const coverage = coverageForFile
|
|
43
|
+
? coverageForMethodInEntry(coverageForFile, startLine)
|
|
44
|
+
: null;
|
|
45
|
+
const crap = coverage === null ? null : crapFormula(cyclomatic, coverage);
|
|
46
|
+
rows.push({ method: m.name, startLine, cyclomatic, coverage, crap });
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply the scanner's `requireCoverage` policy to raw method rows and report
|
|
53
|
+
* how much of the coverage join actually landed.
|
|
54
|
+
*
|
|
55
|
+
* Two policies, one honest each way (Story #4775, fix part 3):
|
|
56
|
+
*
|
|
57
|
+
* - `requireCoverage: true` — an unresolved method is skipped and counted,
|
|
58
|
+
* exactly as before. The baseline stays a record of measured code.
|
|
59
|
+
* - `requireCoverage: false` — an unresolved method scores as **0%
|
|
60
|
+
* covered** (`crap = c² + c`, the formula's own treatment of untested
|
|
61
|
+
* code) and lands in the baseline. Previously the flag only stopped
|
|
62
|
+
* whole *files* being skipped while each individual method was still
|
|
63
|
+
* dropped, which made it a no-op for baseline population — the caller
|
|
64
|
+
* asked for "score it anyway" and got silence.
|
|
65
|
+
*
|
|
66
|
+
* `resolvedMethods` / `totalMethods` count the *join*, not the fill: a
|
|
67
|
+
* method scored 0% because its coverage was unresolved counts as
|
|
68
|
+
* unresolved. That is what makes them usable as a health signal for the
|
|
69
|
+
* updater's fail-closed resolution-rate floor.
|
|
70
|
+
*
|
|
71
|
+
* @param {Array<object>} rawRows Rows from `methodRowsFromReport`.
|
|
72
|
+
* @param {{requireCoverage?: boolean}} [opts]
|
|
73
|
+
* @returns {{
|
|
74
|
+
* rows: Array<object>,
|
|
75
|
+
* skippedMethodsNoCoverage: number,
|
|
76
|
+
* resolvedMethods: number,
|
|
77
|
+
* totalMethods: number,
|
|
78
|
+
* }}
|
|
79
|
+
*/
|
|
80
|
+
export function finalizeMethodRows(rawRows, { requireCoverage = true } = {}) {
|
|
81
|
+
const rows = [];
|
|
82
|
+
let skippedMethodsNoCoverage = 0;
|
|
83
|
+
let resolvedMethods = 0;
|
|
84
|
+
let totalMethods = 0;
|
|
85
|
+
for (const mr of rawRows ?? []) {
|
|
86
|
+
totalMethods += 1;
|
|
87
|
+
const unresolved = mr.crap === null || mr.coverage === null;
|
|
88
|
+
if (!unresolved) resolvedMethods += 1;
|
|
89
|
+
if (unresolved && requireCoverage) {
|
|
90
|
+
skippedMethodsNoCoverage += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const coverage = unresolved ? 0 : mr.coverage;
|
|
94
|
+
const crap = unresolved ? crapFormula(mr.cyclomatic, 0) : mr.crap;
|
|
95
|
+
rows.push({
|
|
96
|
+
method: mr.method,
|
|
97
|
+
startLine: mr.startLine,
|
|
98
|
+
cyclomatic: mr.cyclomatic,
|
|
99
|
+
coverage,
|
|
100
|
+
crap,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods };
|
|
104
|
+
}
|
|
105
|
+
|
|
4
106
|
/**
|
|
5
107
|
* Score each method in a JavaScript source for Change Risk Anti-Patterns
|
|
6
108
|
* (CRAP): `c² · (1 − cov)³ + c`, where `c` is cyclomatic complexity and `cov`
|
|
@@ -11,15 +113,17 @@ import { coverageForMethodInEntry } from './coverage-utils.js';
|
|
|
11
113
|
* `analyzeModule`).
|
|
12
114
|
* - Methods whose coverage cannot be resolved from `coverageForFile`
|
|
13
115
|
* produce `coverage: null` and `crap: null`. Callers apply their own
|
|
14
|
-
* `requireCoverage` policy at the scanner level;
|
|
15
|
-
* decides to skip.
|
|
116
|
+
* `requireCoverage` policy at the scanner level (`finalizeMethodRows`);
|
|
117
|
+
* this kernel never decides to skip.
|
|
16
118
|
* - A parse error returns an empty array — the file is unscorable, not
|
|
17
119
|
* zero-complexity.
|
|
18
120
|
*
|
|
19
|
-
* @param {string} source JavaScript source text.
|
|
121
|
+
* @param {string} source JavaScript source text (possibly transpiled).
|
|
20
122
|
* @param {object|null} coverageForFile The inner value from a
|
|
21
123
|
* `coverage-final.json` map keyed by this file's path, or null when no
|
|
22
124
|
* coverage data is available for this file.
|
|
125
|
+
* @param {((line: number) => number|null)|null} [mapLine] Transpiled →
|
|
126
|
+
* original line resolver; see `methodRowsFromReport`.
|
|
23
127
|
* @returns {Array<{
|
|
24
128
|
* method: string,
|
|
25
129
|
* startLine: number,
|
|
@@ -28,32 +132,18 @@ import { coverageForMethodInEntry } from './coverage-utils.js';
|
|
|
28
132
|
* crap: number|null,
|
|
29
133
|
* }>}
|
|
30
134
|
*/
|
|
31
|
-
export function calculateCrapForSource(
|
|
135
|
+
export function calculateCrapForSource(
|
|
136
|
+
source,
|
|
137
|
+
coverageForFile,
|
|
138
|
+
mapLine = null,
|
|
139
|
+
) {
|
|
32
140
|
let report;
|
|
33
141
|
try {
|
|
34
142
|
report = escomplex.analyzeModule(source);
|
|
35
143
|
} catch {
|
|
36
144
|
return [];
|
|
37
145
|
}
|
|
38
|
-
|
|
39
|
-
const rows = [];
|
|
40
|
-
for (const m of methods) {
|
|
41
|
-
const startLine = m?.lineStart;
|
|
42
|
-
if (typeof startLine !== 'number') continue;
|
|
43
|
-
const cyclomatic = m?.cyclomatic ?? 0;
|
|
44
|
-
const coverage = coverageForFile
|
|
45
|
-
? coverageForMethodInEntry(coverageForFile, startLine)
|
|
46
|
-
: null;
|
|
47
|
-
const crap = coverage === null ? null : crapFormula(cyclomatic, coverage);
|
|
48
|
-
rows.push({
|
|
49
|
-
method: m.name,
|
|
50
|
-
startLine,
|
|
51
|
-
cyclomatic,
|
|
52
|
-
coverage,
|
|
53
|
-
crap,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return rows;
|
|
146
|
+
return methodRowsFromReport(report, coverageForFile, mapLine);
|
|
57
147
|
}
|
|
58
148
|
|
|
59
149
|
/**
|
|
@@ -2,15 +2,15 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import escomplex from 'typhonjs-escomplex';
|
|
4
4
|
import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
|
|
5
|
-
import {
|
|
6
|
-
coverageForMethodInEntry,
|
|
7
|
-
findCoverageEntry,
|
|
8
|
-
} from './coverage-utils.js';
|
|
5
|
+
import { findCoverageEntry } from './coverage-utils.js';
|
|
9
6
|
import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
|
|
10
|
-
import {
|
|
7
|
+
import { finalizeMethodRows, methodRowsFromReport } from './crap-engine.js';
|
|
11
8
|
import { Logger } from './Logger.js';
|
|
12
9
|
import { scanDirectory } from './maintainability-utils.js';
|
|
13
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
prepareSourceForScoring,
|
|
12
|
+
resolveTsTranspilerVersion,
|
|
13
|
+
} from './transpile.js';
|
|
14
14
|
|
|
15
15
|
const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
|
|
16
16
|
const COMBINED_MI_CRAP_WORKER_URL = new URL(
|
|
@@ -201,6 +201,103 @@ export function buildBaselineEnvelope({
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* How many files to name when reporting the worst unresolved offenders. Long
|
|
206
|
+
* enough to point at a pattern, short enough to stay a readable CLI message.
|
|
207
|
+
*/
|
|
208
|
+
const WORST_OFFENDER_LIMIT = 5;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Method-resolution telemetry (Story #4775, fix part 4).
|
|
212
|
+
*
|
|
213
|
+
* The updater used to persist a 100-row baseline built from 5023 dropped
|
|
214
|
+
* methods and log it as success — the rot that let a broken coverage join
|
|
215
|
+
* sit undetected for five weeks across three repos. These three helpers
|
|
216
|
+
* carry the counters that make a thin result *visible* and therefore
|
|
217
|
+
* refusable.
|
|
218
|
+
*
|
|
219
|
+
* The rate is deliberately measured over files that **do** have a coverage
|
|
220
|
+
* entry: a file the test run never touched has no join to fail, so counting
|
|
221
|
+
* it would dilute the signal the floor is meant to catch.
|
|
222
|
+
*/
|
|
223
|
+
function newResolutionAccumulator() {
|
|
224
|
+
return { resolved: 0, total: 0, byFile: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function accumulateResolution(acc, relPath, result) {
|
|
228
|
+
if (result?.hasCoverageEntry !== true) return;
|
|
229
|
+
const total = result.totalMethods ?? 0;
|
|
230
|
+
if (total === 0) return;
|
|
231
|
+
const resolved = result.resolvedMethods ?? 0;
|
|
232
|
+
acc.resolved += resolved;
|
|
233
|
+
acc.total += total;
|
|
234
|
+
if (resolved < total) {
|
|
235
|
+
acc.byFile.push({ file: relPath, unresolved: total - resolved, total });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function summarizeResolution(acc) {
|
|
240
|
+
const worstFiles = [...acc.byFile]
|
|
241
|
+
.sort((a, b) => b.unresolved - a.unresolved || a.file.localeCompare(b.file))
|
|
242
|
+
.slice(0, WORST_OFFENDER_LIMIT);
|
|
243
|
+
return {
|
|
244
|
+
resolvedMethods: acc.resolved,
|
|
245
|
+
joinableMethods: acc.total,
|
|
246
|
+
rate: acc.total === 0 ? 1 : acc.resolved / acc.total,
|
|
247
|
+
worstFiles,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Minimum number of joinable methods before the resolution-rate floor is
|
|
253
|
+
* enforced. A diff-scoped run can legitimately touch a handful of methods,
|
|
254
|
+
* where one unresolved method is a 50% rate and says nothing about the health
|
|
255
|
+
* of the join. Below this sample the rate is reported, never enforced.
|
|
256
|
+
*/
|
|
257
|
+
const MIN_RESOLUTION_SAMPLE = 25;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Fail-closed guard on the per-method coverage join (Story #4775, fix part 4).
|
|
261
|
+
*
|
|
262
|
+
* The updater used to persist a 100-row baseline distilled from 5023 dropped
|
|
263
|
+
* methods and log it as a success — which is exactly how a broken join stayed
|
|
264
|
+
* invisible for five weeks across three repositories. A thin result is now a
|
|
265
|
+
* refusal: the caller throws before anything is written, and the message names
|
|
266
|
+
* the rate, the counts, and the files carrying the most unresolved methods so
|
|
267
|
+
* the operator can tell "my tests do not cover that" apart from "the join is
|
|
268
|
+
* broken".
|
|
269
|
+
*
|
|
270
|
+
* Returns `null` when the run may proceed, or the operator-facing message when
|
|
271
|
+
* it must not.
|
|
272
|
+
*
|
|
273
|
+
* @param {{resolvedMethods: number, joinableMethods: number, rate: number,
|
|
274
|
+
* worstFiles: Array<{file: string, unresolved: number, total: number}>}
|
|
275
|
+
* | undefined} resolution
|
|
276
|
+
* @param {number} floor
|
|
277
|
+
* @returns {string|null}
|
|
278
|
+
*/
|
|
279
|
+
export function checkResolutionFloor(resolution, floor) {
|
|
280
|
+
if (!resolution) return null;
|
|
281
|
+
const { joinableMethods = 0, resolvedMethods = 0, rate = 1 } = resolution;
|
|
282
|
+
if (joinableMethods < MIN_RESOLUTION_SAMPLE) return null;
|
|
283
|
+
if (rate >= floor) return null;
|
|
284
|
+
const worst = (resolution.worstFiles ?? [])
|
|
285
|
+
.map((w) => ` - ${w.file} (${w.unresolved}/${w.total} unresolved)`)
|
|
286
|
+
.join('\n');
|
|
287
|
+
return (
|
|
288
|
+
`[CRAP] Refusing to persist: only ${resolvedMethods}/${joinableMethods} ` +
|
|
289
|
+
`method(s) (${(rate * 100).toFixed(1)}%) resolved a coverage entry in files ` +
|
|
290
|
+
`that HAVE coverage — below the ${(floor * 100).toFixed(1)}% floor ` +
|
|
291
|
+
'(delivery.quality.gates.crap.minMethodResolutionRate).\n' +
|
|
292
|
+
' A baseline built from a broken join is not sparse, it is wrong: ' +
|
|
293
|
+
'unresolved methods are absent and coincidental line collisions are ' +
|
|
294
|
+
'mis-attributed.\n' +
|
|
295
|
+
(worst ? ` Worst unresolved files:\n${worst}\n` : '') +
|
|
296
|
+
" Regenerate coverage ('npm run test:coverage') and re-run; if the " +
|
|
297
|
+
'rate stays low the coverage artifact and the scanned tree disagree.'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
204
301
|
/**
|
|
205
302
|
* Parse `source` exactly once with escomplex and derive both the
|
|
206
303
|
* maintainability score and the raw CRAP method rows from that single report.
|
|
@@ -216,6 +313,10 @@ export function buildBaselineEnvelope({
|
|
|
216
313
|
*
|
|
217
314
|
* @param {string} source Prepared (possibly transpiled) JavaScript source text.
|
|
218
315
|
* @param {object|null} coverageForFile Istanbul coverage entry for this file.
|
|
316
|
+
* @param {((line: number) => number|null)|null} [mapLine] Transpiled →
|
|
317
|
+
* original-source line resolver from `transpileIfNeeded(…, {withLineMap:
|
|
318
|
+
* true})`; `null` for JavaScript, whose coordinates already match the
|
|
319
|
+
* coverage entry's.
|
|
219
320
|
* @returns {{
|
|
220
321
|
* report: object,
|
|
221
322
|
* miScore: number,
|
|
@@ -229,7 +330,7 @@ export function buildBaselineEnvelope({
|
|
|
229
330
|
* parseError: boolean,
|
|
230
331
|
* }}
|
|
231
332
|
*/
|
|
232
|
-
export function analyzeOnce(source, coverageForFile) {
|
|
333
|
+
export function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
233
334
|
let report;
|
|
234
335
|
try {
|
|
235
336
|
report = escomplex.analyzeModule(source);
|
|
@@ -238,18 +339,7 @@ export function analyzeOnce(source, coverageForFile) {
|
|
|
238
339
|
}
|
|
239
340
|
const miScore =
|
|
240
341
|
typeof report.maintainability === 'number' ? report.maintainability : 0;
|
|
241
|
-
const
|
|
242
|
-
const crapRows = [];
|
|
243
|
-
for (const m of methods) {
|
|
244
|
-
const startLine = m?.lineStart;
|
|
245
|
-
if (typeof startLine !== 'number') continue;
|
|
246
|
-
const cyclomatic = m?.cyclomatic ?? 0;
|
|
247
|
-
const coverage = coverageForFile
|
|
248
|
-
? coverageForMethodInEntry(coverageForFile, startLine)
|
|
249
|
-
: null;
|
|
250
|
-
const crap = coverage === null ? null : crapFormula(cyclomatic, coverage);
|
|
251
|
-
crapRows.push({ method: m.name, startLine, cyclomatic, coverage, crap });
|
|
252
|
-
}
|
|
342
|
+
const crapRows = methodRowsFromReport(report, coverageForFile, mapLine);
|
|
253
343
|
return { report, miScore, crapRows, parseError: false };
|
|
254
344
|
}
|
|
255
345
|
|
|
@@ -348,6 +438,7 @@ export async function scanAndScore({
|
|
|
348
438
|
const rows = [];
|
|
349
439
|
let skippedFilesNoCoverage = 0;
|
|
350
440
|
let skippedMethodsNoCoverage = 0;
|
|
441
|
+
const resolution = newResolutionAccumulator();
|
|
351
442
|
for (const { item, result } of perFile) {
|
|
352
443
|
if (!result) continue; // unrecoverable per-file failure: drop silently to match pre-pool semantics
|
|
353
444
|
if (result.skippedFileNoCoverage) {
|
|
@@ -366,6 +457,7 @@ export async function scanAndScore({
|
|
|
366
457
|
continue;
|
|
367
458
|
}
|
|
368
459
|
skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
|
|
460
|
+
accumulateResolution(resolution, item.relPath, result);
|
|
369
461
|
for (const mr of result.rows) {
|
|
370
462
|
rows.push({
|
|
371
463
|
file: item.relPath,
|
|
@@ -390,6 +482,7 @@ export async function scanAndScore({
|
|
|
390
482
|
scannedFiles,
|
|
391
483
|
skippedFilesNoCoverage,
|
|
392
484
|
skippedMethodsNoCoverage,
|
|
485
|
+
resolution: summarizeResolution(resolution),
|
|
393
486
|
};
|
|
394
487
|
}
|
|
395
488
|
|
|
@@ -408,50 +501,33 @@ function scoreFileSerial({ abs, relPath, requireCoverage }, coverage) {
|
|
|
408
501
|
skippedFileNoCoverage: true,
|
|
409
502
|
rows: [],
|
|
410
503
|
skippedMethodsNoCoverage: 0,
|
|
504
|
+
hasCoverageEntry: false,
|
|
505
|
+
resolvedMethods: 0,
|
|
506
|
+
totalMethods: 0,
|
|
411
507
|
};
|
|
412
508
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
rows: null,
|
|
436
|
-
skippedMethodsNoCoverage: 0,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
const rows = [];
|
|
440
|
-
let skippedMethodsNoCoverage = 0;
|
|
441
|
-
for (const mr of crapRows) {
|
|
442
|
-
if (mr.crap === null || mr.coverage === null) {
|
|
443
|
-
skippedMethodsNoCoverage += 1;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
rows.push({
|
|
447
|
-
method: mr.method,
|
|
448
|
-
startLine: mr.startLine,
|
|
449
|
-
cyclomatic: mr.cyclomatic,
|
|
450
|
-
coverage: mr.coverage,
|
|
451
|
-
crap: mr.crap,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return { skippedFileNoCoverage: false, rows, skippedMethodsNoCoverage };
|
|
509
|
+
const dropped = {
|
|
510
|
+
skippedFileNoCoverage: false,
|
|
511
|
+
rows: null,
|
|
512
|
+
skippedMethodsNoCoverage: 0,
|
|
513
|
+
hasCoverageEntry: entry !== null,
|
|
514
|
+
resolvedMethods: 0,
|
|
515
|
+
totalMethods: 0,
|
|
516
|
+
};
|
|
517
|
+
const prepared = prepareSourceForScoring(abs);
|
|
518
|
+
if (prepared.error) return dropped;
|
|
519
|
+
const { crapRows, parseError } = analyzeOnce(
|
|
520
|
+
prepared.code,
|
|
521
|
+
entry,
|
|
522
|
+
prepared.mapLine,
|
|
523
|
+
);
|
|
524
|
+
if (parseError) return dropped;
|
|
525
|
+
const finalized = finalizeMethodRows(crapRows, { requireCoverage });
|
|
526
|
+
return {
|
|
527
|
+
skippedFileNoCoverage: false,
|
|
528
|
+
hasCoverageEntry: entry !== null,
|
|
529
|
+
...finalized,
|
|
530
|
+
};
|
|
455
531
|
}
|
|
456
532
|
|
|
457
533
|
async function scoreFilesViaPool(queue, coverage) {
|
|
@@ -494,33 +570,24 @@ async function scoreFilesViaPool(queue, coverage) {
|
|
|
494
570
|
*/
|
|
495
571
|
function scoreFileCombinedSerial({ abs, relPath, requireCoverage }, coverage) {
|
|
496
572
|
const entry = findCoverageEntry(coverage, relPath);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
source = fs.readFileSync(abs, 'utf-8');
|
|
500
|
-
} catch {
|
|
501
|
-
return {
|
|
502
|
-
relPath,
|
|
503
|
-
miScore: null,
|
|
504
|
-
skippedFileNoCoverage: false,
|
|
505
|
-
crapRows: null,
|
|
506
|
-
skippedMethodsNoCoverage: 0,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
const prepared = transpileIfNeeded(abs, source);
|
|
510
|
-
if (prepared === null) {
|
|
573
|
+
const prepared = prepareSourceForScoring(abs);
|
|
574
|
+
if (prepared.error) {
|
|
511
575
|
return {
|
|
512
576
|
relPath,
|
|
513
|
-
miScore: 0,
|
|
577
|
+
miScore: prepared.error === 'read' ? null : 0,
|
|
514
578
|
skippedFileNoCoverage: false,
|
|
515
579
|
crapRows: null,
|
|
516
580
|
skippedMethodsNoCoverage: 0,
|
|
581
|
+
hasCoverageEntry: entry !== null,
|
|
582
|
+
resolvedMethods: 0,
|
|
583
|
+
totalMethods: 0,
|
|
517
584
|
};
|
|
518
585
|
}
|
|
519
586
|
const {
|
|
520
587
|
miScore,
|
|
521
588
|
crapRows: rawCrapRows,
|
|
522
589
|
parseError,
|
|
523
|
-
} = analyzeOnce(prepared, entry);
|
|
590
|
+
} = analyzeOnce(prepared.code, entry, prepared.mapLine);
|
|
524
591
|
if (parseError) {
|
|
525
592
|
return {
|
|
526
593
|
relPath,
|
|
@@ -528,6 +595,9 @@ function scoreFileCombinedSerial({ abs, relPath, requireCoverage }, coverage) {
|
|
|
528
595
|
skippedFileNoCoverage: false,
|
|
529
596
|
crapRows: null,
|
|
530
597
|
skippedMethodsNoCoverage: 0,
|
|
598
|
+
hasCoverageEntry: entry !== null,
|
|
599
|
+
resolvedMethods: 0,
|
|
600
|
+
totalMethods: 0,
|
|
531
601
|
};
|
|
532
602
|
}
|
|
533
603
|
if (requireCoverage && entry === null) {
|
|
@@ -537,29 +607,22 @@ function scoreFileCombinedSerial({ abs, relPath, requireCoverage }, coverage) {
|
|
|
537
607
|
skippedFileNoCoverage: true,
|
|
538
608
|
crapRows: [],
|
|
539
609
|
skippedMethodsNoCoverage: 0,
|
|
610
|
+
hasCoverageEntry: false,
|
|
611
|
+
resolvedMethods: 0,
|
|
612
|
+
totalMethods: 0,
|
|
540
613
|
};
|
|
541
614
|
}
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
for (const mr of rawCrapRows) {
|
|
545
|
-
if (mr.crap === null || mr.coverage === null) {
|
|
546
|
-
skippedMethodsNoCoverage += 1;
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
crapRows.push({
|
|
550
|
-
method: mr.method,
|
|
551
|
-
startLine: mr.startLine,
|
|
552
|
-
cyclomatic: mr.cyclomatic,
|
|
553
|
-
coverage: mr.coverage,
|
|
554
|
-
crap: mr.crap,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
615
|
+
const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
|
|
616
|
+
finalizeMethodRows(rawCrapRows, { requireCoverage });
|
|
557
617
|
return {
|
|
558
618
|
relPath,
|
|
559
619
|
miScore,
|
|
560
620
|
skippedFileNoCoverage: false,
|
|
561
|
-
crapRows,
|
|
621
|
+
crapRows: rows,
|
|
562
622
|
skippedMethodsNoCoverage,
|
|
623
|
+
hasCoverageEntry: entry !== null,
|
|
624
|
+
resolvedMethods,
|
|
625
|
+
totalMethods,
|
|
563
626
|
};
|
|
564
627
|
}
|
|
565
628
|
|
|
@@ -693,6 +756,7 @@ export async function scanAndScoreCombined({
|
|
|
693
756
|
const crapRows = [];
|
|
694
757
|
let skippedFilesNoCoverage = 0;
|
|
695
758
|
let skippedMethodsNoCoverage = 0;
|
|
759
|
+
const resolution = newResolutionAccumulator();
|
|
696
760
|
|
|
697
761
|
for (const { item, result } of perFile) {
|
|
698
762
|
if (!result) continue; // unrecoverable per-file failure: drop silently
|
|
@@ -716,6 +780,7 @@ export async function scanAndScoreCombined({
|
|
|
716
780
|
continue;
|
|
717
781
|
}
|
|
718
782
|
skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
|
|
783
|
+
accumulateResolution(resolution, item.relPath, result);
|
|
719
784
|
for (const mr of result.crapRows) {
|
|
720
785
|
crapRows.push({
|
|
721
786
|
file: item.relPath,
|
|
@@ -750,6 +815,7 @@ export async function scanAndScoreCombined({
|
|
|
750
815
|
scannedFiles,
|
|
751
816
|
skippedFilesNoCoverage,
|
|
752
817
|
skippedMethodsNoCoverage,
|
|
818
|
+
resolution: summarizeResolution(resolution),
|
|
753
819
|
},
|
|
754
820
|
};
|
|
755
821
|
}
|