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
|
@@ -182,6 +182,56 @@ function validate(kind, parsed, sourceHint) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Internal: the ONE narrowing projection every loaded envelope passes through.
|
|
187
|
+
*
|
|
188
|
+
* `load` and `loadFile` reach a validated `parsed` envelope by different routes
|
|
189
|
+
* — one resolves the path from config, the other infers the kind from
|
|
190
|
+
* `$schema` — but the shape they hand back is the same narrow contract, so it
|
|
191
|
+
* is built here rather than written out at each exit.
|
|
192
|
+
*
|
|
193
|
+
* **That single-sourcing is the point, not tidiness.** The projection is an
|
|
194
|
+
* ALLOW-LIST: a field absent from it is silently dropped, and the envelope
|
|
195
|
+
* stamps below are read off the LOADED object by compat axes that fail closed
|
|
196
|
+
* when a stamp reads `undefined`. While the list was duplicated at the two
|
|
197
|
+
* exits, adding a stamp to one and not the other produced a gate that rejected
|
|
198
|
+
* every baseline in the repo for a stamp that was present on disk — which is
|
|
199
|
+
* exactly what happened to `provenanceStamped` between Story #4901 and this
|
|
200
|
+
* fix. One list means a new stamp cannot be half-added.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} kind
|
|
203
|
+
* @param {object} parsed A validated baseline envelope.
|
|
204
|
+
* @returns {{ rollup: object, rows: Array<object>, kernelVersion: string, generatedAt: string }}
|
|
205
|
+
*/
|
|
206
|
+
function shapeEnvelope(kind, parsed) {
|
|
207
|
+
const rows = Array.isArray(parsed.rows)
|
|
208
|
+
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
209
|
+
: [];
|
|
210
|
+
return {
|
|
211
|
+
rollup: parsed.rollup ?? { '*': {} },
|
|
212
|
+
rows,
|
|
213
|
+
kernelVersion: parsed.kernelVersion,
|
|
214
|
+
generatedAt: parsed.generatedAt,
|
|
215
|
+
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
216
|
+
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
217
|
+
// dropping it here would make every baseline look unstamped and fail the
|
|
218
|
+
// whole repo closed on a stamp that is actually present on disk.
|
|
219
|
+
scoringSemantics: parsed.scoringSemantics,
|
|
220
|
+
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
221
|
+
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
222
|
+
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
223
|
+
// the exact deadness this Story exists to end.
|
|
224
|
+
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
225
|
+
// Story #4901 — the coordinate-provenance marker, and the stamp that proved
|
|
226
|
+
// the warnings above were not hypothetical. Its `provenance-unstamped` axis
|
|
227
|
+
// keys on `!== true`, so while this line was missing the axis read
|
|
228
|
+
// `undefined` off every envelope and rejected each one with "baseline
|
|
229
|
+
// predates coordinate-provenance stamping" — un-satisfiable, because
|
|
230
|
+
// re-deriving the baseline writes the marker the reader then drops.
|
|
231
|
+
provenanceStamped: parsed.provenanceStamped,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
185
235
|
/**
|
|
186
236
|
* Internal: parse + validate + canonicalise. Used by both `load` and
|
|
187
237
|
* `loadFile`.
|
|
@@ -208,25 +258,7 @@ function readAndShape(kind, absolutePath) {
|
|
|
208
258
|
);
|
|
209
259
|
}
|
|
210
260
|
validate(kind, parsed, absolutePath);
|
|
211
|
-
|
|
212
|
-
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
213
|
-
: [];
|
|
214
|
-
return {
|
|
215
|
-
rollup: parsed.rollup ?? { '*': {} },
|
|
216
|
-
rows,
|
|
217
|
-
kernelVersion: parsed.kernelVersion,
|
|
218
|
-
generatedAt: parsed.generatedAt,
|
|
219
|
-
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
220
|
-
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
221
|
-
// dropping it here would make every baseline look unstamped and fail the
|
|
222
|
-
// whole repo closed on a stamp that is actually present on disk.
|
|
223
|
-
scoringSemantics: parsed.scoringSemantics,
|
|
224
|
-
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
225
|
-
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
226
|
-
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
227
|
-
// the exact deadness this Story exists to end.
|
|
228
|
-
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
229
|
-
};
|
|
261
|
+
return shapeEnvelope(kind, parsed);
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
/**
|
|
@@ -306,25 +338,7 @@ export function loadFile(absolutePath, opts = {}) {
|
|
|
306
338
|
);
|
|
307
339
|
}
|
|
308
340
|
validate(kind, parsed, absolutePath);
|
|
309
|
-
|
|
310
|
-
? parsed.rows.map((row) => canonicaliseRow(kind, row))
|
|
311
|
-
: [];
|
|
312
|
-
return {
|
|
313
|
-
rollup: parsed.rollup ?? { '*': {} },
|
|
314
|
-
rows,
|
|
315
|
-
kernelVersion: parsed.kernelVersion,
|
|
316
|
-
generatedAt: parsed.generatedAt,
|
|
317
|
-
// Story #4775 — carry the per-kind scoring-semantics stamp through the
|
|
318
|
-
// narrowing. The gate's compat check reads it off the LOADED envelope, so
|
|
319
|
-
// dropping it here would make every baseline look unstamped and fail the
|
|
320
|
-
// whole repo closed on a stamp that is actually present on disk.
|
|
321
|
-
scoringSemantics: parsed.scoringSemantics,
|
|
322
|
-
// Story #4866 — same contract for the transpiler stamp. Dropping it here
|
|
323
|
-
// would leave the ts-transpiler compat axis reachable but blind: it would
|
|
324
|
-
// read `undefined` off every loaded envelope and pass vacuously, which is
|
|
325
|
-
// the exact deadness this Story exists to end.
|
|
326
|
-
tsTranspilerVersion: parsed.tsTranspilerVersion,
|
|
327
|
-
};
|
|
341
|
+
return shapeEnvelope(kind, parsed);
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
export const _internals = Object.freeze({
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
* (Story #2197, Epic #2173).
|
|
4
4
|
*
|
|
5
5
|
* `refreshBaseline()` is the single funnel through which every baseline
|
|
6
|
-
* regeneration (maintainability, crap, coverage) must flow.
|
|
7
|
-
* previously assembled their own envelopes and called
|
|
8
|
-
* MUST migrate to this entry point — Stories 3/4/5 of
|
|
9
|
-
* migration; Story #2197 only lands the service surface
|
|
6
|
+
* regeneration (maintainability, crap, coverage, duplication) must flow.
|
|
7
|
+
* Callers that previously assembled their own envelopes and called
|
|
8
|
+
* `fs.writeFileSync` MUST migrate to this entry point — Stories 3/4/5 of
|
|
9
|
+
* Epic #2173 do that migration; Story #2197 only lands the service surface
|
|
10
|
+
* and tests. Story #4944 migrated the last holdout, `duplication`.
|
|
10
11
|
*
|
|
11
12
|
* The service is **scoring-agnostic**: it does not itself walk the
|
|
12
|
-
* filesystem to compute MI / CRAP / coverage scores. Scoring
|
|
13
|
-
* by the per-kind default scorers resolved lazily via
|
|
13
|
+
* filesystem to compute MI / CRAP / coverage / duplication scores. Scoring
|
|
14
|
+
* is provided by the per-kind default scorers resolved lazily via
|
|
15
|
+
* `resolveDefaultScorer`
|
|
14
16
|
* (built with the project config resolved against `cwd`) and, in tests or
|
|
15
17
|
* production wiring, injected via the `scorer` option for hermetic
|
|
16
18
|
* determinism. The service is the policy layer:
|
|
@@ -29,7 +31,8 @@
|
|
|
29
31
|
* Public API (Task #2203, AC-1 / AC-2 / AC-7):
|
|
30
32
|
*
|
|
31
33
|
* refreshBaseline({
|
|
32
|
-
* kind, // 'maintainability' | 'crap' | 'coverage'
|
|
34
|
+
* kind, // 'maintainability' | 'crap' | 'coverage'
|
|
35
|
+
* // | 'duplication' REQUIRED
|
|
33
36
|
* baseRef, // git ref to diff against; default 'origin/main'
|
|
34
37
|
* headRef, // git ref under inspection; default 'HEAD'
|
|
35
38
|
* scopeFiles, // Array<string> | null
|
|
@@ -62,8 +65,9 @@
|
|
|
62
65
|
*
|
|
63
66
|
* Acceptance contract:
|
|
64
67
|
*
|
|
65
|
-
* AC-1: All callers that produce a maintainability/crap/coverage
|
|
66
|
-
* go through refreshBaseline(). Enforced by
|
|
68
|
+
* AC-1: All callers that produce a maintainability/crap/coverage/
|
|
69
|
+
* duplication baseline go through refreshBaseline(). Enforced by
|
|
70
|
+
* the Task #2208 invariant.
|
|
67
71
|
* AC-2: scopeFiles=null && !fullScope -> diff-derived scope (Task #2207).
|
|
68
72
|
* AC-4: Out-of-scope rows + their updatedAt fields are preserved byte-
|
|
69
73
|
* for-byte (Task #2209).
|
|
@@ -93,6 +97,7 @@ import {
|
|
|
93
97
|
isIgnoredByGlobs as isIgnoredByGlobsMi,
|
|
94
98
|
scanDirectory as scanDirectoryMi,
|
|
95
99
|
} from '../maintainability-utils.js';
|
|
100
|
+
import { resolveDetectClones, scanDuplication } from './duplication-scanner.js';
|
|
96
101
|
import { filterExcludedRows } from './kinds/maintainability.js';
|
|
97
102
|
import { canonicalizeBaselinePath } from './path-canon.js';
|
|
98
103
|
import {
|
|
@@ -108,7 +113,12 @@ const execFileAsync = promisify(nodeExecFile);
|
|
|
108
113
|
* Kinds the refresh service knows how to dispatch. Stays in lockstep with
|
|
109
114
|
* the per-kind modules under `.agents/scripts/lib/baselines/kinds/`.
|
|
110
115
|
*/
|
|
111
|
-
const SUPPORTED_KINDS = Object.freeze([
|
|
116
|
+
const SUPPORTED_KINDS = Object.freeze([
|
|
117
|
+
'maintainability',
|
|
118
|
+
'crap',
|
|
119
|
+
'coverage',
|
|
120
|
+
'duplication',
|
|
121
|
+
]);
|
|
112
122
|
|
|
113
123
|
/**
|
|
114
124
|
* Per-kind file-extension predicate for diff-scope derivation (Task #2207).
|
|
@@ -125,6 +135,12 @@ const KIND_FILE_PREDICATES = Object.freeze({
|
|
|
125
135
|
maintainability: (p) => /\.(?:m?[jt]sx?)$/i.test(p),
|
|
126
136
|
crap: (p) => /\.(?:m?[jt]sx?)$/i.test(p),
|
|
127
137
|
coverage: (p) => /\.(?:m?[jt]sx?)$/i.test(p),
|
|
138
|
+
// Duplication shares the source-extension filter deliberately. jscpd is
|
|
139
|
+
// configured for the `javascript` format only, so a changed `.ts` file
|
|
140
|
+
// admitted here simply yields no row on either side of the merge —
|
|
141
|
+
// over-inclusive is inert, under-inclusive would silently pin a changed
|
|
142
|
+
// file's prior row. Erring wide is the safe direction.
|
|
143
|
+
duplication: (p) => /\.(?:m?[jt]sx?)$/i.test(p),
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
/**
|
|
@@ -338,6 +354,7 @@ const KIND_SCORER_BUILDERS = Object.freeze({
|
|
|
338
354
|
maintainability: buildDefaultMaintainabilityScorer,
|
|
339
355
|
crap: buildDefaultCrapScorer,
|
|
340
356
|
coverage: buildDefaultCoverageScorer,
|
|
357
|
+
duplication: buildDefaultDuplicationScorer,
|
|
341
358
|
});
|
|
342
359
|
|
|
343
360
|
/**
|
|
@@ -384,7 +401,7 @@ export function resolveDefaultScorer(kind, { cwd } = {}) {
|
|
|
384
401
|
* so callers (and tests) can assert what actually got scored.
|
|
385
402
|
*
|
|
386
403
|
* @param {{
|
|
387
|
-
* kind: 'maintainability' | 'crap' | 'coverage',
|
|
404
|
+
* kind: 'maintainability' | 'crap' | 'coverage' | 'duplication',
|
|
388
405
|
* baseRef?: string,
|
|
389
406
|
* headRef?: string,
|
|
390
407
|
* scopeFiles?: string[] | null,
|
|
@@ -745,3 +762,44 @@ function validateOptions({ kind, scopeFiles, fullScope, writePath }) {
|
|
|
745
762
|
);
|
|
746
763
|
}
|
|
747
764
|
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Build the default duplication scorer.
|
|
768
|
+
*
|
|
769
|
+
* **This scorer always scans the whole target tree, in every scope mode.**
|
|
770
|
+
* That is not an oversight — duplication is the only kind here whose metric
|
|
771
|
+
* is *pairwise*: a clone is a relationship between two files, and jscpd can
|
|
772
|
+
* only report it if both sides are in the corpus it was handed. Narrowing the
|
|
773
|
+
* scan to the diff would drop every clone between a changed file and an
|
|
774
|
+
* unchanged one, i.e. exactly the duplication a refactor is most likely to
|
|
775
|
+
* introduce. So the `files` argument is intentionally unused: scope narrowing
|
|
776
|
+
* for this kind is a **write-side** concern, applied by the service handing
|
|
777
|
+
* `scope` to `writer.write()`, where `mergeRowsByScope` keeps the freshly
|
|
778
|
+
* scanned rows for in-scope files and preserves the prior rows verbatim for
|
|
779
|
+
* everything else.
|
|
780
|
+
*
|
|
781
|
+
* Reads `targetDirs` / `ignoreGlobs` off `gates.duplication`, not off a
|
|
782
|
+
* flattened accessor: `resolveQuality` never lifted duplication into the
|
|
783
|
+
* legacy bag (the kind post-dates it), so `quality.duplication` is always
|
|
784
|
+
* `undefined` and reading it would silently scan nothing.
|
|
785
|
+
*
|
|
786
|
+
* @param {{ cwd: string, config?: object, quality?: object, detect?: (opts: object) => Promise<object[]> }} opts
|
|
787
|
+
* @returns {(files: string[], opts: object) => Promise<object[]>}
|
|
788
|
+
*/
|
|
789
|
+
function buildDefaultDuplicationScorer({ cwd, config, quality, detect } = {}) {
|
|
790
|
+
const dupCfg =
|
|
791
|
+
resolveQualityBlock({ quality, config })?.gates?.duplication ?? {};
|
|
792
|
+
const targetDirs = Array.isArray(dupCfg.targetDirs) ? dupCfg.targetDirs : [];
|
|
793
|
+
const ignoreGlobs = Array.isArray(dupCfg.ignoreGlobs)
|
|
794
|
+
? dupCfg.ignoreGlobs
|
|
795
|
+
: [];
|
|
796
|
+
return async (_files, opts) => {
|
|
797
|
+
const effectiveCwd = opts?.cwd ?? cwd ?? process.cwd();
|
|
798
|
+
return scanDuplication({
|
|
799
|
+
targetDirs,
|
|
800
|
+
cwd: effectiveCwd,
|
|
801
|
+
ignoreGlobs,
|
|
802
|
+
detect: detect ?? resolveDetectClones(),
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
}
|
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
// .agents/scripts/lib/baselines/scope.js
|
|
2
2
|
//
|
|
3
|
-
// Story #1962 / Task #1970 —
|
|
4
|
-
// `check-baselines.js` dispatcher
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// Story #1962 / Task #1970 — the ScopeResolution helper behind the
|
|
4
|
+
// `check-baselines.js` dispatcher: `resolveDispatchScope` in
|
|
5
|
+
// `lib/orchestration/check-baselines/phases/compare.js` is its one
|
|
6
|
+
// caller. Routing the read side's scope decision through a single pure
|
|
7
|
+
// function keeps the precedence rules from being re-implemented per gate.
|
|
8
|
+
//
|
|
9
|
+
// It is NOT the writers' resolver. `lib/baselines/refresh-service.js`
|
|
10
|
+
// defines its own private `resolveScope` (Story #3658) over a different
|
|
11
|
+
// input set, and the header here used to claim otherwise — a claim that
|
|
12
|
+
// made the read/write pair look coupled when it is not. Corrected in
|
|
13
|
+
// Story #4922.
|
|
9
14
|
//
|
|
10
15
|
// The resolver is intentionally pure — it takes already-extracted
|
|
11
|
-
// inputs and returns a frozen ScopeResolution.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// being silently re-implemented at every call site.
|
|
16
|
+
// inputs and returns a frozen ScopeResolution. Env reading and config
|
|
17
|
+
// loading happen in the caller; that keeps this module trivially
|
|
18
|
+
// testable.
|
|
15
19
|
//
|
|
16
20
|
// Precedence (highest → lowest):
|
|
17
21
|
//
|
|
18
|
-
// 1.
|
|
19
|
-
// Operator-typed beats anything in env/config. A CLI override of
|
|
20
|
-
// `--full-scope` wins even if the config says `'diff'`.
|
|
21
|
-
// 2. Environment — `BASELINE_SCOPE` ('full' | 'diff') and
|
|
22
|
+
// 1. Environment — `BASELINE_SCOPE` ('full' | 'diff') and
|
|
22
23
|
// `BASELINE_REF` (any git ref). The dispatcher reads these from
|
|
23
|
-
// `process.env` and forwards via `
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// 3. Config — `configScope` ('full' | 'diff') and `configRef` (any
|
|
24
|
+
// `process.env` and forwards via `envScope` / `envRef` so the
|
|
25
|
+
// resolver itself never touches process state. CI sets these.
|
|
26
|
+
// 2. Config — `configScope` ('full' | 'diff') and `configRef` (any
|
|
27
27
|
// git ref) from `delivery.quality.gateScoping` in `.agentrc.json`.
|
|
28
|
-
//
|
|
28
|
+
// 3. Default — `mode='diff'` against `ref='main'`. This is the
|
|
29
29
|
// framework-wide fallback when nothing else is configured.
|
|
30
30
|
//
|
|
31
|
+
// Story #4922 removed a fourth, highest-precedence layer: a
|
|
32
|
+
// `cliFlags.fullScope` / `cliFlags.changedSinceRef` operator override,
|
|
33
|
+
// plus the `cliFlags.changedFiles` → `files` plumbing that fed it. No
|
|
34
|
+
// production caller ever populated any of the three — only this module's
|
|
35
|
+
// own unit tests did — so the layer's only effect was to advertise a
|
|
36
|
+
// `--full-scope` / `--changed-since` contract that `check-baselines.js`
|
|
37
|
+
// does not implement, and a `files` set that no consumer read. Operators
|
|
38
|
+
// who need full scope set `BASELINE_SCOPE=full`, which is what CI does.
|
|
39
|
+
// (`mergeRowsByScope` below still takes a `files`-bearing scope — that
|
|
40
|
+
// one comes from the refresh service's own resolver, not from here.)
|
|
41
|
+
//
|
|
31
42
|
// Missing-ref fallback: when the resolved mode is `'diff'` but no ref
|
|
32
43
|
// is supplied at any layer, the resolver falls back to `'main'` rather
|
|
33
44
|
// than producing a half-resolved scope with `ref=null`. The dispatcher
|
|
@@ -45,17 +56,8 @@
|
|
|
45
56
|
// kind: string, // echoed back for caller convenience
|
|
46
57
|
// mode: 'full' | 'diff',
|
|
47
58
|
// ref: string | null, // null in full mode; ref string in diff mode
|
|
48
|
-
// files: Set<string>, // empty Set in full mode (sentinel for "all")
|
|
49
59
|
// source: string, // which layer won (debug / friction signal)
|
|
50
60
|
// }
|
|
51
|
-
//
|
|
52
|
-
// `files` is intentionally a Set rather than an Array — callers
|
|
53
|
-
// repeatedly check membership during per-row filtering, and Set lookup
|
|
54
|
-
// is O(1). An empty Set in `'full'` mode means "no filter applies".
|
|
55
|
-
// A non-empty Set in `'diff'` mode means "only these paths are in
|
|
56
|
-
// scope" (the dispatcher pre-computes them via `git diff --name-only`
|
|
57
|
-
// and forwards via `cliFlags.changedFiles`); when omitted, the writer
|
|
58
|
-
// is expected to compute the diff itself against `ref`.
|
|
59
61
|
|
|
60
62
|
const VALID_MODES = new Set(['full', 'diff']);
|
|
61
63
|
const DEFAULT_DIFF_REF = 'main';
|
|
@@ -84,45 +86,19 @@ function asMode(v) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
|
-
*
|
|
88
|
-
* empty Set when the input is missing or empty.
|
|
89
|
-
*
|
|
90
|
-
* @param {unknown} v
|
|
91
|
-
* @returns {Set<string>}
|
|
92
|
-
*/
|
|
93
|
-
function asFilesSet(v) {
|
|
94
|
-
if (v instanceof Set) {
|
|
95
|
-
return new Set(
|
|
96
|
-
Array.from(v).filter((f) => typeof f === 'string' && f.length > 0),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (Array.isArray(v)) {
|
|
100
|
-
return new Set(v.filter((f) => typeof f === 'string' && f.length > 0));
|
|
101
|
-
}
|
|
102
|
-
return new Set();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolve a scope against the layered precedence (CLI > env > config >
|
|
89
|
+
* Resolve a scope against the layered precedence (env > config >
|
|
107
90
|
* default). Pure; no I/O.
|
|
108
91
|
*
|
|
109
92
|
* @param {object} input
|
|
110
|
-
* @param {string} input.kind
|
|
93
|
+
* @param {string} input.kind - Baseline kind (e.g. `'lint'`).
|
|
111
94
|
* @param {string} [input.configScope] - `'full'` | `'diff'` from agentrc.
|
|
112
95
|
* @param {string} [input.configRef] - Diff ref from agentrc.
|
|
113
|
-
* @param {
|
|
114
|
-
* @param {
|
|
115
|
-
* @param {string} [input.cliFlags.changedSinceRef] - `--changed-since <ref>`.
|
|
116
|
-
* @param {string} [input.cliFlags.envScope] - From `BASELINE_SCOPE`.
|
|
117
|
-
* @param {string} [input.cliFlags.envRef] - From `BASELINE_REF`.
|
|
118
|
-
* @param {Iterable<string>} [input.cliFlags.changedFiles]
|
|
119
|
-
* Pre-computed diff paths (when caller already ran `git diff
|
|
120
|
-
* --name-only`). Becomes `files`; only meaningful in `'diff'` mode.
|
|
96
|
+
* @param {string} [input.envScope] - From `BASELINE_SCOPE`.
|
|
97
|
+
* @param {string} [input.envRef] - From `BASELINE_REF`.
|
|
121
98
|
* @returns {{
|
|
122
99
|
* kind: string,
|
|
123
100
|
* mode: 'full' | 'diff',
|
|
124
101
|
* ref: string | null,
|
|
125
|
-
* files: Set<string>,
|
|
126
102
|
* source: string,
|
|
127
103
|
* }}
|
|
128
104
|
*/
|
|
@@ -131,59 +107,34 @@ export function resolveScope(input = {}) {
|
|
|
131
107
|
typeof input.kind === 'string' && input.kind.length > 0
|
|
132
108
|
? input.kind
|
|
133
109
|
: 'unknown';
|
|
134
|
-
const cli = input.cliFlags ?? {};
|
|
135
|
-
|
|
136
|
-
// ---- Layer 1: CLI flags (highest precedence) -------------------------
|
|
137
|
-
if (cli.fullScope === true) {
|
|
138
|
-
return Object.freeze({
|
|
139
|
-
kind,
|
|
140
|
-
mode: 'full',
|
|
141
|
-
ref: null,
|
|
142
|
-
files: new Set(),
|
|
143
|
-
source: 'cli:--full-scope',
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
const cliRef = asNonEmptyString(cli.changedSinceRef);
|
|
147
|
-
if (cliRef) {
|
|
148
|
-
return Object.freeze({
|
|
149
|
-
kind,
|
|
150
|
-
mode: 'diff',
|
|
151
|
-
ref: cliRef,
|
|
152
|
-
files: asFilesSet(cli.changedFiles),
|
|
153
|
-
source: 'cli:--changed-since',
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
110
|
|
|
157
|
-
// ---- Layer
|
|
158
|
-
const envMode = asMode(
|
|
111
|
+
// ---- Layer 1: Environment (extracted by the caller) ------------------
|
|
112
|
+
const envMode = asMode(input.envScope);
|
|
159
113
|
if (envMode === 'full') {
|
|
160
114
|
return Object.freeze({
|
|
161
115
|
kind,
|
|
162
116
|
mode: 'full',
|
|
163
117
|
ref: null,
|
|
164
|
-
files: new Set(),
|
|
165
118
|
source: 'env:BASELINE_SCOPE=full',
|
|
166
119
|
});
|
|
167
120
|
}
|
|
168
|
-
const envRef = asNonEmptyString(
|
|
121
|
+
const envRef = asNonEmptyString(input.envRef);
|
|
169
122
|
if (envMode === 'diff' || envRef) {
|
|
170
123
|
return Object.freeze({
|
|
171
124
|
kind,
|
|
172
125
|
mode: 'diff',
|
|
173
126
|
ref: envRef ?? DEFAULT_DIFF_REF,
|
|
174
|
-
files: asFilesSet(cli.changedFiles),
|
|
175
127
|
source: envRef ? 'env:BASELINE_REF' : 'env:BASELINE_SCOPE=diff',
|
|
176
128
|
});
|
|
177
129
|
}
|
|
178
130
|
|
|
179
|
-
// ---- Layer
|
|
131
|
+
// ---- Layer 2: Config (delivery.quality.gateScoping) ------------------
|
|
180
132
|
const cfgMode = asMode(input.configScope);
|
|
181
133
|
if (cfgMode === 'full') {
|
|
182
134
|
return Object.freeze({
|
|
183
135
|
kind,
|
|
184
136
|
mode: 'full',
|
|
185
137
|
ref: null,
|
|
186
|
-
files: new Set(),
|
|
187
138
|
source: 'config:gateScoping.scope=full',
|
|
188
139
|
});
|
|
189
140
|
}
|
|
@@ -193,19 +144,17 @@ export function resolveScope(input = {}) {
|
|
|
193
144
|
kind,
|
|
194
145
|
mode: 'diff',
|
|
195
146
|
ref: cfgRef ?? DEFAULT_DIFF_REF,
|
|
196
|
-
files: asFilesSet(cli.changedFiles),
|
|
197
147
|
source: cfgRef
|
|
198
148
|
? 'config:gateScoping.diffRef'
|
|
199
149
|
: 'config:gateScoping.scope=diff',
|
|
200
150
|
});
|
|
201
151
|
}
|
|
202
152
|
|
|
203
|
-
// ---- Layer
|
|
153
|
+
// ---- Layer 3: Default ------------------------------------------------
|
|
204
154
|
return Object.freeze({
|
|
205
155
|
kind,
|
|
206
156
|
mode: 'diff',
|
|
207
157
|
ref: DEFAULT_DIFF_REF,
|
|
208
|
-
files: asFilesSet(cli.changedFiles),
|
|
209
158
|
source: 'default',
|
|
210
159
|
});
|
|
211
160
|
}
|
|
@@ -261,21 +261,26 @@ export function writeFile(absPath, envelope, opts = {}) {
|
|
|
261
261
|
// across runs and platforms. Per-kind row keys retain their natural
|
|
262
262
|
// declaration order; the row sort is done by `sortRows()`.
|
|
263
263
|
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
264
|
+
// The projection is deliberately explicit (Story #4775): a per-kind envelope
|
|
265
|
+
// stamp reaches disk only if it is named here, so an unlisted one is present
|
|
266
|
+
// in memory, passes validation, and is absent from the file it exists to
|
|
267
|
+
// protect. That is not hypothetical — Story #4901 added `provenanceStamped`
|
|
268
|
+
// to `envelopeExtras()` and not to this list, and every baseline written
|
|
269
|
+
// between then and Story #4969 lost it at this boundary, leaving the
|
|
270
|
+
// `provenance-unstamped` axis reading an absence its own writer had
|
|
271
|
+
// manufactured.
|
|
272
|
+
//
|
|
273
|
+
// A stamp a kind does not set is simply `undefined`, and `JSON.stringify`
|
|
274
|
+
// omits an undefined-valued key — so naming all three unconditionally emits
|
|
275
|
+
// exactly what the per-key `undefined` guards used to, with nothing to
|
|
276
|
+
// half-apply when the next stamp is added.
|
|
269
277
|
const canonical = {
|
|
270
278
|
$schema: envelope.$schema,
|
|
271
279
|
kernelVersion: envelope.kernelVersion,
|
|
272
280
|
generatedAt: envelope.generatedAt,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
...(envelope.tsTranspilerVersion === undefined
|
|
277
|
-
? {}
|
|
278
|
-
: { tsTranspilerVersion: envelope.tsTranspilerVersion }),
|
|
281
|
+
scoringSemantics: envelope.scoringSemantics,
|
|
282
|
+
tsTranspilerVersion: envelope.tsTranspilerVersion,
|
|
283
|
+
provenanceStamped: envelope.provenanceStamped,
|
|
279
284
|
rollup: envelope.rollup,
|
|
280
285
|
rows: envelope.rows,
|
|
281
286
|
};
|
|
@@ -8,10 +8,17 @@ import { createGitInterface } from './git-utils.js';
|
|
|
8
8
|
*
|
|
9
9
|
* Pure; no I/O.
|
|
10
10
|
*
|
|
11
|
+
* Module-private since Story #4944. It was exported for
|
|
12
|
+
* `diff-scope-cli.js#resolveDiffScopeFiles`, which the duplication-CLI
|
|
13
|
+
* migration deleted; the three remaining callers all live in this file, so
|
|
14
|
+
* exporting it now would ship a seam only the tests reach. Its behaviour is
|
|
15
|
+
* covered through `diffNameOnly`, which returns this function's output
|
|
16
|
+
* verbatim.
|
|
17
|
+
*
|
|
11
18
|
* @param {string | null | undefined} stdout
|
|
12
19
|
* @returns {string[]}
|
|
13
20
|
*/
|
|
14
|
-
|
|
21
|
+
function parseNameOnlyStdout(stdout) {
|
|
15
22
|
if (!stdout) return [];
|
|
16
23
|
return stdout
|
|
17
24
|
.split('\n')
|