mandrel 2.25.0 → 2.27.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 +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -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 +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- 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-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -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-lenses.js +1 -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 +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/reader.js +52 -38
- 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 -11
- 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/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- 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/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -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/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/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/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 +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- 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/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- 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/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- 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/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- 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/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- 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/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -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 +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- 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/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- 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 +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +71 -0
- 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
|
@@ -30,42 +30,6 @@ import { readAuditRulesSync } from './audit-rules-reader.js';
|
|
|
30
30
|
|
|
31
31
|
const DEFAULT_GIT_TIMEOUT_MS = 30000;
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
* The audit-lens identifier for the navigability lens (Epic #4131, F2/F3).
|
|
35
|
-
* Authored as `.agents/workflows/audit-navigability.md`; registered here so the
|
|
36
|
-
* roster, the global-lens allowlist, and the route-added routing seam all
|
|
37
|
-
* reference one symbol rather than a hard-coded string.
|
|
38
|
-
*/
|
|
39
|
-
export const NAVIGABILITY_LENS = 'audit-navigability';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The **global-lens allowlist** — lenses that evaluate a property of the
|
|
43
|
-
* **whole** product (not just the Epic's change set) and are therefore exempt
|
|
44
|
-
* from the cross-epic-leak guard (`#3362`) that narrows every other lens's
|
|
45
|
-
* evidence to the Epic's `changedFiles`. A lens in this set still runs through
|
|
46
|
-
* the SAME `runAuditSuite` / `selectAuditStrategy` engine; only the
|
|
47
|
-
* change-set narrowing is bypassed, and only for the listed lenses. The guard
|
|
48
|
-
* is **not** weakened for any lens absent from this set.
|
|
49
|
-
*
|
|
50
|
-
* Navigability is the founding member: reachability is a global property — a
|
|
51
|
-
* change can orphan a route it never touched — so the lens must read the whole
|
|
52
|
-
* route tree + nav registry regardless of which file triggered it.
|
|
53
|
-
*/
|
|
54
|
-
export const GLOBAL_LENS_ALLOWLIST = Object.freeze([NAVIGABILITY_LENS]);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* True when `lens` is on the global-lens allowlist and is therefore exempt
|
|
58
|
-
* from the cross-epic-leak guard's change-set narrowing. Pure; the single
|
|
59
|
-
* read-side of {@link GLOBAL_LENS_ALLOWLIST} so callers never hard-code the
|
|
60
|
-
* membership test.
|
|
61
|
-
*
|
|
62
|
-
* @param {string} lens
|
|
63
|
-
* @returns {boolean}
|
|
64
|
-
*/
|
|
65
|
-
export function isGlobalLens(lens) {
|
|
66
|
-
return GLOBAL_LENS_ALLOWLIST.includes(lens);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
33
|
/**
|
|
70
34
|
* The canonical concern-ownership tiers a lens can declare via its
|
|
71
35
|
* `scope` field in [`audit-rules.json`](../../../schemas/audit-rules.json).
|
|
@@ -222,40 +186,23 @@ export function selectSensitivePathClasses({
|
|
|
222
186
|
/**
|
|
223
187
|
* Resolve the consumer's navigability route globs from the resolved config.
|
|
224
188
|
* Reads `delivery.quality.navigability.routeGlobs` — the route-tree SSOT the
|
|
225
|
-
* navigability lens enumerates
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
189
|
+
* navigability lens enumerates. Returns an empty array when the block (or any
|
|
190
|
+
* ancestor) is absent, so an unconfigured consumer contributes no web-surface
|
|
191
|
+
* evidence (Epic #4131 — "no-op when unconfigured").
|
|
192
|
+
*
|
|
193
|
+
* Module-local: the sole reader is {@link hasWebSurface}. It was previously
|
|
194
|
+
* exported alongside a route-added routing seam that Story #4926 removed —
|
|
195
|
+
* lens routing is decided by the `scope` field in `audit-rules.json`, never by
|
|
196
|
+
* a second predicate here.
|
|
229
197
|
*
|
|
230
198
|
* @param {object|null|undefined} config Resolved `.agentrc.json` wrapper.
|
|
231
199
|
* @returns {string[]} Route globs, or `[]` when unconfigured.
|
|
232
200
|
*/
|
|
233
|
-
|
|
201
|
+
function resolveNavigabilityRouteGlobs(config) {
|
|
234
202
|
const globs = config?.delivery?.quality?.navigability?.routeGlobs;
|
|
235
203
|
return Array.isArray(globs) ? globs.filter((g) => typeof g === 'string') : [];
|
|
236
204
|
}
|
|
237
205
|
|
|
238
|
-
/**
|
|
239
|
-
* Decide whether a change set routes the navigability lens. The lens is routed
|
|
240
|
-
* when any `changedFiles` entry matches a consumer-configured route glob
|
|
241
|
-
* (`delivery.quality.navigability.routeGlobs`) — i.e. the change set adds or
|
|
242
|
-
* touches a route file. When no route globs are configured, this returns
|
|
243
|
-
* `false` (the unconfigured no-op), so the existing change-set-scoped lens
|
|
244
|
-
* selection is unchanged.
|
|
245
|
-
*
|
|
246
|
-
* A pure predicate over a change set — the same input {@link selectAudits} and
|
|
247
|
-
* {@link selectLocalLenses} already match against. The caller unions its result
|
|
248
|
-
* into the lens roster it is assembling; no new routing machinery is added.
|
|
249
|
-
*
|
|
250
|
-
* @param {{ changedFiles?: string[], config?: object|null }} params
|
|
251
|
-
* @returns {boolean}
|
|
252
|
-
*/
|
|
253
|
-
export function routesNavigabilityLens({ changedFiles, config } = {}) {
|
|
254
|
-
const globs = resolveNavigabilityRouteGlobs(config);
|
|
255
|
-
if (globs.length === 0) return false;
|
|
256
|
-
return matchesAnyFilePattern(globs, changedFiles ?? []);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
206
|
/**
|
|
260
207
|
* Package names (or scope/name segments of them) that declare a **web
|
|
261
208
|
* rendering surface**. Matched against the consumer's root `package.json`
|
|
@@ -45,10 +45,22 @@ export const BASELINE_KIND_SCHEMA_FILES = Object.freeze([
|
|
|
45
45
|
'duplication.schema.json',
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Report schemas that live under `.agents/schemas/baselines/` but describe
|
|
50
|
+
* an *engine's output about* the baselines rather than a committed baseline
|
|
51
|
+
* (Story #4902). They are registered here for two reasons: the AJV instance
|
|
52
|
+
* below is the one place a caller can compile a baselines-directory schema
|
|
53
|
+
* without re-registering the envelope, and the mirror-drift test compares
|
|
54
|
+
* this registry against the on-disk listing — an unregistered file there is
|
|
55
|
+
* drift regardless of which category it belongs to.
|
|
56
|
+
*/
|
|
57
|
+
const BASELINE_REPORT_SCHEMA_FILES = ['audit-baselines-envelope.schema.json'];
|
|
58
|
+
|
|
59
|
+
/** Every baseline schema filename (envelope + per-kind + report) in registration order. */
|
|
49
60
|
export const BASELINE_SCHEMA_FILES = Object.freeze([
|
|
50
61
|
BASELINE_ENVELOPE_FILE,
|
|
51
62
|
...BASELINE_KIND_SCHEMA_FILES,
|
|
63
|
+
...BASELINE_REPORT_SCHEMA_FILES,
|
|
52
64
|
]);
|
|
53
65
|
|
|
54
66
|
/**
|
|
@@ -3,26 +3,32 @@
|
|
|
3
3
|
* the manual baseline-update CLIs (Story #1974 / Task #1986, Epic #1943).
|
|
4
4
|
*
|
|
5
5
|
* `update-coverage-baseline.js`, `update-crap-baseline.js`,
|
|
6
|
-
* `update-maintainability-baseline.js`, and
|
|
7
|
-
* all accept an opt-in `--diff-scope <ref>`
|
|
8
|
-
* baseline write narrows to files changed since
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* `update-maintainability-baseline.js`, and
|
|
7
|
+
* `update-duplication-baseline.js` all accept an opt-in `--diff-scope <ref>`
|
|
8
|
+
* flag. When supplied, the baseline write narrows to files changed since
|
|
9
|
+
* `<ref>`; out-of-scope rows are preserved verbatim from the prior on-disk
|
|
10
|
+
* baseline.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* intentionally rewrite the whole baseline.
|
|
12
|
+
* The helper is shared to keep the flag's contract identical across the four
|
|
13
|
+
* scripts: one argv parser, one spelling, one error message.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* **Scope of this module, post-Story #4944.** It parses the flag and nothing
|
|
16
|
+
* else. Everything downstream of the parse — resolving the changed-file set,
|
|
17
|
+
* reading the prior envelope, and assembling the writer call — used to live
|
|
18
|
+
* here too, in `buildWriterScopeArgs` and its helpers. Those were the
|
|
19
|
+
* pre-service path; `lib/baselines/refresh-service.js` owns all of it now
|
|
20
|
+
* (`resolveScope`, `readPriorEnvelope`, and the `writer.write()` handoff),
|
|
21
|
+
* and `refreshBaseline()` derives its own diff via `execFile` rather than
|
|
22
|
+
* `spawnSync`. `update-duplication-baseline.js` was the last caller of the
|
|
23
|
+
* legacy path; when it migrated, the whole write-side half of this module
|
|
24
|
+
* became unreachable and was removed rather than left shipped-but-uncalled.
|
|
25
|
+
*
|
|
26
|
+
* Note for anyone tracing the per-kind prior-row reader that used to live
|
|
27
|
+
* here: it is gone, not relocated. The service reads the prior envelope
|
|
28
|
+
* kind-agnostically (any `{ rows: [], rollup: {} }` document), so the
|
|
29
|
+
* per-kind row-shape filter it needed no longer has a job to do.
|
|
20
30
|
*/
|
|
21
31
|
|
|
22
|
-
import { spawnSync } from 'node:child_process';
|
|
23
|
-
import fs from 'node:fs';
|
|
24
|
-
import { parseNameOnlyStdout } from '../changed-files.js';
|
|
25
|
-
|
|
26
32
|
/**
|
|
27
33
|
* Parse `--diff-scope <ref>` (and the legacy `--diff-scope=<ref>` form)
|
|
28
34
|
* from an argv slice. Returns `null` when the flag is absent. Throws a
|
|
@@ -57,147 +63,3 @@ export function parseDiffScopeFlag(argv = []) {
|
|
|
57
63
|
}
|
|
58
64
|
return null;
|
|
59
65
|
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the file footprint of `git diff --name-only <ref>...HEAD`.
|
|
63
|
-
* Returns a `Set<string>` of repo-relative paths with forward-slash
|
|
64
|
-
* normalisation. Returns an empty Set when the diff is empty or git
|
|
65
|
-
* exits non-zero (best-effort; a missing-ref or corrupt repo is the
|
|
66
|
-
* operator's signal to inspect the working tree).
|
|
67
|
-
*
|
|
68
|
-
* The `spawnImpl` seam exists for unit tests — production callers omit it.
|
|
69
|
-
*
|
|
70
|
-
* @param {{ ref: string, cwd?: string, spawnImpl?: typeof spawnSync }} args
|
|
71
|
-
* @returns {Set<string>}
|
|
72
|
-
*/
|
|
73
|
-
export function resolveDiffScopeFiles({
|
|
74
|
-
ref,
|
|
75
|
-
cwd = process.cwd(),
|
|
76
|
-
spawnImpl = spawnSync,
|
|
77
|
-
} = {}) {
|
|
78
|
-
if (typeof ref !== 'string' || ref.length === 0) return new Set();
|
|
79
|
-
const res = spawnImpl('git', ['diff', '--name-only', `${ref}...HEAD`], {
|
|
80
|
-
cwd,
|
|
81
|
-
encoding: 'utf8',
|
|
82
|
-
});
|
|
83
|
-
if (!res || res.status !== 0) return new Set();
|
|
84
|
-
return new Set(parseNameOnlyStdout(res.stdout));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Convenience: parse `--diff-scope` and resolve files in one call.
|
|
89
|
-
* Returns `null` when the flag is absent (so the caller can branch on
|
|
90
|
-
* "scope was opted in?"); otherwise returns
|
|
91
|
-
* `{ ref, files: Set<string>, scope: { mode: 'diff', files } }` ready to
|
|
92
|
-
* pass into `writer.write({ scope })`.
|
|
93
|
-
*
|
|
94
|
-
* @param {{ argv: string[], cwd?: string, spawnImpl?: typeof spawnSync }} args
|
|
95
|
-
* @returns {{ ref: string, files: Set<string>, scope: {mode: 'diff', files: Set<string>} } | null}
|
|
96
|
-
*/
|
|
97
|
-
export function resolveDiffScope({ argv, cwd, spawnImpl } = {}) {
|
|
98
|
-
const ref = parseDiffScopeFlag(argv);
|
|
99
|
-
if (ref === null) return null;
|
|
100
|
-
const files = resolveDiffScopeFiles({ ref, cwd, spawnImpl });
|
|
101
|
-
return { ref, files, scope: { mode: 'diff', files } };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Read + parse the prior baseline at `absBaselinePath` and return the
|
|
106
|
-
* canonical `rows[]` array (per-kind row shape with `path:` keys, as
|
|
107
|
-
* expected by the per-kind `mergeRows` / `applyEpsilon` helpers from
|
|
108
|
-
* Story #1974). Returns `null` when the file is absent, malformed, or
|
|
109
|
-
* missing a `rows[]` envelope; the caller treats `null` as "skip the
|
|
110
|
-
* merge" (regression-fail-safe — equivalent to a fresh write).
|
|
111
|
-
*
|
|
112
|
-
* Pure-by-design (file I/O through the injected `fsImpl` seam).
|
|
113
|
-
*
|
|
114
|
-
* @param {{ kind: 'maintainability' | 'crap', absBaselinePath: string, fsImpl?: typeof fs }} args
|
|
115
|
-
* @returns {Array<object> | null}
|
|
116
|
-
*/
|
|
117
|
-
// Read + JSON-parse a baseline file. Returns `null` on any I/O or parse
|
|
118
|
-
// failure (the caller treats "no prior" the same as "unreadable prior").
|
|
119
|
-
function readBaselineJson(absBaselinePath, fsImpl) {
|
|
120
|
-
let raw;
|
|
121
|
-
try {
|
|
122
|
-
raw = fsImpl.readFileSync(absBaselinePath, 'utf8');
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const parsed = JSON.parse(raw);
|
|
128
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// CRAP path: envelope `rows[]` only; rows already carry canonical `path:`.
|
|
135
|
-
function readCrapPriorRows(parsed) {
|
|
136
|
-
if (!Array.isArray(parsed.rows)) return null;
|
|
137
|
-
return parsed.rows;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Maintainability path: envelope `rows[]` only; rows already carry
|
|
141
|
-
// canonical `path:` + `mi:`.
|
|
142
|
-
function readMaintainabilityPriorRows(parsed) {
|
|
143
|
-
if (!Array.isArray(parsed.rows)) return null;
|
|
144
|
-
return parsed.rows.filter(
|
|
145
|
-
(r) => r && typeof r.path === 'string' && typeof r.mi === 'number',
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function readPriorBaselineRows({ kind, absBaselinePath, fsImpl = fs }) {
|
|
150
|
-
const parsed = readBaselineJson(absBaselinePath, fsImpl);
|
|
151
|
-
if (!parsed) return null;
|
|
152
|
-
if (kind === 'crap') return readCrapPriorRows(parsed);
|
|
153
|
-
return readMaintainabilityPriorRows(parsed);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Compose the full Story #1974 write-side payload for a manual baseline
|
|
158
|
-
* CLI: read prior rows, resolve `--diff-scope`, log the scope decision,
|
|
159
|
-
* and return the four params (`prior`, `epsilon`, `scope`, plus the
|
|
160
|
-
* resolved `diffScope` for caller-side logging) that the CLI feeds into
|
|
161
|
-
* `writer.write({ ..., prior, epsilon, scope })`.
|
|
162
|
-
*
|
|
163
|
-
* Returns a flat record so each CLI can spread it into the writer call.
|
|
164
|
-
*
|
|
165
|
-
* @param {{
|
|
166
|
-
* kind: 'maintainability' | 'crap',
|
|
167
|
-
* absBaselinePath: string,
|
|
168
|
-
* epsilon: number,
|
|
169
|
-
* argv?: string[],
|
|
170
|
-
* cwd?: string,
|
|
171
|
-
* logger?: { info?: (msg: string) => void },
|
|
172
|
-
* logTag: string,
|
|
173
|
-
* }} args
|
|
174
|
-
* @returns {{
|
|
175
|
-
* prior: Array<object> | undefined,
|
|
176
|
-
* epsilon: number | undefined,
|
|
177
|
-
* scope: {mode: 'diff', files: Set<string>} | undefined,
|
|
178
|
-
* diffScope: {ref: string, files: Set<string>, scope: object} | null,
|
|
179
|
-
* }}
|
|
180
|
-
*/
|
|
181
|
-
export function buildWriterScopeArgs({
|
|
182
|
-
kind,
|
|
183
|
-
absBaselinePath,
|
|
184
|
-
epsilon,
|
|
185
|
-
argv = process.argv.slice(2),
|
|
186
|
-
cwd,
|
|
187
|
-
logger,
|
|
188
|
-
logTag,
|
|
189
|
-
}) {
|
|
190
|
-
const prior = readPriorBaselineRows({ kind, absBaselinePath });
|
|
191
|
-
const diffScope = resolveDiffScope({ argv, cwd });
|
|
192
|
-
if (diffScope && logger?.info) {
|
|
193
|
-
logger.info(
|
|
194
|
-
`${logTag} --diff-scope ${diffScope.ref}: ${diffScope.files.size} file(s) in scope; out-of-scope rows preserved verbatim.`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
prior: prior ?? undefined,
|
|
199
|
-
epsilon: prior ? epsilon : undefined,
|
|
200
|
-
scope: diffScope?.scope,
|
|
201
|
-
diffScope,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
@@ -26,11 +26,38 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
29
30
|
import path from 'node:path';
|
|
30
31
|
|
|
31
32
|
const DEFAULT_MIN_TOKENS = 50;
|
|
32
33
|
const DEFAULT_FORMATS = Object.freeze(['javascript']);
|
|
33
34
|
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve jscpd's `detectClones` lazily. The jscpd ESM entrypoint has a
|
|
39
|
+
* broken transitive `colors/safe` specifier under Node's strict ESM
|
|
40
|
+
* resolver, so we load the CJS build via `createRequire`. Isolated behind a
|
|
41
|
+
* function so the rest of the module stays import-pure and testable — no
|
|
42
|
+
* importer of this file pays the jscpd load unless it actually scans.
|
|
43
|
+
*
|
|
44
|
+
* Lives here rather than in `update-duplication-baseline.js` since Story
|
|
45
|
+
* #4944: the refresh service's default duplication scorer needs the same
|
|
46
|
+
* seam, and a second copy in the CLI would be the classic "two
|
|
47
|
+
* implementations of one probe" divergence.
|
|
48
|
+
*
|
|
49
|
+
* @returns {(opts: object) => Promise<Array<object>>}
|
|
50
|
+
*/
|
|
51
|
+
export function resolveDetectClones() {
|
|
52
|
+
const jscpd = require('jscpd');
|
|
53
|
+
if (typeof jscpd.detectClones !== 'function') {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"[Duplication] jscpd.detectClones is not available — run 'npm install'",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return jscpd.detectClones;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Normalise a jscpd `sourceId` (or any path) to a canonical POSIX
|
|
36
63
|
* repo-relative path. jscpd already emits cwd-relative paths, but a future
|
|
@@ -29,10 +29,15 @@
|
|
|
29
29
|
// loader treats this as a normal "no baseline at base ref" case
|
|
30
30
|
// and returns `null`. Callers branch on `result === null` rather
|
|
31
31
|
// than catching an error.
|
|
32
|
-
// - Any other
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// baseline". A bad ref is a
|
|
32
|
+
// - Any other outcome (bad ref, corrupted repo, git binary missing, or
|
|
33
|
+
// the child killed by signal so `status` is `null`): thrown so the
|
|
34
|
+
// dispatcher can surface a config-error exit code rather than
|
|
35
|
+
// silently treating the failure as "no baseline". A bad ref is a
|
|
36
|
+
// config bug, not a missing file.
|
|
37
|
+
//
|
|
38
|
+
// That two-way split is load-bearing (Story #4914): `null` means and only
|
|
39
|
+
// means "path absent at ref". Callers may therefore treat a throw as a
|
|
40
|
+
// hard failure without having to re-diagnose it.
|
|
36
41
|
|
|
37
42
|
import { spawnSync } from 'node:child_process';
|
|
38
43
|
|
|
@@ -47,6 +52,21 @@ import { spawnSync } from 'node:child_process';
|
|
|
47
52
|
|
|
48
53
|
const DEFAULT_MAX_ENTRIES = 64;
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Story #4914 — explicit stdout ceiling for every git read in this module.
|
|
57
|
+
*
|
|
58
|
+
* `child_process.spawnSync` defaults `maxBuffer` to 1 MB. A committed
|
|
59
|
+
* baseline legitimately grows past that (a real consumer's crap baseline
|
|
60
|
+
* measured 1,178,910 bytes), at which point the child is killed —
|
|
61
|
+
* `status: null`, `signal: 'SIGTERM'`, `error.code: 'ENOBUFS'` — and the
|
|
62
|
+
* read fails for a reason that has nothing to do with the repository.
|
|
63
|
+
*
|
|
64
|
+
* 64 MB is not a new number: it is the bound already used at
|
|
65
|
+
* `run-test-profile.js:87`, `audit-baselines/trend.js:61` and
|
|
66
|
+
* `audit-baselines/weights.js:65`. This module was the outlier.
|
|
67
|
+
*/
|
|
68
|
+
const MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
69
|
+
|
|
50
70
|
let _spawnSync = spawnSync;
|
|
51
71
|
let _cache = new Map();
|
|
52
72
|
let _maxEntries = DEFAULT_MAX_ENTRIES;
|
|
@@ -165,6 +185,7 @@ export function readBaseFromGit(ref, file, opts = {}) {
|
|
|
165
185
|
encoding: 'utf-8',
|
|
166
186
|
shell: false,
|
|
167
187
|
env: cleanGitEnv(),
|
|
188
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
168
189
|
});
|
|
169
190
|
|
|
170
191
|
// `child_process.spawnSync` returns `status: null` when the child died
|
|
@@ -232,6 +253,7 @@ export function readRangeSubjectsTouchingFile(baseRef, file, opts = {}) {
|
|
|
232
253
|
encoding: 'utf-8',
|
|
233
254
|
shell: false,
|
|
234
255
|
env: cleanGitEnv(),
|
|
256
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
235
257
|
},
|
|
236
258
|
);
|
|
237
259
|
} catch {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
COORDINATE_TRANSPILED,
|
|
20
20
|
deriveFixGuidance,
|
|
21
21
|
} from '../../crap-engine.js';
|
|
22
|
+
import { isAnonymousMethodLabel } from '../../crap-method-identity.js';
|
|
22
23
|
import { getCrapBaseline } from '../../crap-utils.js';
|
|
23
24
|
import { loadBaseline } from '../../gates/baseline-store.js';
|
|
24
25
|
import { Logger } from '../../Logger.js';
|
|
@@ -95,15 +96,23 @@ export function kernelVersion() {
|
|
|
95
96
|
* worse, phantom passes.
|
|
96
97
|
*
|
|
97
98
|
* The stamp makes the boundary explicit and fails closed. Bump it whenever
|
|
98
|
-
* the coverage join, the line coordinate system,
|
|
99
|
-
*
|
|
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.
|
|
100
109
|
*
|
|
101
110
|
* Deliberately module-local: `envelopeExtras()` is the single production door
|
|
102
111
|
* to this value, so exporting the bare constant would add a second entry
|
|
103
112
|
* point that nothing in production reaches. Callers and tests that need the
|
|
104
113
|
* string read it off `envelopeExtras().scoringSemantics`.
|
|
105
114
|
*/
|
|
106
|
-
const SCORING_SEMANTICS = '
|
|
115
|
+
const SCORING_SEMANTICS = 'method-identity-v3';
|
|
107
116
|
|
|
108
117
|
/**
|
|
109
118
|
* Envelope-level stamps this kind contributes beyond the shared envelope
|
|
@@ -116,12 +125,22 @@ const SCORING_SEMANTICS = 'coverage-join-v2';
|
|
|
116
125
|
* the stamp on disk the `ts-transpiler-drift` axis had nothing to compare and
|
|
117
126
|
* passed vacuously.
|
|
118
127
|
*
|
|
119
|
-
*
|
|
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}}
|
|
120
138
|
*/
|
|
121
139
|
export function envelopeExtras() {
|
|
122
140
|
return {
|
|
123
141
|
scoringSemantics: SCORING_SEMANTICS,
|
|
124
142
|
tsTranspilerVersion: resolveTsTranspilerVersion(),
|
|
143
|
+
provenanceStamped: true,
|
|
125
144
|
};
|
|
126
145
|
}
|
|
127
146
|
|
|
@@ -144,6 +163,12 @@ export function projectRow(row) {
|
|
|
144
163
|
if (row.coordinateSystem === COORDINATE_TRANSPILED) {
|
|
145
164
|
projected.coordinateSystem = COORDINATE_TRANSPILED;
|
|
146
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
|
+
}
|
|
147
172
|
return projected;
|
|
148
173
|
}
|
|
149
174
|
|
|
@@ -636,6 +661,17 @@ export function assessComparisonBasis(compareResult, opts = {}) {
|
|
|
636
661
|
* and `tsTranspilerVersion` drift to **warn**, not fail; `escomplexVersion`
|
|
637
662
|
* mismatch continues to fail closed.
|
|
638
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
|
+
|
|
639
675
|
export const CRAP_COMPAT_AXES = [
|
|
640
676
|
// Universal axes hoisted into envelope.js (Story #2467, Task #2492). The
|
|
641
677
|
// missing-baseline and kernel-drift checks live in exactly one place;
|
|
@@ -661,9 +697,7 @@ export const CRAP_COMPAT_AXES = [
|
|
|
661
697
|
`[CRAP] scoring semantics changed: baseline=${stamped ?? '<unstamped>'} ` +
|
|
662
698
|
`running=${SCORING_SEMANTICS}. Rows scored by the previous per-method ` +
|
|
663
699
|
'coverage join are not comparable to rows scored by the current one, ' +
|
|
664
|
-
|
|
665
|
-
"'npm run test:coverage' then 'npm run crap:update -- --full-scope' " +
|
|
666
|
-
"and commit the result with a 'baseline-refresh:' subject."
|
|
700
|
+
`so this baseline cannot be compared. ${RESEED_REMEDY}`
|
|
667
701
|
);
|
|
668
702
|
},
|
|
669
703
|
},
|
|
@@ -688,10 +722,65 @@ export const CRAP_COMPAT_AXES = [
|
|
|
688
722
|
`[CRAP] tsTranspilerVersion changed: baseline=${baselineTs} running=${runningTsTranspilerVersion}. ` +
|
|
689
723
|
"A TS row's startLine is an original-source coordinate only because the transpiler's " +
|
|
690
724
|
'sourcemap said so, and that coordinate is half the row identity key — so rows scored ' +
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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;
|
|
776
|
+
return (
|
|
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}`
|
|
695
784
|
);
|
|
696
785
|
},
|
|
697
786
|
},
|
|
@@ -742,15 +831,23 @@ export function evaluateBaselineCompatibility(ctx) {
|
|
|
742
831
|
|
|
743
832
|
/**
|
|
744
833
|
* The compat axes a *loaded* envelope can be judged against on its own,
|
|
745
|
-
* without a second baseline to diff.
|
|
746
|
-
*
|
|
747
|
-
*
|
|
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.
|
|
748
840
|
*
|
|
749
841
|
* `escomplex-mismatch` and `kernel-drift` stay out — the v2 envelope carries
|
|
750
842
|
* no `escomplexVersion`, so that axis would compare `undefined` to `undefined`
|
|
751
843
|
* and pass vacuously, which is worse than not running it.
|
|
752
844
|
*/
|
|
753
|
-
const LOADED_ENVELOPE_AXES = [
|
|
845
|
+
const LOADED_ENVELOPE_AXES = [
|
|
846
|
+
'scoring-semantics-drift',
|
|
847
|
+
'ts-transpiler-drift',
|
|
848
|
+
'provenance-unstamped',
|
|
849
|
+
'anon-identity-unstamped',
|
|
850
|
+
];
|
|
754
851
|
|
|
755
852
|
/**
|
|
756
853
|
* Kind-module hook (Story #4775, extended by Story #4866): judge a *loaded*
|