mandrel 2.66.0 → 2.68.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 +2 -2
- package/.agents/agents/story-worker.md +15 -11
- package/.agents/docs/agentrc-reference.json +5 -2
- package/.agents/docs/configuration.md +38 -2
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +2 -1
- package/.agents/rules/git-conventions-reference.md +5 -5
- package/.agents/rules/git-conventions.md +1 -1
- package/.agents/schemas/agentrc.schema.json +20 -2
- package/.agents/schemas/story-deliver-terminal.schema.json +23 -1
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/boot-sweep.js +97 -9
- package/.agents/scripts/{git-cleanup.js → clean-git.js} +2 -2
- package/.agents/scripts/clean-temp.js +54 -0
- package/.agents/scripts/clean-worktrees.js +593 -0
- package/.agents/scripts/coverage-capture.js +65 -9
- package/.agents/scripts/drain-pending-cleanup.js +5 -4
- package/.agents/scripts/evidence-gate.js +106 -8
- package/.agents/scripts/lib/baselines/coverage-refresh-scope.js +60 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +101 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +1 -1
- package/.agents/scripts/lib/baselines/seat-missing.js +228 -0
- package/.agents/scripts/lib/child-exec.js +39 -1
- package/.agents/scripts/lib/clean-temp.js +440 -0
- package/.agents/scripts/lib/close-validation/gates.js +59 -19
- package/.agents/scripts/lib/close-validation/process.js +23 -24
- package/.agents/scripts/lib/close-validation/runner.js +71 -40
- package/.agents/scripts/lib/config/gates/coverage.schema.js +21 -0
- package/.agents/scripts/lib/config/quality.js +7 -1
- package/.agents/scripts/lib/config/temp-paths.js +15 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +12 -3
- package/.agents/scripts/lib/coverage-baseline.js +78 -5
- package/.agents/scripts/lib/coverage-capture-affected.js +345 -0
- package/.agents/scripts/lib/coverage-capture-delta.js +180 -0
- package/.agents/scripts/lib/coverage-capture-fullscope.js +53 -32
- package/.agents/scripts/lib/coverage-capture-incremental.js +49 -26
- package/.agents/scripts/lib/coverage-capture-usage.js +1 -1
- package/.agents/scripts/lib/coverage-capture.js +121 -81
- package/.agents/scripts/lib/full-suite-lock.js +49 -46
- package/.agents/scripts/lib/full-suite-queue.js +83 -8
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +4 -1
- package/.agents/scripts/lib/orchestration/code-review.js +15 -3
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +149 -97
- package/.agents/scripts/lib/orchestration/review-deposit.js +219 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +11 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +29 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +124 -73
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +38 -20
- package/.agents/scripts/lib/orchestration/single-story-close/phases/lock-wait-pending.js +8 -2
- package/.agents/scripts/lib/orchestration/single-story-close/review-overlap.js +161 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +47 -7
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +8 -16
- package/.agents/scripts/lib/process-group.js +1 -1
- package/.agents/scripts/lib/single-story-sweep.js +2 -2
- package/.agents/scripts/lib/supervised-suite.js +247 -0
- package/.agents/scripts/lib/temp-removal.js +110 -0
- package/.agents/scripts/lib/temp-retention.js +122 -73
- package/.agents/scripts/lib/wave-runner/cross-run-overlap.js +120 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +5 -1
- package/.agents/scripts/lib/worktree/canonical-path.js +34 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +15 -4
- package/.agents/scripts/quality-preview.js +112 -14
- package/.agents/scripts/single-story-init.js +120 -17
- package/.agents/scripts/stories-wave-tick.js +47 -0
- package/.agents/scripts/story-review-compute.js +207 -0
- package/.agents/scripts/update-coverage-baseline.js +15 -10
- package/.agents/scripts/update-crap-baseline.js +12 -2
- package/.agents/scripts/update-maintainability-baseline.js +12 -2
- package/.agents/workflows/{git-cleanup.md → clean-git.md} +10 -10
- package/.agents/workflows/clean-temp.md +67 -0
- package/.agents/workflows/clean-worktrees.md +63 -0
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +3 -2
- package/.agents/workflows/helpers/code-review.md +7 -5
- package/.agents/workflows/helpers/deliver-digest.md +39 -36
- package/.agents/workflows/helpers/deliver-reference.md +115 -5
- package/.agents/workflows/helpers/deliver-story-reference.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +2 -1
- package/docs/CHANGELOG.md +39 -0
- package/lib/cli/registry.js +125 -18
- package/lib/migrations/steps/strip-removed-agentrc-keys.js +0 -5
- package/package.json +1 -1
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Ensures `coverage/coverage-final.json` is fresh before the CRAP gate: skip
|
|
4
4
|
* when the gate is off, no changed file is under `crap.targetDirs`, or the
|
|
5
|
-
* content-digest stamp matches; otherwise run `test:coverage`
|
|
6
|
-
*
|
|
5
|
+
* content-digest stamp matches; otherwise run `test:coverage` (or, under
|
|
6
|
+
* `coverage.captureScope: "affected"`, the consumer's `test:coverage:affected`)
|
|
7
|
+
* behind the host-level full-suite lock and stamp on success.
|
|
7
8
|
*
|
|
8
9
|
* `--require-credited` must stay an argument, never a config read: as policy
|
|
9
10
|
* it also refused the depositing run, leaving no path that could deposit.
|
|
10
11
|
*
|
|
11
12
|
* Exit codes: 0 fresh/skipped/captured; 1 capture failed or refused (callers
|
|
12
|
-
* MUST surface it); 75 lock wait expired
|
|
13
|
-
* `MANDREL_FULL_SUITE_LOCK_ON_EXPIRY=defer`); 124 suite timed out.
|
|
13
|
+
* MUST surface it); 75 lock wait expired, nothing spawned; 124 suite timed out.
|
|
14
14
|
*/
|
|
15
15
|
import { getChangedFiles } from './lib/changed-files.js';
|
|
16
16
|
import { isDirectInvocation } from './lib/cli-utils.js';
|
|
@@ -23,13 +23,13 @@ import {
|
|
|
23
23
|
runCapture,
|
|
24
24
|
writeCaptureStamp,
|
|
25
25
|
} from './lib/coverage-capture.js';
|
|
26
|
+
import { tryScopedCapture } from './lib/coverage-capture-affected.js';
|
|
26
27
|
import { runFullScopeCapture } from './lib/coverage-capture-fullscope.js';
|
|
27
|
-
import { tryIncrementalCapture } from './lib/coverage-capture-incremental.js';
|
|
28
28
|
import { handleCoverageCaptureHelp } from './lib/coverage-capture-usage.js';
|
|
29
29
|
import { lockedCapture } from './lib/full-suite-lock.js';
|
|
30
|
-
|
|
31
30
|
import { Logger } from './lib/Logger.js';
|
|
32
31
|
import { hasNpmScript, readPackageScripts } from './lib/npm-scripts.js';
|
|
32
|
+
import { formatSuiteTimings } from './lib/supervised-suite.js';
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* A `null` ref defers the fallback to `resolveChangedFilesRef`.
|
|
@@ -67,6 +67,7 @@ export function parseArgs(argv) {
|
|
|
67
67
|
* writeCaptureStampImpl?: typeof writeCaptureStamp,
|
|
68
68
|
* filterFilesUnderTargetsImpl?: typeof filterFilesUnderTargets,
|
|
69
69
|
* logger?: { info: Function, warn: Function, error: Function },
|
|
70
|
+
* lockOptions?: object,
|
|
70
71
|
* }} [deps]
|
|
71
72
|
* @returns {Promise<number>}
|
|
72
73
|
*/
|
|
@@ -83,6 +84,7 @@ export async function runCoverageCapture(argv = process.argv, deps = {}) {
|
|
|
83
84
|
writeCaptureStampImpl = writeCaptureStamp,
|
|
84
85
|
filterFilesUnderTargetsImpl = filterFilesUnderTargets,
|
|
85
86
|
logger = Logger,
|
|
87
|
+
lockOptions = {},
|
|
86
88
|
} = deps;
|
|
87
89
|
const args = parseArgs(argv);
|
|
88
90
|
const config = resolveConfigImpl({ cwd: args.cwd });
|
|
@@ -105,13 +107,65 @@ export async function runCoverageCapture(argv = process.argv, deps = {}) {
|
|
|
105
107
|
|
|
106
108
|
// Outermost first: the credit probe announces or refuses the run, and only
|
|
107
109
|
// a surviving run reaches the host-level full-suite lock.
|
|
108
|
-
const
|
|
110
|
+
const locked = lockedCapture(runCaptureImpl, config, process.env, {
|
|
111
|
+
rerunCommand: rerunCommandFor(argv),
|
|
112
|
+
...lockOptions,
|
|
113
|
+
});
|
|
114
|
+
const credited = creditedCapture(locked, {
|
|
109
115
|
requireCredited: args.requireCredited,
|
|
110
116
|
logger,
|
|
111
117
|
});
|
|
118
|
+
let timings = null;
|
|
119
|
+
const capture = (captureOpts) =>
|
|
120
|
+
credited({
|
|
121
|
+
...captureOpts,
|
|
122
|
+
onTimings: (t) => {
|
|
123
|
+
timings = t;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const code = await runCaptureScopes({
|
|
127
|
+
crap,
|
|
128
|
+
coverage,
|
|
129
|
+
args,
|
|
130
|
+
capture,
|
|
131
|
+
deps: {
|
|
132
|
+
getChangedFilesImpl,
|
|
133
|
+
isCoverageFreshImpl,
|
|
134
|
+
computeContentDigestImpl,
|
|
135
|
+
writeCaptureStampImpl,
|
|
136
|
+
filterFilesUnderTargetsImpl,
|
|
137
|
+
readPackageScriptsImpl,
|
|
138
|
+
hasNpmScriptImpl,
|
|
139
|
+
logger,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (timings) logger.info(`[coverage-capture] ${formatSuiteTimings(timings)}`);
|
|
143
|
+
return code;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @param {string[]} argv */
|
|
147
|
+
function rerunCommandFor(argv) {
|
|
148
|
+
return ['node', ...argv.slice(1)].join(' ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {{ crap: object, coverage: object, args: object, capture: Function, deps: object }} opts
|
|
153
|
+
* @returns {Promise<number>}
|
|
154
|
+
*/
|
|
155
|
+
async function runCaptureScopes({ crap, coverage, args, capture, deps }) {
|
|
156
|
+
const {
|
|
157
|
+
getChangedFilesImpl,
|
|
158
|
+
isCoverageFreshImpl,
|
|
159
|
+
computeContentDigestImpl,
|
|
160
|
+
writeCaptureStampImpl,
|
|
161
|
+
filterFilesUnderTargetsImpl,
|
|
162
|
+
readPackageScriptsImpl,
|
|
163
|
+
hasNpmScriptImpl,
|
|
164
|
+
logger,
|
|
165
|
+
} = deps;
|
|
112
166
|
|
|
113
167
|
// Shared so a new seam cannot reach one capture path and miss the other.
|
|
114
|
-
//
|
|
168
|
+
// A scoped `null` means not applicable: fall through to full scope.
|
|
115
169
|
const shared = {
|
|
116
170
|
crap,
|
|
117
171
|
coverage,
|
|
@@ -124,9 +178,11 @@ export async function runCoverageCapture(argv = process.argv, deps = {}) {
|
|
|
124
178
|
logger,
|
|
125
179
|
};
|
|
126
180
|
|
|
127
|
-
const incrementalResult = await
|
|
181
|
+
const incrementalResult = await tryScopedCapture({
|
|
128
182
|
...shared,
|
|
129
183
|
filterFilesUnderTargetsImpl,
|
|
184
|
+
readPackageScriptsImpl,
|
|
185
|
+
hasNpmScriptImpl,
|
|
130
186
|
});
|
|
131
187
|
if (incrementalResult !== null) return incrementalResult;
|
|
132
188
|
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* any still-stuck entries by enumerating the processes holding handles
|
|
9
9
|
* inside the worktree path and terminating them.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* (
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
11
|
+
* The same drain runs automatically at workflow boot: `runBootSweep`
|
|
12
|
+
* (`boot-sweep.js`) calls `sweepStaleStoryWorktrees`
|
|
13
|
+
* (`lib/orchestration/plan-runner/worktree-sweep.js`), which force-drains
|
|
14
|
+
* the ledger before reaping closed-Story worktrees. Operators can also run
|
|
15
|
+
* it standalone:
|
|
15
16
|
*
|
|
16
17
|
* node .agents/scripts/drain-pending-cleanup.js # full drain + escalate
|
|
17
18
|
* node .agents/scripts/drain-pending-cleanup.js --no-escalate # passive drain only
|
|
@@ -5,14 +5,25 @@
|
|
|
5
5
|
* passed for the current HEAD and tree, and recording a pass for the next
|
|
6
6
|
* caller. `--standalone` is required: it keys evidence by Story id, the same
|
|
7
7
|
* keyspace close consults, so worker-side verify[] runs credit the close.
|
|
8
|
+
* `--gate test` takes the host full-suite lock.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { spawnSync } from 'node:child_process';
|
|
11
12
|
import { parseArgs } from 'node:util';
|
|
12
13
|
import { runAsCli } from './lib/cli-utils.js';
|
|
14
|
+
import { getQuality, resolveConfig } from './lib/config-resolver.js';
|
|
15
|
+
import {
|
|
16
|
+
fullSuiteLockPolicy,
|
|
17
|
+
LOCK_WAIT_EXPIRED_EXIT_CODE,
|
|
18
|
+
withFullSuiteLockAsync,
|
|
19
|
+
} from './lib/full-suite-lock.js';
|
|
13
20
|
import { gitSpawn } from './lib/git-utils.js';
|
|
14
21
|
import { Logger } from './lib/Logger.js';
|
|
15
22
|
import { PROJECT_ROOT } from './lib/project-root.js';
|
|
23
|
+
import {
|
|
24
|
+
formatSuiteTimings,
|
|
25
|
+
runSupervisedSuite,
|
|
26
|
+
} from './lib/supervised-suite.js';
|
|
16
27
|
import {
|
|
17
28
|
hashCommandConfig,
|
|
18
29
|
recordPass,
|
|
@@ -20,6 +31,8 @@ import {
|
|
|
20
31
|
treeFingerprint,
|
|
21
32
|
} from './lib/validation-evidence.js';
|
|
22
33
|
|
|
34
|
+
const FULL_SUITE_GATE = 'test';
|
|
35
|
+
|
|
23
36
|
export function splitOnDashDash(argv) {
|
|
24
37
|
const idx = argv.indexOf('--');
|
|
25
38
|
if (idx === -1) return { wrapperArgs: argv, runnerArgs: [] };
|
|
@@ -90,6 +103,7 @@ function resolveEvidenceKeys({ spawnCwd, gitSpawnFn, useEvidence }) {
|
|
|
90
103
|
* @param {Function} [deps.shouldSkipFn]
|
|
91
104
|
* @param {Function} [deps.recordPassFn]
|
|
92
105
|
* @param {object} [deps.logger]
|
|
106
|
+
* @param {Function} [deps.runSuiteFn]
|
|
93
107
|
* @returns {{ status: number, skipped: boolean }}
|
|
94
108
|
*/
|
|
95
109
|
export async function runEvidenceGate(params, deps = {}) {
|
|
@@ -99,6 +113,7 @@ export async function runEvidenceGate(params, deps = {}) {
|
|
|
99
113
|
shouldSkipFn = shouldSkip,
|
|
100
114
|
recordPassFn = recordPass,
|
|
101
115
|
logger = Logger,
|
|
116
|
+
runSuiteFn = runLockedSuite,
|
|
102
117
|
} = deps;
|
|
103
118
|
const {
|
|
104
119
|
scopeId,
|
|
@@ -159,17 +174,18 @@ export async function runEvidenceGate(params, deps = {}) {
|
|
|
159
174
|
logger.info(
|
|
160
175
|
`[evidence-gate] ▶ ${gate} → ${cmd} ${cmdArgs.join(' ')} (cwd=${spawnCwd})`,
|
|
161
176
|
);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
const status = await runGateCommand({
|
|
178
|
+
gate,
|
|
179
|
+
cmd,
|
|
180
|
+
cmdArgs,
|
|
181
|
+
spawnCwd,
|
|
182
|
+
logger,
|
|
183
|
+
spawnFn,
|
|
184
|
+
runSuiteFn,
|
|
166
185
|
});
|
|
167
|
-
const status = result.status ?? 1;
|
|
168
186
|
if (status !== 0) {
|
|
169
187
|
process.exitCode = status;
|
|
170
|
-
logger
|
|
171
|
-
`[evidence-gate] ✖ ${gate} failed (exit ${status}) in ${spawnCwd}`,
|
|
172
|
-
);
|
|
188
|
+
reportGateExit({ gate, status, spawnCwd, logger });
|
|
173
189
|
return { status, skipped: false };
|
|
174
190
|
}
|
|
175
191
|
|
|
@@ -197,6 +213,88 @@ export async function runEvidenceGate(params, deps = {}) {
|
|
|
197
213
|
return { status: 0, skipped: false };
|
|
198
214
|
}
|
|
199
215
|
|
|
216
|
+
/**
|
|
217
|
+
* @param {{ cmd: string, args: string[], cwd: string, log: (m: string) => void }} run
|
|
218
|
+
* @param {{ config?: object|null, env?: object, lockOptions?: object, runSuiteImpl?: typeof runSupervisedSuite }} [seams]
|
|
219
|
+
* @returns {Promise<number>} 75 when the lock wait expired.
|
|
220
|
+
*/
|
|
221
|
+
export function runLockedSuite(
|
|
222
|
+
{ cmd, args, cwd, log },
|
|
223
|
+
{
|
|
224
|
+
config = loadConfig(cwd),
|
|
225
|
+
env = process.env,
|
|
226
|
+
lockOptions = {},
|
|
227
|
+
runSuiteImpl = runSupervisedSuite,
|
|
228
|
+
} = {},
|
|
229
|
+
) {
|
|
230
|
+
const timeoutMs = getQuality(config).coverage?.timeoutMs;
|
|
231
|
+
return withFullSuiteLockAsync(
|
|
232
|
+
{
|
|
233
|
+
...fullSuiteLockPolicy(config, env),
|
|
234
|
+
cwd,
|
|
235
|
+
log,
|
|
236
|
+
rerunCommand: ['node', ...process.argv.slice(1)].join(' '),
|
|
237
|
+
...lockOptions,
|
|
238
|
+
},
|
|
239
|
+
({ lockWaitMs }) =>
|
|
240
|
+
runSuiteImpl({
|
|
241
|
+
cmd,
|
|
242
|
+
args,
|
|
243
|
+
cwd,
|
|
244
|
+
timeoutMs,
|
|
245
|
+
lockWaitMs,
|
|
246
|
+
onTimings: (t) => log(`[evidence-gate] ${formatSuiteTimings(t)}`),
|
|
247
|
+
onTimeout: () =>
|
|
248
|
+
log(
|
|
249
|
+
`[evidence-gate] ⏱ ${cmd} ${args.join(' ')} exceeded ${timeoutMs}ms — killed its process group.`,
|
|
250
|
+
),
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The `test` gate runs locked and supervised; any other gate plainly. */
|
|
256
|
+
async function runGateCommand({
|
|
257
|
+
gate,
|
|
258
|
+
cmd,
|
|
259
|
+
cmdArgs,
|
|
260
|
+
spawnCwd,
|
|
261
|
+
logger,
|
|
262
|
+
spawnFn,
|
|
263
|
+
runSuiteFn,
|
|
264
|
+
}) {
|
|
265
|
+
if (gate === FULL_SUITE_GATE) {
|
|
266
|
+
const log = (m) => logger.info(m);
|
|
267
|
+
return await runSuiteFn({ cmd, args: cmdArgs, cwd: spawnCwd, log });
|
|
268
|
+
}
|
|
269
|
+
const result = spawnFn(cmd, cmdArgs, {
|
|
270
|
+
cwd: spawnCwd,
|
|
271
|
+
stdio: 'inherit',
|
|
272
|
+
shell: process.platform === 'win32',
|
|
273
|
+
});
|
|
274
|
+
return result.status ?? 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function reportGateExit({ gate, status, spawnCwd, logger }) {
|
|
278
|
+
if (status === LOCK_WAIT_EXPIRED_EXIT_CODE) {
|
|
279
|
+
logger.info(
|
|
280
|
+
`[evidence-gate] ⏸ ${gate} deferred (exit ${status}) — another full suite still holds the host lock, so nothing ran and no evidence was recorded.`,
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
logger.error(
|
|
285
|
+
`[evidence-gate] ✖ ${gate} failed (exit ${status}) in ${spawnCwd}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** An unloadable config falls back to the defaults. */
|
|
290
|
+
function loadConfig(cwd) {
|
|
291
|
+
try {
|
|
292
|
+
return resolveConfig({ cwd });
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
200
298
|
async function main() {
|
|
201
299
|
const { wrapperArgs, runnerArgs } = splitOnDashDash(process.argv.slice(2));
|
|
202
300
|
const args = parseWrapperArgs(wrapperArgs);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Wires `resolveCoverageRefreshScope` to the on-disk artifact and c8 scope. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
buildScopePredicate,
|
|
6
|
+
readArtifactCaptureScope,
|
|
7
|
+
readCoverageFinal,
|
|
8
|
+
resolveCoverageRefreshScope,
|
|
9
|
+
scoreCoverageFinal,
|
|
10
|
+
} from '../coverage-baseline.js';
|
|
11
|
+
import { deriveScopeFromDiff } from './refresh-service.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `refreshBaseline` scope options for `update-coverage-baseline.js`.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} cwd
|
|
17
|
+
* @param {{
|
|
18
|
+
* fullScope: boolean,
|
|
19
|
+
* diffScopeRef: string | null,
|
|
20
|
+
* loadScope: (cwd: string) => { include?: string[], exclude?: string[] },
|
|
21
|
+
* readCaptureScope?: typeof readArtifactCaptureScope,
|
|
22
|
+
* readCoverage?: typeof readCoverageFinal,
|
|
23
|
+
* existsSync?: typeof fs.existsSync,
|
|
24
|
+
* deriveDiff?: typeof deriveScopeFromDiff,
|
|
25
|
+
* }} opts
|
|
26
|
+
*/
|
|
27
|
+
export function resolveUpdaterRefreshScope(
|
|
28
|
+
cwd,
|
|
29
|
+
{
|
|
30
|
+
fullScope,
|
|
31
|
+
diffScopeRef,
|
|
32
|
+
loadScope,
|
|
33
|
+
readCaptureScope = readArtifactCaptureScope,
|
|
34
|
+
readCoverage = readCoverageFinal,
|
|
35
|
+
existsSync = fs.existsSync,
|
|
36
|
+
deriveDiff = deriveScopeFromDiff,
|
|
37
|
+
},
|
|
38
|
+
) {
|
|
39
|
+
const c8 = () => {
|
|
40
|
+
const config = loadScope(cwd);
|
|
41
|
+
return buildScopePredicate({
|
|
42
|
+
include: config.include ?? [],
|
|
43
|
+
exclude: config.exclude ?? [],
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
return resolveCoverageRefreshScope({
|
|
47
|
+
cwd,
|
|
48
|
+
fullScope,
|
|
49
|
+
diffScopeRef,
|
|
50
|
+
readCaptureScope,
|
|
51
|
+
listMeasured: () =>
|
|
52
|
+
Object.keys(
|
|
53
|
+
scoreCoverageFinal({ raw: readCoverage(cwd), cwd, scope: c8() }),
|
|
54
|
+
),
|
|
55
|
+
inCoverageScope: (file) =>
|
|
56
|
+
c8()(file) && existsSync(path.resolve(cwd, file)),
|
|
57
|
+
deriveDiffFiles: (baseRef) =>
|
|
58
|
+
deriveDiff({ baseRef, headRef: 'HEAD', predicate: () => true, cwd }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { getBaselines, getQuality, resolveConfig } from '../config-resolver.js';
|
|
10
|
+
import { isCoverageFresh } from '../coverage-capture.js';
|
|
11
|
+
import { loadCoverage as loadCoverageDefault } from '../coverage-utils.js';
|
|
9
12
|
import { checkResolutionFloor, scanAndScore } from '../crap-utils.js';
|
|
10
13
|
import { Logger } from '../Logger.js';
|
|
11
14
|
import { parseDiffScopeFlag } from './diff-scope-cli.js';
|
|
15
|
+
import {
|
|
16
|
+
checkSeatResolution,
|
|
17
|
+
runSeatMissing,
|
|
18
|
+
SeatRefusal,
|
|
19
|
+
} from './seat-missing.js';
|
|
12
20
|
|
|
13
21
|
const DEFAULT_COVERAGE_PATH = 'coverage/coverage-final.json';
|
|
14
22
|
|
|
@@ -20,7 +28,7 @@ const DEFAULT_MIN_RESOLUTION_RATE = 0.75;
|
|
|
20
28
|
*
|
|
21
29
|
* @param {string[]} [argv]
|
|
22
30
|
* @returns {{baselinePath: string|undefined, coveragePath: string|undefined,
|
|
23
|
-
* fullScope: boolean, diffScopeRef: string|null}}
|
|
31
|
+
* fullScope: boolean, diffScopeRef: string|null, seatMissing: boolean}}
|
|
24
32
|
*/
|
|
25
33
|
export function parseCrapUpdaterArgs(argv = []) {
|
|
26
34
|
const out = {
|
|
@@ -28,6 +36,7 @@ export function parseCrapUpdaterArgs(argv = []) {
|
|
|
28
36
|
coveragePath: undefined,
|
|
29
37
|
fullScope: false,
|
|
30
38
|
diffScopeRef: parseDiffScopeFlag(argv),
|
|
39
|
+
seatMissing: argv.includes('--seat-missing'),
|
|
31
40
|
};
|
|
32
41
|
for (let i = 0; i < argv.length; i += 1) {
|
|
33
42
|
if (argv[i] === '--baseline' && argv[i + 1]) {
|
|
@@ -45,16 +54,17 @@ export function parseCrapUpdaterArgs(argv = []) {
|
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* Flag → config → default. Throws on `--full-scope` with `--diff-scope`:
|
|
48
|
-
* silently preferring one would write a baseline nobody asked for
|
|
57
|
+
* silently preferring one would write a baseline nobody asked for
|
|
58
|
+
* (`runSeatMissing` refuses the `--seat-missing` pairing).
|
|
49
59
|
*
|
|
50
60
|
* @param {{baselinePath?: string, coveragePath?: string, fullScope?: boolean,
|
|
51
|
-
* diffScopeRef?: string|null}} args
|
|
61
|
+
* diffScopeRef?: string|null, seatMissing?: boolean}} args
|
|
52
62
|
* @param {{crap?: object, baselines?: object}} sources
|
|
53
63
|
* @param {string} [cwd]
|
|
54
64
|
* @returns {{targetDirs: string[], ignoreGlobs: string[],
|
|
55
65
|
* requireCoverage: boolean, minMethodResolutionRate: number,
|
|
56
66
|
* coveragePath: string, baselinePath: string, absBaselinePath: string,
|
|
57
|
-
* fullScope: boolean, diffScopeRef: string|null}}
|
|
67
|
+
* fullScope: boolean, diffScopeRef: string|null, seatMissing: boolean}}
|
|
58
68
|
*/
|
|
59
69
|
export function resolveCrapUpdaterOptions(
|
|
60
70
|
args = {},
|
|
@@ -82,6 +92,7 @@ export function resolveCrapUpdaterOptions(
|
|
|
82
92
|
: path.resolve(cwd, baselinePath),
|
|
83
93
|
fullScope: Boolean(args.fullScope),
|
|
84
94
|
diffScopeRef: args.diffScopeRef ?? null,
|
|
95
|
+
seatMissing: Boolean(args.seatMissing),
|
|
85
96
|
};
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -172,3 +183,89 @@ export function buildCrapUpdaterScorer(
|
|
|
172
183
|
);
|
|
173
184
|
};
|
|
174
185
|
}
|
|
186
|
+
|
|
187
|
+
/** Stamp scopes a capture may have written; any one fresh stamp suffices. */
|
|
188
|
+
const CAPTURE_SCOPES = ['full', 'incremental', 'affected'];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The `--seat-missing` scorer: refuses (throws {@link SeatRefusal}) unless
|
|
192
|
+
* the coverage artifact is fresh for the current tree and every in-scope
|
|
193
|
+
* method resolved a coverage entry — a wrong-coordinate row stays wrong even
|
|
194
|
+
* when it is only inserted.
|
|
195
|
+
*
|
|
196
|
+
* @param {ReturnType<typeof resolveCrapUpdaterOptions>} options
|
|
197
|
+
* @param {{loadCoverage: Function, scan?: Function, isFresh?: Function,
|
|
198
|
+
* cwd?: string, logger?: object}} deps
|
|
199
|
+
* @returns {(files: string[]) => Promise<object[]>}
|
|
200
|
+
*/
|
|
201
|
+
export function buildCrapSeatScorer(
|
|
202
|
+
options,
|
|
203
|
+
{
|
|
204
|
+
loadCoverage,
|
|
205
|
+
scan = scanAndScore,
|
|
206
|
+
isFresh = isCoverageFresh,
|
|
207
|
+
cwd = process.cwd(),
|
|
208
|
+
logger = Logger,
|
|
209
|
+
} = {},
|
|
210
|
+
) {
|
|
211
|
+
const fixCommand = `node .agents/scripts/coverage-capture.js --cwd ${cwd}`;
|
|
212
|
+
return async (files) => {
|
|
213
|
+
const fresh = CAPTURE_SCOPES.some(
|
|
214
|
+
(requireScope) =>
|
|
215
|
+
isFresh({
|
|
216
|
+
coveragePath: options.coveragePath,
|
|
217
|
+
targetDirs: options.targetDirs,
|
|
218
|
+
cwd,
|
|
219
|
+
requireScope,
|
|
220
|
+
}).fresh,
|
|
221
|
+
);
|
|
222
|
+
const coverage = fresh
|
|
223
|
+
? loadCoverage(path.resolve(cwd, options.coveragePath))
|
|
224
|
+
: null;
|
|
225
|
+
if (!coverage) {
|
|
226
|
+
throw new SeatRefusal(
|
|
227
|
+
`[CRAP] --seat-missing refused: no coverage artifact at ${options.coveragePath} is fresh for the current tree ` +
|
|
228
|
+
`(method resolution unmeasured; unresolved files: ${files.join(', ')}).\n` +
|
|
229
|
+
`Fix: re-capture coverage for the current tree — ${fixCommand}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const summary = await scan({
|
|
233
|
+
targetDirs: options.targetDirs,
|
|
234
|
+
coverage,
|
|
235
|
+
requireCoverage: options.requireCoverage,
|
|
236
|
+
cwd,
|
|
237
|
+
ignoreGlobs: options.ignoreGlobs,
|
|
238
|
+
scopeFiles: files,
|
|
239
|
+
});
|
|
240
|
+
reportScanSummary(summary, logger);
|
|
241
|
+
const refusal = checkSeatResolution(summary.resolution, fixCommand);
|
|
242
|
+
if (refusal) throw new SeatRefusal(refusal);
|
|
243
|
+
return (summary.rows ?? []).filter(
|
|
244
|
+
(r) => typeof r?.crap === 'number' && Number.isFinite(r.crap),
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* `update-crap-baseline.js --seat-missing`: seat the diff's new methods
|
|
251
|
+
* through {@link buildCrapSeatScorer}. Resolves to the exit code.
|
|
252
|
+
*
|
|
253
|
+
* @param {string[]} argv
|
|
254
|
+
* @param {{config?: object}} [deps]
|
|
255
|
+
* @returns {Promise<number>}
|
|
256
|
+
*/
|
|
257
|
+
export function seatCrapBaseline(argv, { config = resolveConfig() } = {}) {
|
|
258
|
+
const options = resolveCrapUpdaterOptions(parseCrapUpdaterArgs(argv), {
|
|
259
|
+
crap: getQuality(config).crap,
|
|
260
|
+
baselines: getBaselines(config),
|
|
261
|
+
});
|
|
262
|
+
return runSeatMissing({
|
|
263
|
+
kind: 'crap',
|
|
264
|
+
label: 'CRAP',
|
|
265
|
+
writePath: options.absBaselinePath,
|
|
266
|
+
diffScopeRef: options.diffScopeRef,
|
|
267
|
+
fullScope: options.fullScope,
|
|
268
|
+
baseBranch: config.project.baseBranch,
|
|
269
|
+
score: buildCrapSeatScorer(options, { loadCoverage: loadCoverageDefault }),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -507,7 +507,7 @@ async function resolveScope({
|
|
|
507
507
|
* @param {typeof nodeFs} fs
|
|
508
508
|
* @returns {object | null}
|
|
509
509
|
*/
|
|
510
|
-
function readPriorEnvelope(writePath, fs) {
|
|
510
|
+
export function readPriorEnvelope(writePath, fs) {
|
|
511
511
|
let raw;
|
|
512
512
|
try {
|
|
513
513
|
raw = fs.readFileSync(writePath, 'utf8');
|