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
|
@@ -30,10 +30,14 @@ import {
|
|
|
30
30
|
import { calculateAll, scanDirectory } from '../maintainability-utils.js';
|
|
31
31
|
import { resolveCrapEnvOverrides } from './env-overrides.js';
|
|
32
32
|
import {
|
|
33
|
+
assertBaselineCompatible,
|
|
34
|
+
assessComparisonBasis,
|
|
33
35
|
buildCrapReport,
|
|
34
36
|
compareCrap,
|
|
35
37
|
filterRowsByFileScope,
|
|
38
|
+
INCOMPATIBLE_BASELINE_DIAGNOSTIC,
|
|
36
39
|
loadCrapBaseline,
|
|
40
|
+
suppressVerdicts,
|
|
37
41
|
} from './kinds/crap.js';
|
|
38
42
|
import {
|
|
39
43
|
buildMaintainabilityReport,
|
|
@@ -131,6 +135,34 @@ function hasCrapRegressions(result) {
|
|
|
131
135
|
return result.regressions > 0 || result.newViolations > 0;
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Build the zero-row CRAP envelope the preview returns when it has nothing to
|
|
140
|
+
* compare against — no baseline, or the gate disabled.
|
|
141
|
+
*
|
|
142
|
+
* @param {{scope: string, diffRef: string|null}} scopeInfo
|
|
143
|
+
* @returns {object}
|
|
144
|
+
*/
|
|
145
|
+
function emptyCrapEnvelope({ scope, diffRef }) {
|
|
146
|
+
return {
|
|
147
|
+
kernelVersion: KERNEL_VERSION,
|
|
148
|
+
escomplexVersion: resolveEscomplexVersion(),
|
|
149
|
+
summary: {
|
|
150
|
+
total: 0,
|
|
151
|
+
regressions: 0,
|
|
152
|
+
newViolations: 0,
|
|
153
|
+
drifted: 0,
|
|
154
|
+
incomparable: 0,
|
|
155
|
+
provenanceMismatched: 0,
|
|
156
|
+
unscorable: 0,
|
|
157
|
+
removed: 0,
|
|
158
|
+
skippedNoCoverage: 0,
|
|
159
|
+
scope,
|
|
160
|
+
diffRef,
|
|
161
|
+
},
|
|
162
|
+
violations: [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
function applyDiffScopeMi({ files, baseline, scopeSet, cwd }) {
|
|
135
167
|
if (!scopeSet) {
|
|
136
168
|
return { scopedFiles: files, scopedBaseline: baseline ?? {} };
|
|
@@ -236,23 +268,24 @@ export async function runCrapPreview({
|
|
|
236
268
|
const quality = getQuality(config);
|
|
237
269
|
const crap = quality.crap;
|
|
238
270
|
if (!baseline || crap.enabled === false) {
|
|
271
|
+
return { exitCode: 0, envelope: emptyCrapEnvelope({ scope, diffRef }) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Story #4866 (AC-6): judge the loaded envelope BEFORE comparing anything
|
|
275
|
+
// against it. A baseline whose scoring semantics or transpiler coordinates
|
|
276
|
+
// predate the running scorer cannot yield a meaningful per-method verdict,
|
|
277
|
+
// and emitting regressions from it asks the operator to fix code that is
|
|
278
|
+
// not broken. Fail open — the authoritative `check-baselines` gate still
|
|
279
|
+
// gates the merge, so a permissive pre-commit hook costs no real coverage
|
|
280
|
+
// while a blocking one costs a provably defect-free commit.
|
|
281
|
+
const incompatible = assertBaselineCompatible(baseline);
|
|
282
|
+
if (incompatible) {
|
|
239
283
|
return {
|
|
240
284
|
exitCode: 0,
|
|
241
|
-
envelope: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
total: 0,
|
|
246
|
-
regressions: 0,
|
|
247
|
-
newViolations: 0,
|
|
248
|
-
drifted: 0,
|
|
249
|
-
removed: 0,
|
|
250
|
-
skippedNoCoverage: 0,
|
|
251
|
-
scope,
|
|
252
|
-
diffRef,
|
|
253
|
-
},
|
|
254
|
-
violations: [],
|
|
255
|
-
},
|
|
285
|
+
envelope: suppressVerdicts(emptyCrapEnvelope({ scope, diffRef }), {
|
|
286
|
+
name: INCOMPATIBLE_BASELINE_DIAGNOSTIC,
|
|
287
|
+
message: incompatible,
|
|
288
|
+
}),
|
|
256
289
|
};
|
|
257
290
|
}
|
|
258
291
|
|
|
@@ -298,6 +331,16 @@ export async function runCrapPreview({
|
|
|
298
331
|
diffRef,
|
|
299
332
|
},
|
|
300
333
|
});
|
|
334
|
+
// Story #4866 (AC-5): above the drifted-row ratio the basis is self-
|
|
335
|
+
// evidently unsound and every per-method verdict below it is an artefact of
|
|
336
|
+
// a mis-keyed join. Say so once, by name, and fail open.
|
|
337
|
+
const basis = assessComparisonBasis(result);
|
|
338
|
+
if (!basis.sound) {
|
|
339
|
+
return {
|
|
340
|
+
exitCode: 0,
|
|
341
|
+
envelope: suppressVerdicts(envelope, basis.diagnostic),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
301
344
|
const exitCode = hasCrapRegressions(result) ? 1 : 0;
|
|
302
345
|
return { exitCode, envelope };
|
|
303
346
|
}
|
|
@@ -221,6 +221,11 @@ function readAndShape(kind, absolutePath) {
|
|
|
221
221
|
// dropping it here would make every baseline look unstamped and fail the
|
|
222
222
|
// whole repo closed on a stamp that is actually present on disk.
|
|
223
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,
|
|
224
229
|
};
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -314,6 +319,11 @@ export function loadFile(absolutePath, opts = {}) {
|
|
|
314
319
|
// dropping it here would make every baseline look unstamped and fail the
|
|
315
320
|
// whole repo closed on a stamp that is actually present on disk.
|
|
316
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,
|
|
317
327
|
};
|
|
318
328
|
}
|
|
319
329
|
|
|
@@ -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,17 +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
|
-
//
|
|
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.
|
|
268
277
|
const canonical = {
|
|
269
278
|
$schema: envelope.$schema,
|
|
270
279
|
kernelVersion: envelope.kernelVersion,
|
|
271
280
|
generatedAt: envelope.generatedAt,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
281
|
+
scoringSemantics: envelope.scoringSemantics,
|
|
282
|
+
tsTranspilerVersion: envelope.tsTranspilerVersion,
|
|
283
|
+
provenanceStamped: envelope.provenanceStamped,
|
|
275
284
|
rollup: envelope.rollup,
|
|
276
285
|
rows: envelope.rows,
|
|
277
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')
|