mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -14,10 +14,16 @@ import fs from 'node:fs';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { readBaselineAtRef } from '../../baseline-loader.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
COORDINATE_ORIGINAL,
|
|
19
|
+
COORDINATE_TRANSPILED,
|
|
20
|
+
deriveFixGuidance,
|
|
21
|
+
} from '../../crap-engine.js';
|
|
22
|
+
import { isAnonymousMethodLabel } from '../../crap-method-identity.js';
|
|
18
23
|
import { getCrapBaseline } from '../../crap-utils.js';
|
|
19
24
|
import { loadBaseline } from '../../gates/baseline-store.js';
|
|
20
25
|
import { Logger } from '../../Logger.js';
|
|
26
|
+
import { resolveTsTranspilerVersion } from '../../transpile.js';
|
|
21
27
|
import {
|
|
22
28
|
kernelDriftAxis,
|
|
23
29
|
missingBaselineAxis,
|
|
@@ -90,33 +96,95 @@ export function kernelVersion() {
|
|
|
90
96
|
* worse, phantom passes.
|
|
91
97
|
*
|
|
92
98
|
* The stamp makes the boundary explicit and fails closed. Bump it whenever
|
|
93
|
-
* the coverage join, the line coordinate system,
|
|
94
|
-
*
|
|
99
|
+
* the coverage join, the line coordinate system, the unresolved-method policy,
|
|
100
|
+
* or the **method identity rule** changes.
|
|
101
|
+
*
|
|
102
|
+
* Story #4969 bumped it to `method-identity-v3` for the last of those. An
|
|
103
|
+
* anonymous method used to be keyed by escomplex's `<anon method-N>` ordinal
|
|
104
|
+
* and is now keyed by its enclosing-scope path; 34.6% of rows changed identity
|
|
105
|
+
* in one step. Rows keyed the old way and rows keyed the new way describe the
|
|
106
|
+
* same functions under different names, so pairing them is exactly the
|
|
107
|
+
* mis-keyed join this stamp exists to refuse — hence the bump, which is what
|
|
108
|
+
* makes the migration report nothing rather than a wall of phantom verdicts.
|
|
95
109
|
*
|
|
96
110
|
* Deliberately module-local: `envelopeExtras()` is the single production door
|
|
97
111
|
* to this value, so exporting the bare constant would add a second entry
|
|
98
112
|
* point that nothing in production reaches. Callers and tests that need the
|
|
99
113
|
* string read it off `envelopeExtras().scoringSemantics`.
|
|
100
114
|
*/
|
|
101
|
-
const SCORING_SEMANTICS = '
|
|
115
|
+
const SCORING_SEMANTICS = 'method-identity-v3';
|
|
102
116
|
|
|
103
117
|
/**
|
|
104
118
|
* Envelope-level stamps this kind contributes beyond the shared envelope
|
|
105
119
|
* keys. Consumed by `writer.write` via the kind-module protocol.
|
|
106
120
|
*
|
|
107
|
-
*
|
|
121
|
+
* `tsTranspilerVersion` joined the stamp set in Story #4866. A TS row's
|
|
122
|
+
* `startLine` is only an original-source coordinate because a sourcemap said
|
|
123
|
+
* so, and that map is the transpiler's output — so a transpiler change can
|
|
124
|
+
* move every TS row's coordinate, which is half the row identity key. Without
|
|
125
|
+
* the stamp on disk the `ts-transpiler-drift` axis had nothing to compare and
|
|
126
|
+
* passed vacuously.
|
|
127
|
+
*
|
|
128
|
+
* `provenanceStamped` joined in Story #4901 as a **positive** marker: the
|
|
129
|
+
* writer asserting it recorded per-row provenance at all. Absence is the only
|
|
130
|
+
* evidence a pre-#4866 baseline leaves, and no other stamp detects it —
|
|
131
|
+
* `kernelVersion` / `escomplexVersion` track the escomplex package (unmoved
|
|
132
|
+
* by the fix), `scoringSemantics` did not change, and `tsTranspilerVersion`
|
|
133
|
+
* is unreliable in the negative (the `'0.0.0'` sentinel). See the
|
|
134
|
+
* `provenance-unstamped` axis.
|
|
135
|
+
*
|
|
136
|
+
* @returns {{scoringSemantics: string, tsTranspilerVersion: string,
|
|
137
|
+
* provenanceStamped: boolean}}
|
|
108
138
|
*/
|
|
109
139
|
export function envelopeExtras() {
|
|
110
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
scoringSemantics: SCORING_SEMANTICS,
|
|
142
|
+
tsTranspilerVersion: resolveTsTranspilerVersion(),
|
|
143
|
+
provenanceStamped: true,
|
|
144
|
+
};
|
|
111
145
|
}
|
|
112
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Project a scan row onto the persisted baseline row shape.
|
|
149
|
+
*
|
|
150
|
+
* `coordinateSystem` is written **only** when it is not the default
|
|
151
|
+
* `original` (Story #4866). A pure-JavaScript scan therefore emits the exact
|
|
152
|
+
* four-key row it always did — byte-identical baselines, no refresh — while a
|
|
153
|
+
* row that kept transpiled coordinates is distinguishable on disk from one
|
|
154
|
+
* whose sourcemap lookup resolved.
|
|
155
|
+
*/
|
|
113
156
|
export function projectRow(row) {
|
|
114
|
-
|
|
157
|
+
const projected = {
|
|
115
158
|
path: canonicalise(row.path ?? row.file),
|
|
116
159
|
method: row.method,
|
|
117
160
|
startLine: row.startLine,
|
|
118
161
|
crap: row.crap,
|
|
119
162
|
};
|
|
163
|
+
if (row.coordinateSystem === COORDINATE_TRANSPILED) {
|
|
164
|
+
projected.coordinateSystem = COORDINATE_TRANSPILED;
|
|
165
|
+
}
|
|
166
|
+
// Story #4969, same write-only-when-non-default idiom: `anonymous` marks a
|
|
167
|
+
// `method` that is a derived scope-path identity rather than a name the
|
|
168
|
+
// source carries. A named row stays the exact four-key row it always was.
|
|
169
|
+
if (row.anonymous === true) {
|
|
170
|
+
projected.anonymous = true;
|
|
171
|
+
}
|
|
172
|
+
return projected;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read a row's coordinate provenance, defaulting to `original`.
|
|
177
|
+
*
|
|
178
|
+
* A baseline written before Story #4866 carries no stamp at all, and every
|
|
179
|
+
* such row IS an original-source coordinate — un-remapped rows could not
|
|
180
|
+
* reach a `requireCoverage: true` baseline, and the pure-JavaScript case was
|
|
181
|
+
* never affected. Defaulting is therefore back-compatible, not a guess.
|
|
182
|
+
*
|
|
183
|
+
* @param {{coordinateSystem?: string}|null|undefined} row
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
function coordinateSystemOf(row) {
|
|
187
|
+
return row?.coordinateSystem ?? COORDINATE_ORIGINAL;
|
|
120
188
|
}
|
|
121
189
|
|
|
122
190
|
export function sortRows(rows) {
|
|
@@ -273,18 +341,47 @@ export function checkCrapRegression(row, baseline, tolerance, kind) {
|
|
|
273
341
|
|
|
274
342
|
/**
|
|
275
343
|
* Pure comparator. Given scanned `currentRows` and committed
|
|
276
|
-
* `baselineRows`, produce a structured verdict covering all
|
|
344
|
+
* `baselineRows`, produce a structured verdict covering all five match
|
|
277
345
|
* paths:
|
|
278
346
|
*
|
|
279
|
-
* 1. **exact**
|
|
280
|
-
*
|
|
281
|
-
* 2. **drifted**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* 3. **
|
|
286
|
-
*
|
|
287
|
-
*
|
|
347
|
+
* 1. **exact** — same (file, method, startLine). Regresses if
|
|
348
|
+
* current crap > baseline crap + tolerance.
|
|
349
|
+
* 2. **drifted** — same (file, method) but startLine shifted. Uses
|
|
350
|
+
* the closest line-drifted baseline row under the
|
|
351
|
+
* same no-regression rule. A drift without
|
|
352
|
+
* regression is reported informationally.
|
|
353
|
+
* 3. **incomparable** — same (file, method), but the two rows express
|
|
354
|
+
* their startLine in DIFFERENT coordinate systems
|
|
355
|
+
* (Story #4866). Reported, never scored.
|
|
356
|
+
* 4. **new** — no baseline match. Violates if crap > ceiling.
|
|
357
|
+
* 5. **removed** — baseline rows not seen in the current scan.
|
|
358
|
+
* Surfaced only; never a failure.
|
|
359
|
+
*
|
|
360
|
+
* **Why the incomparable bucket exists.** The drift heuristic assumes both
|
|
361
|
+
* rows measure the same axis and the method simply moved along it. When one
|
|
362
|
+
* row's line is an original-source coordinate and the other's is a transpiled
|
|
363
|
+
* one, the "distance" it minimises over is the gap between two coordinate
|
|
364
|
+
* systems, not a code movement — so it pairs rows arbitrarily and then scores
|
|
365
|
+
* the pairing. That is precisely how a scan that changed nothing reports a
|
|
366
|
+
* regression no edit can satisfy. Refusing is the only sound answer: a row
|
|
367
|
+
* whose provenance differs from every candidate's is counted and surfaced,
|
|
368
|
+
* and it does NOT fall through to the new-method arm (it is not new; it is
|
|
369
|
+
* unmeasurable against this baseline).
|
|
370
|
+
*
|
|
371
|
+
* **`provenanceMismatched` is captured BEFORE the provenance filter runs**
|
|
372
|
+
* (Story #4871). `incomparable` only counts rows where *every* candidate
|
|
373
|
+
* disagreed on coordinates; a row with one agreeing candidate and three
|
|
374
|
+
* disagreeing ones scores normally and leaves no trace. That made the
|
|
375
|
+
* evidence of coordinate mixing strictly narrower than the mixing itself —
|
|
376
|
+
* and the unsound-basis backstop, which needs exactly that evidence, had
|
|
377
|
+
* nothing sound to read. The counter below is incremented for **any** row
|
|
378
|
+
* with at least one provenance-mismatched candidate, whichever arm then
|
|
379
|
+
* resolves it.
|
|
380
|
+
*
|
|
381
|
+
* **Unscorable rows never reach an arm.** A row the scan could not score
|
|
382
|
+
* (`crap: null` / `coverage: null`, or an explicit `unscorable: true`) carries
|
|
383
|
+
* no measurement to compare. It is bucketed and counted, and it is excluded
|
|
384
|
+
* from `comparable` so it cannot dilute any ratio derived from this result.
|
|
288
385
|
*/
|
|
289
386
|
export function compareCrap({
|
|
290
387
|
currentRows,
|
|
@@ -303,15 +400,29 @@ export function compareCrap({
|
|
|
303
400
|
const seenBaselineKeys = new Set();
|
|
304
401
|
|
|
305
402
|
const violations = [];
|
|
403
|
+
const incomparableRows = [];
|
|
404
|
+
const unscorableRows = [];
|
|
306
405
|
let regressions = 0;
|
|
307
406
|
let newViolations = 0;
|
|
308
407
|
let drifted = 0;
|
|
408
|
+
let provenanceMismatched = 0;
|
|
309
409
|
|
|
310
410
|
for (const row of currentRows ?? []) {
|
|
411
|
+
if (isUnscorableRow(row)) {
|
|
412
|
+
unscorableRows.push({ ...row, kind: 'unscorable' });
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
311
415
|
const exactKey = `${row.file}::${row.method}@${row.startLine}`;
|
|
312
416
|
const methodKey = `${row.file}::${row.method}`;
|
|
417
|
+
const rowCoords = coordinateSystemOf(row);
|
|
418
|
+
const candidates = methodIndex.get(methodKey) ?? [];
|
|
419
|
+
// Evidence first, filtering second — see the block comment above.
|
|
420
|
+
if (candidates.some((c) => coordinateSystemOf(c) !== rowCoords)) {
|
|
421
|
+
provenanceMismatched += 1;
|
|
422
|
+
}
|
|
423
|
+
|
|
313
424
|
const exact = exactIndex.get(exactKey);
|
|
314
|
-
if (exact) {
|
|
425
|
+
if (exact && coordinateSystemOf(exact) === rowCoords) {
|
|
315
426
|
seenBaselineKeys.add(exactKey);
|
|
316
427
|
const v = checkCrapRegression(row, exact, tolerance, 'regression');
|
|
317
428
|
if (v) {
|
|
@@ -321,22 +432,26 @@ export function compareCrap({
|
|
|
321
432
|
continue;
|
|
322
433
|
}
|
|
323
434
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
435
|
+
if (candidates.length > 0) {
|
|
436
|
+
// Only rows expressed in the SAME coordinate system are comparable;
|
|
437
|
+
// everything else would be resolved through a line-distance heuristic
|
|
438
|
+
// that cannot mean anything across two coordinate systems.
|
|
439
|
+
const comparable = candidates.filter(
|
|
440
|
+
(c) => coordinateSystemOf(c) === rowCoords,
|
|
441
|
+
);
|
|
442
|
+
if (comparable.length === 0) {
|
|
443
|
+
incomparableRows.push({
|
|
444
|
+
...row,
|
|
445
|
+
kind: 'incomparable',
|
|
446
|
+
coordinateSystem: rowCoords,
|
|
447
|
+
baselineCoordinateSystem: coordinateSystemOf(candidates[0]),
|
|
448
|
+
});
|
|
449
|
+
for (const c of candidates) {
|
|
450
|
+
seenBaselineKeys.add(`${c.file}::${c.method}@${c.startLine}`);
|
|
337
451
|
}
|
|
452
|
+
continue;
|
|
338
453
|
}
|
|
339
|
-
|
|
454
|
+
const pick = pickDriftCandidate(comparable, row, seenBaselineKeys);
|
|
340
455
|
seenBaselineKeys.add(`${pick.file}::${pick.method}@${pick.startLine}`);
|
|
341
456
|
drifted += 1;
|
|
342
457
|
const v = checkCrapRegression(row, pick, tolerance, 'drifted-regression');
|
|
@@ -364,17 +479,170 @@ export function compareCrap({
|
|
|
364
479
|
if (!seenBaselineKeys.has(k)) removedRows.push(b);
|
|
365
480
|
}
|
|
366
481
|
|
|
482
|
+
const total = currentRows?.length ?? 0;
|
|
367
483
|
return {
|
|
368
|
-
total
|
|
484
|
+
total,
|
|
485
|
+
// Rows that carried a measurement and therefore *could* be compared. The
|
|
486
|
+
// denominator of every ratio derived from this result.
|
|
487
|
+
comparable: total - unscorableRows.length,
|
|
369
488
|
regressions,
|
|
370
489
|
newViolations,
|
|
371
490
|
drifted,
|
|
491
|
+
provenanceMismatched,
|
|
492
|
+
incomparable: incomparableRows.length,
|
|
493
|
+
unscorable: unscorableRows.length,
|
|
372
494
|
removed: removedRows.length,
|
|
373
495
|
violations,
|
|
496
|
+
incomparableRows,
|
|
497
|
+
unscorableRows,
|
|
374
498
|
removedRows,
|
|
375
499
|
};
|
|
376
500
|
}
|
|
377
501
|
|
|
502
|
+
/**
|
|
503
|
+
* True when a scanned row carries no measurement to compare.
|
|
504
|
+
*
|
|
505
|
+
* Deliberately strict on `null`: a scan row always sets `crap`, and the
|
|
506
|
+
* scorer's own contract is that an unresolved method yields `crap: null` /
|
|
507
|
+
* `coverage: null` rather than an inferred zero. A caller-built row that
|
|
508
|
+
* simply omits `coverage` is not making that claim and stays scorable.
|
|
509
|
+
*
|
|
510
|
+
* @param {{crap?: number|null, coverage?: number|null, unscorable?: boolean}} row
|
|
511
|
+
* @returns {boolean}
|
|
512
|
+
*/
|
|
513
|
+
function isUnscorableRow(row) {
|
|
514
|
+
return (
|
|
515
|
+
row?.unscorable === true || row?.crap === null || row?.coverage === null
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Pick the baseline row a drifted method should be scored against: the
|
|
521
|
+
* closest un-seen candidate by `startLine` distance, falling back to the
|
|
522
|
+
* first when every candidate has already been claimed (duplicate method
|
|
523
|
+
* names in one file).
|
|
524
|
+
*
|
|
525
|
+
* @param {Array<{file: string, method: string, startLine: number}>} comparable
|
|
526
|
+
* @param {{startLine: number}} row
|
|
527
|
+
* @param {Set<string>} seenBaselineKeys
|
|
528
|
+
* @returns {object}
|
|
529
|
+
*/
|
|
530
|
+
function pickDriftCandidate(comparable, row, seenBaselineKeys) {
|
|
531
|
+
let pick = null;
|
|
532
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
533
|
+
for (const c of comparable) {
|
|
534
|
+
const k = `${c.file}::${c.method}@${c.startLine}`;
|
|
535
|
+
if (seenBaselineKeys.has(k)) continue;
|
|
536
|
+
const d = Math.abs(c.startLine - row.startLine);
|
|
537
|
+
if (d < bestDist) {
|
|
538
|
+
bestDist = d;
|
|
539
|
+
pick = c;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return pick ?? comparable[0];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Fraction of a compare's comparable rows that hit a coordinate-provenance
|
|
547
|
+
* mismatch before the comparison basis is judged self-evidently unsound
|
|
548
|
+
* (Story #4866, numerator corrected in Story #4871).
|
|
549
|
+
*
|
|
550
|
+
* **What this ratio must measure.** The backstop exists for one condition: the
|
|
551
|
+
* scan and the baseline expressing `startLine` in different coordinate
|
|
552
|
+
* systems, which makes the nearest-line drift heuristic pair rows arbitrarily
|
|
553
|
+
* and then score the pairing. The only quantity that evidences that condition
|
|
554
|
+
* is a provenance disagreement between a row and its baseline candidates.
|
|
555
|
+
*
|
|
556
|
+
* **Why `drifted` was the wrong numerator.** A row reaches the drift arm only
|
|
557
|
+
* *after* passing the provenance filter, so every drifted row is one whose
|
|
558
|
+
* coordinate system **agreed** with its baseline's. Counting agreements as
|
|
559
|
+
* evidence of disagreement is not a mis-calibration, it is the wrong
|
|
560
|
+
* measurement: on this pure-JavaScript repository — where a coordinate mix is
|
|
561
|
+
* structurally impossible — it read 56–68% across every diff scope, which
|
|
562
|
+
* suppressed the gate's per-method verdicts on every commit while the gate
|
|
563
|
+
* still reported success. `startLine` is half the row identity key, so any
|
|
564
|
+
* insertion re-keys every method below it; ordinary drift is the normal
|
|
565
|
+
* operating state, not an anomaly.
|
|
566
|
+
*
|
|
567
|
+
* Deliberately module-local, like `SCORING_SEMANTICS` above: the three
|
|
568
|
+
* tuning values below are reachable through `assessComparisonBasis` — which
|
|
569
|
+
* takes them as overridable options — so exporting the bare constants would
|
|
570
|
+
* add entry points nothing in production reaches.
|
|
571
|
+
*/
|
|
572
|
+
const UNSOUND_BASIS_MISMATCH_RATIO = 0.5;
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Minimum comparable rows before the unsound-basis check is allowed to fire.
|
|
576
|
+
* A diff-scoped preview can legitimately score three methods, two of which
|
|
577
|
+
* moved; that is a normal edit, not a broken basis. Mirrors the
|
|
578
|
+
* minimum-sample discipline the coverage-join resolution floor already uses.
|
|
579
|
+
*/
|
|
580
|
+
const UNSOUND_BASIS_MIN_SAMPLE = 20;
|
|
581
|
+
|
|
582
|
+
/** Diagnostic name for an unsound comparison basis. */
|
|
583
|
+
const UNSOUND_BASIS_DIAGNOSTIC = 'crap-unsound-comparison-basis';
|
|
584
|
+
|
|
585
|
+
/** Diagnostic name for a baseline the running scorer refuses to compare. */
|
|
586
|
+
export const INCOMPATIBLE_BASELINE_DIAGNOSTIC = 'crap-baseline-incompatible';
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Pure verdict on whether a `compareCrap` result rests on a sound basis.
|
|
590
|
+
*
|
|
591
|
+
* The numerator is `provenanceMismatched` — rows whose baseline candidates
|
|
592
|
+
* included at least one expressed in a different coordinate system, counted
|
|
593
|
+
* by `compareCrap` **before** its provenance filter discards that evidence.
|
|
594
|
+
* The denominator is `comparable`: rows that carried a measurement at all, so
|
|
595
|
+
* a method the scan could not score cannot dilute the ratio into silence.
|
|
596
|
+
*
|
|
597
|
+
* Above the ratio the per-method verdicts are noise derived from a mis-keyed
|
|
598
|
+
* join, and reporting them as regressions asks the operator to fix code that
|
|
599
|
+
* is not broken. Below it — including a scan where every row drifted, which
|
|
600
|
+
* is what an ordinary insertion produces — the verdicts stand.
|
|
601
|
+
*
|
|
602
|
+
* Returns `{ sound: true }` or `{ sound: false, diagnostic: {name, message} }`.
|
|
603
|
+
*
|
|
604
|
+
* @param {{total?: number, comparable?: number, provenanceMismatched?: number,
|
|
605
|
+
* incomparable?: number}} compareResult
|
|
606
|
+
* @param {{ratio?: number, minSample?: number}} [opts]
|
|
607
|
+
*/
|
|
608
|
+
export function assessComparisonBasis(compareResult, opts = {}) {
|
|
609
|
+
const ratio = Number.isFinite(opts.ratio)
|
|
610
|
+
? opts.ratio
|
|
611
|
+
: UNSOUND_BASIS_MISMATCH_RATIO;
|
|
612
|
+
const minSample = Number.isFinite(opts.minSample)
|
|
613
|
+
? opts.minSample
|
|
614
|
+
: UNSOUND_BASIS_MIN_SAMPLE;
|
|
615
|
+
const comparable = compareResult?.comparable ?? compareResult?.total ?? 0;
|
|
616
|
+
if (comparable < minSample) return { sound: true };
|
|
617
|
+
// `incomparable` is the total-mismatch subset of `provenanceMismatched`, so
|
|
618
|
+
// it is already counted; the max guards a caller that supplies only one.
|
|
619
|
+
const mismatched = Math.max(
|
|
620
|
+
compareResult?.provenanceMismatched ?? 0,
|
|
621
|
+
compareResult?.incomparable ?? 0,
|
|
622
|
+
);
|
|
623
|
+
const observed = mismatched / comparable;
|
|
624
|
+
if (observed <= ratio) return { sound: true };
|
|
625
|
+
return {
|
|
626
|
+
sound: false,
|
|
627
|
+
diagnostic: {
|
|
628
|
+
name: UNSOUND_BASIS_DIAGNOSTIC,
|
|
629
|
+
message:
|
|
630
|
+
`[CRAP] ⚠ Comparison basis is unsound: ${mismatched}/${comparable} ` +
|
|
631
|
+
`(${(observed * 100).toFixed(1)}%) of comparable methods matched a ` +
|
|
632
|
+
'baseline row expressed in a DIFFERENT line coordinate system — ' +
|
|
633
|
+
`above the ${(ratio * 100).toFixed(0)}% threshold.\n` +
|
|
634
|
+
' At this ratio the baseline and the scan are not describing ' +
|
|
635
|
+
'the same line coordinates, so every per-method verdict below would ' +
|
|
636
|
+
'be derived from a mis-keyed join rather than from your change. ' +
|
|
637
|
+
'They are suppressed.\n' +
|
|
638
|
+
" Re-seed the baseline: run 'npm run test:coverage' then " +
|
|
639
|
+
"'npm run crap:update -- --full-scope' and commit the result with a " +
|
|
640
|
+
"'baseline-refresh:' subject. The authoritative check-baselines gate " +
|
|
641
|
+
'still gates the merge.',
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
378
646
|
/**
|
|
379
647
|
* Declarative axis table for `evaluateBaselineCompatibility` (Story #2467).
|
|
380
648
|
*
|
|
@@ -393,6 +661,17 @@ export function compareCrap({
|
|
|
393
661
|
* and `tsTranspilerVersion` drift to **warn**, not fail; `escomplexVersion`
|
|
394
662
|
* mismatch continues to fail closed.
|
|
395
663
|
*/
|
|
664
|
+
/**
|
|
665
|
+
* The one re-seed recipe every coordinate-invalidating axis ends on. Three
|
|
666
|
+
* axes and the unsound-basis diagnostic previously carried their own copy of
|
|
667
|
+
* this sentence; a single constant keeps them from drifting apart on the
|
|
668
|
+
* command an operator is told to run.
|
|
669
|
+
*/
|
|
670
|
+
const RESEED_REMEDY =
|
|
671
|
+
"Re-derive the baseline: run 'npm run test:coverage' then " +
|
|
672
|
+
"'npm run crap:update -- --full-scope' and commit the result with a " +
|
|
673
|
+
"'baseline-refresh:' subject.";
|
|
674
|
+
|
|
396
675
|
export const CRAP_COMPAT_AXES = [
|
|
397
676
|
// Universal axes hoisted into envelope.js (Story #2467, Task #2492). The
|
|
398
677
|
// missing-baseline and kernel-drift checks live in exactly one place;
|
|
@@ -418,27 +697,124 @@ export const CRAP_COMPAT_AXES = [
|
|
|
418
697
|
`[CRAP] scoring semantics changed: baseline=${stamped ?? '<unstamped>'} ` +
|
|
419
698
|
`running=${SCORING_SEMANTICS}. Rows scored by the previous per-method ` +
|
|
420
699
|
'coverage join are not comparable to rows scored by the current one, ' +
|
|
421
|
-
|
|
422
|
-
"'npm run test:coverage' then 'npm run crap:update -- --full-scope' " +
|
|
423
|
-
"and commit the result with a 'baseline-refresh:' subject."
|
|
700
|
+
`so this baseline cannot be compared. ${RESEED_REMEDY}`
|
|
424
701
|
);
|
|
425
702
|
},
|
|
426
703
|
},
|
|
427
704
|
{
|
|
428
705
|
name: 'ts-transpiler-drift',
|
|
429
|
-
severity: '
|
|
706
|
+
severity: 'fatal',
|
|
430
707
|
check: ({ baseline, runningTsTranspilerVersion }) => {
|
|
431
|
-
if (!baseline
|
|
432
|
-
|
|
708
|
+
if (!baseline) return null;
|
|
709
|
+
if (!isKnownVersion(runningTsTranspilerVersion)) return null;
|
|
710
|
+
const baselineTs = baseline.tsTranspilerVersion;
|
|
711
|
+
// An unstamped (or sentinel) baseline has nothing to compare — the
|
|
712
|
+
// stamp landed in Story #4866 and comparing against its absence would
|
|
713
|
+
// fail every pre-existing baseline closed for no evidence at all.
|
|
714
|
+
if (!isKnownVersion(baselineTs)) return null;
|
|
433
715
|
if (baselineTs === runningTsTranspilerVersion) return null;
|
|
716
|
+
// Scoped to baselines that actually contain transpiled sources: a
|
|
717
|
+
// transpiler change moves TS row coordinates and nothing else. A
|
|
718
|
+
// pure-JavaScript tree has no coordinate to move, so a TS bump there is
|
|
719
|
+
// not a coordinate-invalidating event and must not fail its gate.
|
|
720
|
+
if (!hasTranspiledRows(baseline)) return null;
|
|
721
|
+
return (
|
|
722
|
+
`[CRAP] tsTranspilerVersion changed: baseline=${baselineTs} running=${runningTsTranspilerVersion}. ` +
|
|
723
|
+
"A TS row's startLine is an original-source coordinate only because the transpiler's " +
|
|
724
|
+
'sourcemap said so, and that coordinate is half the row identity key — so rows scored ' +
|
|
725
|
+
`under the previous transpiler are not comparable to rows scored under this one. ${RESEED_REMEDY}`
|
|
726
|
+
);
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
// Story #4901. Closes the exemption directly above: `ts-transpiler-drift`
|
|
731
|
+
// returns null for an unstamped baseline rather than fail every
|
|
732
|
+
// pre-existing one for want of evidence — and a pre-#4866 baseline is
|
|
733
|
+
// exactly that shape, so the one door that could catch it lets it through
|
|
734
|
+
// while it asserts by omission that all its rows are original coordinates.
|
|
735
|
+
// Keyed on a positive marker because absence alone cannot separate
|
|
736
|
+
// "written before provenance existed" from "typescript was unresolvable".
|
|
737
|
+
// Scoped like `ts-transpiler-drift`: a pure-JavaScript baseline's two
|
|
738
|
+
// coordinate systems coincide, so it was never affected and must not fail.
|
|
739
|
+
name: 'provenance-unstamped',
|
|
740
|
+
severity: 'fatal',
|
|
741
|
+
check: ({ baseline }) =>
|
|
742
|
+
baseline &&
|
|
743
|
+
baseline.provenanceStamped !== true &&
|
|
744
|
+
hasTranspiledRows(baseline)
|
|
745
|
+
? '[CRAP] baseline predates coordinate-provenance stamping: it carries ' +
|
|
746
|
+
'transpiled-source rows but no `provenanceStamped` marker, so it ' +
|
|
747
|
+
'asserts by omission that every row is an original-source coordinate. ' +
|
|
748
|
+
'`startLine` is half the row identity key, so the comparator would ' +
|
|
749
|
+
'key those rows against a coordinate space they are not in and ' +
|
|
750
|
+
`report regressions no edit can satisfy. ${RESEED_REMEDY}`
|
|
751
|
+
: null,
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
// Story #4969. The `scoring-semantics-drift` axis above rejects a baseline
|
|
755
|
+
// stamped with the OLD semantics wholesale, which covers a clean migration.
|
|
756
|
+
// It cannot see a HALF-migrated one: a diff-scoped refresh preserves
|
|
757
|
+
// out-of-scope rows verbatim, so a baseline can carry ordinal-keyed rows
|
|
758
|
+
// for the files that were never re-scored while the writer stamps the
|
|
759
|
+
// envelope with the current semantics — the stamp says v3, some rows are
|
|
760
|
+
// still v2, and the one axis that would catch it has already passed.
|
|
761
|
+
//
|
|
762
|
+
// Such a row cannot be paired with anything: its `<anon method-N>` label
|
|
763
|
+
// matches no scope-path identity, so the comparator files the live method
|
|
764
|
+
// as NEW (scored against the ceiling, not its own baseline) and the stale
|
|
765
|
+
// row as removed. Keyed on the positive `anonymous` marker for the same
|
|
766
|
+
// reason `provenance-unstamped` is: absence is the only trace the old
|
|
767
|
+
// writer leaves.
|
|
768
|
+
name: 'anon-identity-unstamped',
|
|
769
|
+
severity: 'fatal',
|
|
770
|
+
check: ({ baseline }) => {
|
|
771
|
+
if (!baseline) return null;
|
|
772
|
+
const stale = (baseline.rows ?? []).filter(
|
|
773
|
+
(row) => isAnonymousMethodLabel(row?.method) && row?.anonymous !== true,
|
|
774
|
+
);
|
|
775
|
+
if (stale.length === 0) return null;
|
|
434
776
|
return (
|
|
435
|
-
`[CRAP]
|
|
436
|
-
|
|
777
|
+
`[CRAP] baseline carries ${stale.length} anonymous row(s) keyed by the ` +
|
|
778
|
+
'superseded `<anon method-N>` ordinal (e.g. ' +
|
|
779
|
+
`${stale[0].path ?? stale[0].file}::${stale[0].method}) while the ` +
|
|
780
|
+
'envelope claims the current scoring semantics. Those ordinals ' +
|
|
781
|
+
'renumber whenever any anonymous function is added or removed, so the ' +
|
|
782
|
+
'comparator would score the live methods against the new-method ' +
|
|
783
|
+
`ceiling instead of their own baseline. ${RESEED_REMEDY}`
|
|
437
784
|
);
|
|
438
785
|
},
|
|
439
786
|
},
|
|
440
787
|
];
|
|
441
788
|
|
|
789
|
+
/** Sources whose escomplex coordinates are transpiled, not original. */
|
|
790
|
+
const TRANSPILED_SOURCE_RE = /\.(?:ts|tsx|mts|cts)$/i;
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* `'0.0.0'` is this codebase's established "unknown environment" sentinel for
|
|
794
|
+
* a resolved dependency version, not a real release. Treating it as a
|
|
795
|
+
* comparable value would turn "we could not resolve typescript" into
|
|
796
|
+
* "typescript changed".
|
|
797
|
+
*
|
|
798
|
+
* @param {unknown} version
|
|
799
|
+
* @returns {boolean}
|
|
800
|
+
*/
|
|
801
|
+
function isKnownVersion(version) {
|
|
802
|
+
return typeof version === 'string' && version !== '' && version !== '0.0.0';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* True when a baseline contains at least one row derived from a transpiled
|
|
807
|
+
* source — the only rows a transpiler-version change can move.
|
|
808
|
+
*
|
|
809
|
+
* @param {{rows?: Array<{path?: string, file?: string}>}|null} baseline
|
|
810
|
+
* @returns {boolean}
|
|
811
|
+
*/
|
|
812
|
+
function hasTranspiledRows(baseline) {
|
|
813
|
+
return (baseline?.rows ?? []).some((row) =>
|
|
814
|
+
TRANSPILED_SOURCE_RE.test(String(row?.path ?? row?.file ?? '')),
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
442
818
|
/**
|
|
443
819
|
* Pure decision helper for the missing-baseline / kernel-mismatch /
|
|
444
820
|
* escomplex-mismatch / tsTranspiler-mismatch gate paths. Lets tests
|
|
@@ -454,27 +830,53 @@ export function evaluateBaselineCompatibility(ctx) {
|
|
|
454
830
|
}
|
|
455
831
|
|
|
456
832
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* `
|
|
466
|
-
*
|
|
467
|
-
* vacuously
|
|
833
|
+
* The compat axes a *loaded* envelope can be judged against on its own,
|
|
834
|
+
* without a second baseline to diff. Each invalidates one half of the row
|
|
835
|
+
* identity key. Three are about the `startLine` half: one names the join that
|
|
836
|
+
* produced the rows, one names the transpiler whose sourcemap decided what a
|
|
837
|
+
* TS row's `startLine` even means, and one (Story #4901) catches a baseline
|
|
838
|
+
* predating both questions. The fourth (Story #4969) is about the `method`
|
|
839
|
+
* half — a baseline still carrying ordinal-keyed anonymous rows.
|
|
840
|
+
*
|
|
841
|
+
* `escomplex-mismatch` and `kernel-drift` stay out — the v2 envelope carries
|
|
842
|
+
* no `escomplexVersion`, so that axis would compare `undefined` to `undefined`
|
|
843
|
+
* and pass vacuously, which is worse than not running it.
|
|
844
|
+
*/
|
|
845
|
+
const LOADED_ENVELOPE_AXES = [
|
|
846
|
+
'scoring-semantics-drift',
|
|
847
|
+
'ts-transpiler-drift',
|
|
848
|
+
'provenance-unstamped',
|
|
849
|
+
'anon-identity-unstamped',
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Kind-module hook (Story #4775, extended by Story #4866): judge a *loaded*
|
|
854
|
+
* v2 envelope against the axes that need no peer baseline. The unified
|
|
855
|
+
* `check-baselines` gate calls it straight after `reader.load` and turns a
|
|
856
|
+
* message into a fail-closed schema-class error, so a baseline written by
|
|
857
|
+
* incompatible scoring semantics — or by a different transpiler, which moves
|
|
858
|
+
* the coordinates that are half the row identity key — can never be silently
|
|
859
|
+
* compared against current scores.
|
|
860
|
+
*
|
|
861
|
+
* This is the *production* door for `ts-transpiler-drift`. Before #4866 the
|
|
862
|
+
* axis was reachable only from its own unit test: nothing in production called
|
|
863
|
+
* `evaluateBaselineCompatibility`, and this function deliberately excluded it.
|
|
468
864
|
*
|
|
469
865
|
* @param {object|null} baseline A loaded v2 baseline envelope.
|
|
866
|
+
* @param {{runningTsTranspilerVersion?: string}} [ctx] Injectable running
|
|
867
|
+
* versions; resolved from the environment when omitted.
|
|
470
868
|
* @returns {string|null} Operator-facing message, or null when compatible.
|
|
471
869
|
*/
|
|
472
|
-
export function assertBaselineCompatible(baseline) {
|
|
870
|
+
export function assertBaselineCompatible(baseline, ctx = {}) {
|
|
473
871
|
if (!baseline) return null;
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
|
|
872
|
+
const runningTsTranspilerVersion =
|
|
873
|
+
ctx.runningTsTranspilerVersion ?? resolveTsTranspilerVersion();
|
|
874
|
+
for (const name of LOADED_ENVELOPE_AXES) {
|
|
875
|
+
const axis = CRAP_COMPAT_AXES.find((a) => a.name === name);
|
|
876
|
+
const message = axis?.check({ baseline, runningTsTranspilerVersion });
|
|
877
|
+
if (message) return message;
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
478
880
|
}
|
|
479
881
|
|
|
480
882
|
/**
|
|
@@ -571,6 +973,17 @@ export function buildCrapReport({
|
|
|
571
973
|
regressions: compareResult.regressions,
|
|
572
974
|
newViolations: compareResult.newViolations,
|
|
573
975
|
drifted: compareResult.drifted,
|
|
976
|
+
// Story #4866: rows the compare refused to resolve because their
|
|
977
|
+
// coordinate provenance differs from the baseline's.
|
|
978
|
+
incomparable: compareResult.incomparable ?? 0,
|
|
979
|
+
// Story #4871: rows whose baseline candidates included at least one in a
|
|
980
|
+
// different coordinate system — the evidence the unsound-basis backstop
|
|
981
|
+
// reads, captured before the provenance filter discards it.
|
|
982
|
+
provenanceMismatched: compareResult.provenanceMismatched ?? 0,
|
|
983
|
+
// Story #4871: methods carrying no measurement to compare — the scan
|
|
984
|
+
// found no coverage artifact for them. Reported, never scored from an
|
|
985
|
+
// assumed zero, and never part of a ratio's denominator.
|
|
986
|
+
unscorable: (compareResult.unscorable ?? 0) + skippedNoCoverage,
|
|
574
987
|
removed: compareResult.removed,
|
|
575
988
|
skippedNoCoverage,
|
|
576
989
|
scope,
|
|
@@ -580,6 +993,33 @@ export function buildCrapReport({
|
|
|
580
993
|
};
|
|
581
994
|
}
|
|
582
995
|
|
|
996
|
+
/**
|
|
997
|
+
* Rebuild a CRAP envelope with its per-method verdicts suppressed and one
|
|
998
|
+
* named diagnostic in their place (Story #4866).
|
|
999
|
+
*
|
|
1000
|
+
* Used by the preview gate on the two conditions under which a per-method
|
|
1001
|
+
* verdict cannot mean anything: a baseline the running scorer refuses to
|
|
1002
|
+
* compare, and a comparison basis whose drifted-row ratio proves the two
|
|
1003
|
+
* sides disagree on coordinates. The counts stay so the operator can see the
|
|
1004
|
+
* evidence; only the accusations go.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {object} envelope
|
|
1007
|
+
* @param {{name: string, message: string}} diagnostic
|
|
1008
|
+
* @returns {object}
|
|
1009
|
+
*/
|
|
1010
|
+
export function suppressVerdicts(envelope, diagnostic) {
|
|
1011
|
+
return {
|
|
1012
|
+
...envelope,
|
|
1013
|
+
summary: {
|
|
1014
|
+
...envelope.summary,
|
|
1015
|
+
regressions: 0,
|
|
1016
|
+
newViolations: 0,
|
|
1017
|
+
},
|
|
1018
|
+
violations: [],
|
|
1019
|
+
diagnostics: [diagnostic],
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
583
1023
|
/**
|
|
584
1024
|
* Logger-only printers hoisted from `check-crap.js`. Kept here so the
|
|
585
1025
|
* CLI shell stays thin and the printers can be exercised in unit tests
|
|
@@ -591,6 +1031,8 @@ export function printSummaryHeader(result, scanSummary) {
|
|
|
591
1031
|
Logger.info(`Regressions: ${result.regressions}`);
|
|
592
1032
|
Logger.info(`New-method violations: ${result.newViolations}`);
|
|
593
1033
|
Logger.info(`Drifted (matched): ${result.drifted}`);
|
|
1034
|
+
Logger.info(`Provenance mismatched: ${result.provenanceMismatched ?? 0}`);
|
|
1035
|
+
Logger.info(`Unscorable (no cov): ${result.unscorable ?? 0}`);
|
|
594
1036
|
Logger.info(`Removed from baseline: ${result.removed}`);
|
|
595
1037
|
if (scanSummary?.skippedFilesNoCoverage) {
|
|
596
1038
|
Logger.info(
|