probatio 0.3.0 → 0.5.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/README.md +10 -198
- package/bin/probatio.js +52 -0
- package/package.json +21 -29
- package/LICENSE +0 -21
- package/dist/cli.js +0 -721
- package/dist/contract.js +0 -32
- package/dist/flags.js +0 -87
- package/dist/golden/check.js +0 -188
- package/dist/golden/record.js +0 -395
- package/dist/golden/recorder.mjs +0 -78
- package/dist/ledger/build.js +0 -348
- package/dist/ledger/check.js +0 -133
- package/dist/matrix/report.js +0 -256
- package/dist/mcp.js +0 -188
- package/dist/memory/state.js +0 -222
- package/dist/mutate/affected.js +0 -173
- package/dist/mutate/child-lines.mjs +0 -39
- package/dist/mutate/coverage-map.js +0 -229
- package/dist/mutate/csharp-coverage.js +0 -161
- package/dist/mutate/fail-reporter.mjs +0 -65
- package/dist/mutate/find.js +0 -13
- package/dist/mutate/generate.js +0 -156
- package/dist/mutate/go-coverage.js +0 -100
- package/dist/mutate/ids.js +0 -22
- package/dist/mutate/java-coverage.js +0 -217
- package/dist/mutate/kill-label.js +0 -34
- package/dist/mutate/load-failure.mjs +0 -40
- package/dist/mutate/mocha-coverage.cjs +0 -77
- package/dist/mutate/node-batch-loader.mjs +0 -99
- package/dist/mutate/node-batch.mjs +0 -167
- package/dist/mutate/node-coverage.mjs +0 -77
- package/dist/mutate/operators.js +0 -181
- package/dist/mutate/patch.js +0 -136
- package/dist/mutate/precise-lines.mjs +0 -304
- package/dist/mutate/probatio-jacoco-run.java +0 -516
- package/dist/mutate/project-config.js +0 -58
- package/dist/mutate/py-pytest-reporter.py +0 -145
- package/dist/mutate/py-reporter.py +0 -79
- package/dist/mutate/run.js +0 -1335
- package/dist/mutate/rust-coverage.js +0 -144
- package/dist/mutate/sealed.js +0 -118
- package/dist/mutate/suite-decision.js +0 -130
- package/dist/mutate/suites.js +0 -1524
- package/dist/mutate/tally.js +0 -240
- package/dist/mutate/text-operators.js +0 -637
- package/dist/mutate/titles.js +0 -23
- package/dist/node-version.js +0 -13
- package/dist/swarm/check-kill.js +0 -114
- package/dist/swarm/queue.js +0 -68
- package/dist/verify/change.js +0 -340
- package/schemas/check-kill.schema.json +0 -93
- package/schemas/findings.add.schema.json +0 -53
- package/schemas/gap.fix.schema.json +0 -53
- package/schemas/gap.revert.schema.json +0 -53
- package/schemas/golden.check.schema.json +0 -89
- package/schemas/golden.compare.schema.json +0 -151
- package/schemas/golden.record.schema.json +0 -126
- package/schemas/guard.check.schema.json +0 -53
- package/schemas/ledger.build.schema.json +0 -132
- package/schemas/ledger.check.schema.json +0 -143
- package/schemas/matrix.report.schema.json +0 -129
- package/schemas/mcp.schema.json +0 -53
- package/schemas/mutate.generate.schema.json +0 -127
- package/schemas/mutate.help.schema.json +0 -53
- package/schemas/mutate.run.schema.json +0 -272
- package/schemas/mutate.sealed.schema.json +0 -53
- package/schemas/mutate.tally.schema.json +0 -137
- package/schemas/queue.claim.schema.json +0 -53
- package/schemas/queue.reap.schema.json +0 -71
- package/schemas/queue.seed.schema.json +0 -71
- package/schemas/schema.schema.json +0 -74
- package/schemas/seal.schema.json +0 -69
- package/schemas/status.schema.json +0 -103
- package/schemas/verify-change.schema.json +0 -222
package/dist/cli.js
DELETED
|
@@ -1,721 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { SCHEMA_VERSION, render } from "./contract.js";
|
|
6
|
-
import { bool, int, parseArgs, requireText, text, texts } from "./flags.js";
|
|
7
|
-
import { asTable, checkGolden, goldenShape, readGolden, writeGolden } from "./golden/check.js";
|
|
8
|
-
import { compareGoldens, recordGoldens } from "./golden/record.js";
|
|
9
|
-
import { buildLedger } from "./ledger/build.js";
|
|
10
|
-
import { checkLedger } from "./ledger/check.js";
|
|
11
|
-
import { readKills, reportMatrix } from "./matrix/report.js";
|
|
12
|
-
import { appendFinding, chooseSealed, fixGap, guardAllows, lineHash, loadState, parseGuard, readSourceLine, revertGap, saveState, sealIds, statusEnvelope, } from "./memory/state.js";
|
|
13
|
-
import { git } from "./mutate/patch.js";
|
|
14
|
-
import { generateMutants } from "./mutate/generate.js";
|
|
15
|
-
import { runMutants } from "./mutate/run.js";
|
|
16
|
-
import { fixEditedKillers, LABEL_LEAK_SUMMARY, labelLeaked, readSealedLabel, sealedTreeLeaked } from "./mutate/sealed.js";
|
|
17
|
-
import { generateNext } from "./mutate/suite-decision.js";
|
|
18
|
-
import { discoverSuite } from "./mutate/suites.js";
|
|
19
|
-
import { tallyRun } from "./mutate/tally.js";
|
|
20
|
-
import { checkKill } from "./swarm/check-kill.js";
|
|
21
|
-
import { claimItem, reapClaims, seedQueue } from "./swarm/queue.js";
|
|
22
|
-
import { verifyChange } from "./verify/change.js";
|
|
23
|
-
import { nodeTooOld } from "./node-version.js";
|
|
24
|
-
const tooOld = nodeTooOld(process.versions.node);
|
|
25
|
-
if (tooOld) {
|
|
26
|
-
process.stdout.write(`${JSON.stringify({
|
|
27
|
-
schemaVersion: SCHEMA_VERSION,
|
|
28
|
-
ok: false,
|
|
29
|
-
command: "mutate.help",
|
|
30
|
-
summary: tooOld,
|
|
31
|
-
next: "Install Node 22.6 or newer and run the command again.",
|
|
32
|
-
nextCall: null,
|
|
33
|
-
})}\n`);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
const parsed = parseArgs(process.argv.slice(2));
|
|
37
|
-
const [group, action] = parsed.command;
|
|
38
|
-
if (group === "mcp" && !action) {
|
|
39
|
-
await import("./mcp.js");
|
|
40
|
-
}
|
|
41
|
-
else
|
|
42
|
-
try {
|
|
43
|
-
if (group === "mutate" && action === "generate")
|
|
44
|
-
finish(await generateCommand(parsed.flags), parsed.human);
|
|
45
|
-
if (group === "mutate" && action === "run")
|
|
46
|
-
finish(await runCommand(parsed.flags), parsed.human);
|
|
47
|
-
if (group === "mutate" && action === "sealed")
|
|
48
|
-
finish(await sealedCommand(parsed.flags), parsed.human);
|
|
49
|
-
if (group === "mutate" && action === "tally")
|
|
50
|
-
finish(tallyCommand(parsed.flags), parsed.human);
|
|
51
|
-
if (group === "ledger" && action === "build")
|
|
52
|
-
finish(await ledgerCommand(parsed.flags), parsed.human);
|
|
53
|
-
if (group === "ledger" && action === "check")
|
|
54
|
-
finish(await ledgerCheckCommand(parsed.flags), parsed.human);
|
|
55
|
-
if (group === "matrix" && action === "report")
|
|
56
|
-
finish(matrixCommand(parsed.flags), parsed.human);
|
|
57
|
-
if (group === "golden" && action === "check")
|
|
58
|
-
finish(goldenCommand(parsed.flags), parsed.human);
|
|
59
|
-
if (group === "golden" && action === "record")
|
|
60
|
-
finish(await goldenRecordCommand(parsed.flags), parsed.human);
|
|
61
|
-
if (group === "golden" && action === "compare")
|
|
62
|
-
finish(await goldenCompareCommand(parsed.flags), parsed.human);
|
|
63
|
-
if (group === "gap" && action === "fix")
|
|
64
|
-
finish(gapFixCommand(parsed.flags), parsed.human);
|
|
65
|
-
if (group === "gap" && action === "revert")
|
|
66
|
-
finish(gapRevertCommand(parsed.flags), parsed.human);
|
|
67
|
-
if (group === "guard" && action === "check")
|
|
68
|
-
finish(guardCommand(parsed.flags), parsed.human);
|
|
69
|
-
if (group === "findings" && action === "add")
|
|
70
|
-
finish(findingsCommand(parsed.flags), parsed.human);
|
|
71
|
-
if (group === "status")
|
|
72
|
-
finish(statusCommand(parsed.flags), parsed.human);
|
|
73
|
-
if (group === "seal")
|
|
74
|
-
finish(sealCommand(parsed.flags), parsed.human);
|
|
75
|
-
if (group === "queue" && action === "seed")
|
|
76
|
-
finish(queueSeedCommand(parsed.flags), parsed.human);
|
|
77
|
-
if (group === "queue" && action === "claim")
|
|
78
|
-
finish(queueClaimCommand(parsed.flags), parsed.human);
|
|
79
|
-
if (group === "queue" && action === "reap")
|
|
80
|
-
finish(queueReapCommand(parsed.flags), parsed.human);
|
|
81
|
-
if (group === "check-kill")
|
|
82
|
-
finish(await checkKillCommand(parsed.flags, action), parsed.human);
|
|
83
|
-
if (group === "verify-change")
|
|
84
|
-
finish(await verifyCommand(parsed.flags), parsed.human);
|
|
85
|
-
if (group === "schema")
|
|
86
|
-
finish(schemaCommand(action), parsed.human);
|
|
87
|
-
finish(usage(false, "Use mutate, ledger, matrix, golden, gap, guard, findings, status, queue, check-kill, verify-change, or schema."), parsed.human);
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
91
|
-
finish({
|
|
92
|
-
schemaVersion: SCHEMA_VERSION,
|
|
93
|
-
ok: false,
|
|
94
|
-
command: `${group ?? "mutate"}.${action ?? "help"}`,
|
|
95
|
-
summary: message,
|
|
96
|
-
next: "Fix the command and run it again.",
|
|
97
|
-
nextCall: null,
|
|
98
|
-
}, parsed.human);
|
|
99
|
-
}
|
|
100
|
-
function usage(ok, summary) {
|
|
101
|
-
return {
|
|
102
|
-
schemaVersion: SCHEMA_VERSION,
|
|
103
|
-
ok,
|
|
104
|
-
command: "mutate.help",
|
|
105
|
-
summary,
|
|
106
|
-
next: "probatio mutate generate writes operator mutants. probatio mutate run confirms each kill twice. probatio mutate sealed runs one forward bug and keeps the label file out of the report. probatio mutate tally reads a finished run and does not delete a test. probatio ledger build reverts fix commits. probatio matrix report reads a recorded score table.",
|
|
107
|
-
nextCall: null,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
async function generateCommand(flags) {
|
|
111
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
112
|
-
const outDir = path.resolve(requireText(flags, "out"));
|
|
113
|
-
const perFile = int(flags, "per-file");
|
|
114
|
-
const result = generateMutants({
|
|
115
|
-
packageDir,
|
|
116
|
-
srcDir: text(flags, "src") ?? "src",
|
|
117
|
-
outDir,
|
|
118
|
-
perFile: perFile === undefined ? null : perFile,
|
|
119
|
-
skip: int(flags, "skip") ?? 0,
|
|
120
|
-
seed: int(flags, "seed") ?? 20261003,
|
|
121
|
-
maxMutants: int(flags, "max-mutants") ?? null,
|
|
122
|
-
maxMinutes: int(flags, "max-minutes") ?? null,
|
|
123
|
-
skipFiles: texts(flags, "skip-file"),
|
|
124
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
125
|
-
workingTree: bool(flags, "working-tree", false),
|
|
126
|
-
operators: operatorSet(flags),
|
|
127
|
-
});
|
|
128
|
-
if (result.error) {
|
|
129
|
-
return {
|
|
130
|
-
schemaVersion: SCHEMA_VERSION,
|
|
131
|
-
ok: false,
|
|
132
|
-
command: "mutate.generate",
|
|
133
|
-
summary: result.error,
|
|
134
|
-
next: "Commit the file, or pass --working-tree to mutate the checkout on disk. mutate run scores the commit.",
|
|
135
|
-
nextCall: null,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
if (result.violations.length > 0) {
|
|
139
|
-
const first = result.violations[0];
|
|
140
|
-
return {
|
|
141
|
-
schemaVersion: SCHEMA_VERSION,
|
|
142
|
-
ok: false,
|
|
143
|
-
command: "mutate.generate",
|
|
144
|
-
summary: `Refusing to write mutants: ${first.file}:${first.line} sits inside a string or a comment.`,
|
|
145
|
-
next: "That is a generator bug. Do not use the batch.",
|
|
146
|
-
nextCall: null,
|
|
147
|
-
stringLiteralMutants: result.violations.length,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
const shown = result.mutants.slice(0, 10).map(({ id, file, line, op }) => ({ id, file, line, op }));
|
|
151
|
-
const patches = path.join(outDir, "mutants");
|
|
152
|
-
const stopped = result.budgetHit;
|
|
153
|
-
const suite = discoverSuite(packageDir, text(flags, "tests-dir") ?? "tests");
|
|
154
|
-
return {
|
|
155
|
-
schemaVersion: SCHEMA_VERSION,
|
|
156
|
-
ok: true,
|
|
157
|
-
command: "mutate.generate",
|
|
158
|
-
summary: stopped
|
|
159
|
-
? `Stopped by the time budget after ${result.filesVisited} files. ${result.mutants.length} mutants written.`
|
|
160
|
-
: `${result.mutants.length} mutants in ${result.filesVisited} files. No string-literal mutant.`,
|
|
161
|
-
next: [
|
|
162
|
-
stopped
|
|
163
|
-
? "Narrow --src or raise --max-minutes, then run the same command."
|
|
164
|
-
: generateNext(result.mutants.length, suite ? { kind: suite.kind, command: suite.command } : null),
|
|
165
|
-
result.source === "working-tree"
|
|
166
|
-
? "These patches match the working tree. mutate run scores the commit, so commit the files before mutate run."
|
|
167
|
-
: "",
|
|
168
|
-
].filter((line) => line.length > 0).join(" "),
|
|
169
|
-
nextCall: result.mutants.length === 0
|
|
170
|
-
? null
|
|
171
|
-
: { argv: ["mutate", "run", "--package", packageDir, "--patches", patches, "--out", path.join(outDir, "runs")] },
|
|
172
|
-
mutantCount: result.mutants.length,
|
|
173
|
-
operators: operatorSet(flags),
|
|
174
|
-
stringLiteralMutants: 0,
|
|
175
|
-
filesVisited: result.filesVisited,
|
|
176
|
-
budgetHit: result.budgetHit,
|
|
177
|
-
mutants: shown,
|
|
178
|
-
rest: Math.max(result.mutants.length - shown.length, 0),
|
|
179
|
-
full: path.join(patches, "mutants.json"),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
async function runCommand(flags, hide = null) {
|
|
183
|
-
const hidden = hide ?? texts(flags, "hide");
|
|
184
|
-
const escaped = hidden.find((rel) => rel.split(/[\\/]/).includes(".."));
|
|
185
|
-
if (escaped) {
|
|
186
|
-
return {
|
|
187
|
-
schemaVersion: SCHEMA_VERSION,
|
|
188
|
-
ok: false,
|
|
189
|
-
command: "mutate.run",
|
|
190
|
-
summary: `--hide stays inside the package. Refused ${escaped}.`,
|
|
191
|
-
next: "Pass a relative path with no .. segment.",
|
|
192
|
-
nextCall: null,
|
|
193
|
-
killed: 0,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const onlyNames = texts(flags, "only-test");
|
|
197
|
-
if (onlyNames.some((id) => id === "::command" || id.endsWith("::command"))) {
|
|
198
|
-
return {
|
|
199
|
-
schemaVersion: SCHEMA_VERSION,
|
|
200
|
-
ok: false,
|
|
201
|
-
command: "mutate.run",
|
|
202
|
-
summary: "::command is not a test id.",
|
|
203
|
-
next: "Pass a test name the suite can collect.",
|
|
204
|
-
nextCall: null,
|
|
205
|
-
killed: 0,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
if (onlyNames.length > 0 && text(flags, "suite-command")) {
|
|
209
|
-
return {
|
|
210
|
-
schemaVersion: SCHEMA_VERSION,
|
|
211
|
-
ok: false,
|
|
212
|
-
command: "mutate.run",
|
|
213
|
-
summary: "--only-test cannot be combined with --suite-command.",
|
|
214
|
-
next: "Drop --suite-command and pass the keep ids with --only-test.",
|
|
215
|
-
nextCall: null,
|
|
216
|
-
killed: 0,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
220
|
-
const patchDirs = texts(flags, "patches").map((dir) => path.resolve(dir));
|
|
221
|
-
if (patchDirs.length === 0)
|
|
222
|
-
throw new Error("--patches is required");
|
|
223
|
-
const outDir = path.resolve(requireText(flags, "out"));
|
|
224
|
-
const repoDir = path.resolve(text(flags, "repo") ?? gitRoot(packageDir));
|
|
225
|
-
const workers = int(flags, "workers") ?? 1;
|
|
226
|
-
if (workers < 1)
|
|
227
|
-
throw new Error("--workers must be at least 1");
|
|
228
|
-
const direction = text(flags, "direction") ?? "auto";
|
|
229
|
-
if (direction !== "auto" && direction !== "forward" && direction !== "reverse") {
|
|
230
|
-
throw new Error("--direction must be auto, forward, or reverse");
|
|
231
|
-
}
|
|
232
|
-
const build = buildCommand(flags);
|
|
233
|
-
const report = await runMutants({
|
|
234
|
-
packageDir,
|
|
235
|
-
repoDir,
|
|
236
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
237
|
-
patchDirs,
|
|
238
|
-
outDir,
|
|
239
|
-
direction,
|
|
240
|
-
workers,
|
|
241
|
-
concurrency: int(flags, "concurrency") ?? 3,
|
|
242
|
-
maxMinutes: int(flags, "max-minutes") ?? null,
|
|
243
|
-
maxMutants: int(flags, "max-mutants") ?? null,
|
|
244
|
-
build,
|
|
245
|
-
testsDir: text(flags, "tests-dir") ?? "tests",
|
|
246
|
-
unset: texts(flags, "unset"),
|
|
247
|
-
suiteTimeoutMs: int(flags, "suite-timeout-ms") ?? 600_000,
|
|
248
|
-
testTimeoutMs: int(flags, "test-timeout-ms") ?? 60_000,
|
|
249
|
-
confirm: bool(flags, "confirm", true),
|
|
250
|
-
affected: bool(flags, "affected", false),
|
|
251
|
-
agent: text(flags, "agent") ?? null,
|
|
252
|
-
suiteCommand: text(flags, "suite-command") ?? null,
|
|
253
|
-
onlyNames: onlyNames.length > 0 ? onlyNames : null,
|
|
254
|
-
historyPath: text(flags, "history") ? path.resolve(text(flags, "history")) : null,
|
|
255
|
-
timeoutMultiple: int(flags, "timeout-multiple") ?? 5,
|
|
256
|
-
timeoutFloorMs: int(flags, "timeout-floor-ms") ?? 20_000,
|
|
257
|
-
budgetMs: int(flags, "budget-ms") ?? null,
|
|
258
|
-
hide,
|
|
259
|
-
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
260
|
-
});
|
|
261
|
-
const resume = report.budgetHit
|
|
262
|
-
? { argv: ["mutate", "run", "--package", packageDir, "--repo", repoDir, "--out", outDir, ...patchDirs.flatMap((dir) => ["--patches", dir])] }
|
|
263
|
-
: null;
|
|
264
|
-
return {
|
|
265
|
-
schemaVersion: SCHEMA_VERSION,
|
|
266
|
-
ok: report.ok,
|
|
267
|
-
command: "mutate.run",
|
|
268
|
-
summary: report.summary,
|
|
269
|
-
next: report.next,
|
|
270
|
-
nextCall: report.ok ? resume : null,
|
|
271
|
-
killed: report.killed,
|
|
272
|
-
unviable: report.unviable,
|
|
273
|
-
unviableIds: report.unviableIds,
|
|
274
|
-
survived: report.survived,
|
|
275
|
-
flaky: report.flaky,
|
|
276
|
-
timeouts: report.timeouts,
|
|
277
|
-
errors: report.errors,
|
|
278
|
-
noCoverage: report.noCoverage,
|
|
279
|
-
notStarted: report.notStarted,
|
|
280
|
-
suiteCommand: report.suiteCommand,
|
|
281
|
-
baselineMs: report.baselineMs,
|
|
282
|
-
commands: report.commands,
|
|
283
|
-
gaps: report.gaps,
|
|
284
|
-
rest: report.rest,
|
|
285
|
-
flakyIds: report.flakyIds,
|
|
286
|
-
budgetHit: report.budgetHit,
|
|
287
|
-
commit: report.commit,
|
|
288
|
-
runId: report.runId,
|
|
289
|
-
kills: report.kills,
|
|
290
|
-
full: report.full,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function tallyCommand(flags) {
|
|
294
|
-
return tallyRun(path.resolve(requireText(flags, "out")));
|
|
295
|
-
}
|
|
296
|
-
async function sealedCommand(flags) {
|
|
297
|
-
const label = readSealedLabel(path.resolve(requireText(flags, "label")));
|
|
298
|
-
const hide = texts(flags, "hide");
|
|
299
|
-
const outDir = path.resolve(requireText(flags, "out"));
|
|
300
|
-
const report = await runCommand(flags, hide);
|
|
301
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
302
|
-
const repoDir = path.resolve(text(flags, "repo") ?? gitRoot(packageDir));
|
|
303
|
-
const kills = Array.isArray(report.kills) ? report.kills : [];
|
|
304
|
-
// The fix commit is the scored commit unless --fix names an earlier one (new tests landed on top of it).
|
|
305
|
-
const scored = typeof report.commit === "string" ? report.commit : "";
|
|
306
|
-
const fixRef = text(flags, "fix");
|
|
307
|
-
const fixSha = fixRef ? git(repoDir, ["rev-parse", "--verify", `${fixRef}^{commit}`]) : null;
|
|
308
|
-
if (fixSha && fixSha.status !== 0)
|
|
309
|
-
throw new Error(`--fix ${fixRef} does not resolve`);
|
|
310
|
-
const commit = fixSha ? fixSha.stdout.trim() : scored;
|
|
311
|
-
const edited = report.ok && commit ? fixEditedKillers(repoDir, packageDir, commit, hide, kills) : { files: [], olderTestCatch: false };
|
|
312
|
-
const fromFix = kills.length > 0 && !edited.olderTestCatch;
|
|
313
|
-
const judged = {
|
|
314
|
-
...report,
|
|
315
|
-
...(fromFix
|
|
316
|
-
? {
|
|
317
|
-
summary: `${report.summary} Every kill came from a test edited by the fix commit (${edited.files.join(", ")}). That is the fix's own test, so no older test caught the bug.`,
|
|
318
|
-
next: "Record this as a miss. Do not restore the hidden test.",
|
|
319
|
-
}
|
|
320
|
-
: {}),
|
|
321
|
-
fixEdited: edited.files,
|
|
322
|
-
olderTestCatch: edited.olderTestCatch,
|
|
323
|
-
};
|
|
324
|
-
const body = JSON.stringify(judged);
|
|
325
|
-
if (!labelLeaked(body, label) && !sealedTreeLeaked(outDir, label))
|
|
326
|
-
return judged;
|
|
327
|
-
return {
|
|
328
|
-
schemaVersion: SCHEMA_VERSION,
|
|
329
|
-
ok: false,
|
|
330
|
-
command: "mutate.sealed",
|
|
331
|
-
summary: LABEL_LEAK_SUMMARY,
|
|
332
|
-
next: "Keep the label in its own file. Run the same command again.",
|
|
333
|
-
nextCall: null,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
async function ledgerCommand(flags) {
|
|
337
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
338
|
-
const outDir = path.resolve(requireText(flags, "out"));
|
|
339
|
-
const repoDir = path.resolve(text(flags, "repo") ?? gitRoot(packageDir));
|
|
340
|
-
const report = buildLedger({
|
|
341
|
-
packageDir,
|
|
342
|
-
repoDir,
|
|
343
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
344
|
-
outDir,
|
|
345
|
-
srcDir: text(flags, "src") ?? "src",
|
|
346
|
-
testsDir: text(flags, "tests-dir") ?? "tests",
|
|
347
|
-
maxLines: int(flags, "max-lines") ?? 300,
|
|
348
|
-
maxCommits: int(flags, "max-commits") ?? null,
|
|
349
|
-
});
|
|
350
|
-
return {
|
|
351
|
-
schemaVersion: SCHEMA_VERSION,
|
|
352
|
-
ok: report.ok,
|
|
353
|
-
command: "ledger.build",
|
|
354
|
-
summary: report.summary,
|
|
355
|
-
next: report.next,
|
|
356
|
-
nextCall: report.ok ? report.nextCall : null,
|
|
357
|
-
clean: report.clean,
|
|
358
|
-
hand: report.hand,
|
|
359
|
-
handmade: report.handmade,
|
|
360
|
-
skipped: report.skipped,
|
|
361
|
-
handNeeded: report.handNeeded,
|
|
362
|
-
rest: report.rest,
|
|
363
|
-
commit: report.commit,
|
|
364
|
-
full: report.full,
|
|
365
|
-
historyBudgetHit: report.historyBudgetHit,
|
|
366
|
-
commitsScanned: report.commitsScanned,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
function matrixCommand(flags) {
|
|
370
|
-
const scoresPath = requireText(flags, "scores");
|
|
371
|
-
const directPath = requireText(flags, "direct");
|
|
372
|
-
const outDir = path.resolve(requireText(flags, "out"));
|
|
373
|
-
const killsPath = text(flags, "kills");
|
|
374
|
-
return reportMatrix({
|
|
375
|
-
scores: readFileSync(scoresPath, "utf8"),
|
|
376
|
-
direct: readFileSync(directPath, "utf8"),
|
|
377
|
-
patchDirs: texts(flags, "patches").map((dir) => path.resolve(dir)),
|
|
378
|
-
outDir,
|
|
379
|
-
kills: killsPath ? readKills(killsPath) : new Map(),
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
function goldenCommand(flags) {
|
|
383
|
-
const recordedPath = requireText(flags, "recorded");
|
|
384
|
-
const recordedText = readFileSync(recordedPath, "utf8");
|
|
385
|
-
const updateRaw = text(flags, "update");
|
|
386
|
-
if (updateRaw && updateRaw !== "wording" && updateRaw !== "all")
|
|
387
|
-
throw new Error("--update must be wording or all");
|
|
388
|
-
const result = checkGolden({
|
|
389
|
-
recorded: asTable(recordedText),
|
|
390
|
-
actual: readGolden(requireText(flags, "actual")),
|
|
391
|
-
update: updateRaw === "wording" || updateRaw === "all" ? updateRaw : null,
|
|
392
|
-
message: text(flags, "message") ?? "",
|
|
393
|
-
roots: texts(flags, "root").map((dir) => path.resolve(dir)),
|
|
394
|
-
});
|
|
395
|
-
if (result.write)
|
|
396
|
-
writeGolden(recordedPath, result.table, goldenShape(recordedText));
|
|
397
|
-
return result.envelope;
|
|
398
|
-
}
|
|
399
|
-
function gapFixCommand(flags) {
|
|
400
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
401
|
-
const line = int(flags, "line");
|
|
402
|
-
if (line === undefined || line < 1)
|
|
403
|
-
throw new Error("--line must be a positive integer");
|
|
404
|
-
const state = fixGap(loadState(dir), {
|
|
405
|
-
id: requireText(flags, "id"),
|
|
406
|
-
file: requireText(flags, "file"),
|
|
407
|
-
line,
|
|
408
|
-
guard: requireText(flags, "guard"),
|
|
409
|
-
commit: requireText(flags, "commit"),
|
|
410
|
-
root: path.resolve(text(flags, "root") ?? dir),
|
|
411
|
-
});
|
|
412
|
-
saveState(dir, state);
|
|
413
|
-
const gap = state.gaps.find((item) => item.id === requireText(flags, "id"));
|
|
414
|
-
return {
|
|
415
|
-
schemaVersion: SCHEMA_VERSION,
|
|
416
|
-
ok: true,
|
|
417
|
-
command: "gap.fix",
|
|
418
|
-
summary: `${gap?.id} is fixed, guarded by ${gap?.guard}.`,
|
|
419
|
-
next: "Run status.",
|
|
420
|
-
nextCall: { argv: ["status", "--state", dir] },
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
function gapRevertCommand(flags) {
|
|
424
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
425
|
-
const reverted = revertGap(loadState(dir), requireText(flags, "id"));
|
|
426
|
-
saveState(dir, reverted.state);
|
|
427
|
-
return {
|
|
428
|
-
schemaVersion: SCHEMA_VERSION,
|
|
429
|
-
ok: true,
|
|
430
|
-
command: "gap.revert",
|
|
431
|
-
summary: reverted.text,
|
|
432
|
-
next: reverted.text,
|
|
433
|
-
nextCall: null,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function guardCommand(flags) {
|
|
437
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
438
|
-
const guard = requireText(flags, "guard");
|
|
439
|
-
parseGuard(guard);
|
|
440
|
-
const result = guardAllows(loadState(dir), guard, text(flags, "message") ?? "", path.resolve(text(flags, "root") ?? dir));
|
|
441
|
-
return {
|
|
442
|
-
schemaVersion: SCHEMA_VERSION,
|
|
443
|
-
ok: result.ok,
|
|
444
|
-
command: "guard.check",
|
|
445
|
-
summary: result.summary,
|
|
446
|
-
next: result.ok ? "The guard check passed." : "Add a Guard-Change trailer naming the guard, or restore it.",
|
|
447
|
-
nextCall: null,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
function findingsCommand(flags) {
|
|
451
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
452
|
-
const status = text(flags, "status");
|
|
453
|
-
if (status !== "open" && status !== "fixed" && status !== "equivalent" && status !== "wont-fix") {
|
|
454
|
-
throw new Error("--status must be open, fixed, equivalent, or wont-fix");
|
|
455
|
-
}
|
|
456
|
-
const id = requireText(flags, "id");
|
|
457
|
-
const reason = requireText(flags, "reason");
|
|
458
|
-
const finding = {
|
|
459
|
-
id,
|
|
460
|
-
status,
|
|
461
|
-
reason,
|
|
462
|
-
};
|
|
463
|
-
const agent = text(flags, "agent");
|
|
464
|
-
if (agent)
|
|
465
|
-
finding.agent = agent;
|
|
466
|
-
const file = text(flags, "file");
|
|
467
|
-
const line = int(flags, "line");
|
|
468
|
-
if (file && line !== undefined) {
|
|
469
|
-
const source = readSourceLine(path.resolve(text(flags, "root") ?? dir, file), line);
|
|
470
|
-
if (source === null)
|
|
471
|
-
throw new Error("decision line does not exist");
|
|
472
|
-
finding.file = file;
|
|
473
|
-
finding.line = line;
|
|
474
|
-
finding.hash = lineHash(source);
|
|
475
|
-
const state = loadState(dir);
|
|
476
|
-
const decisions = state.decisions.filter((item) => item.id !== id);
|
|
477
|
-
decisions.push({ id, file, line, hash: finding.hash, status, reason });
|
|
478
|
-
saveState(dir, { ...state, decisions });
|
|
479
|
-
}
|
|
480
|
-
appendFinding(dir, finding);
|
|
481
|
-
return {
|
|
482
|
-
schemaVersion: SCHEMA_VERSION,
|
|
483
|
-
ok: true,
|
|
484
|
-
command: "findings.add",
|
|
485
|
-
summary: `Recorded ${id} as ${status}.`,
|
|
486
|
-
next: "Run status.",
|
|
487
|
-
nextCall: null,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
function statusCommand(flags) {
|
|
491
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
492
|
-
const root = path.resolve(text(flags, "root") ?? dir);
|
|
493
|
-
const pagePath = path.resolve(text(flags, "page") ?? "PROBATIO.md");
|
|
494
|
-
const scoresPath = text(flags, "scores");
|
|
495
|
-
const score = scoresPath ? JSON.parse(readFileSync(scoresPath, "utf8")) : null;
|
|
496
|
-
const view = statusEnvelope(dir, root, score, text(flags, "agent") ?? null);
|
|
497
|
-
mkdirSync(path.dirname(pagePath), { recursive: true });
|
|
498
|
-
writeFileSync(pagePath, view.page);
|
|
499
|
-
return view.envelope;
|
|
500
|
-
}
|
|
501
|
-
function sealCommand(flags) {
|
|
502
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
503
|
-
const from = text(flags, "from");
|
|
504
|
-
let ids = texts(flags, "id");
|
|
505
|
-
if (from) {
|
|
506
|
-
const parsed = JSON.parse(readFileSync(from, "utf8"));
|
|
507
|
-
const list = Array.isArray(parsed) ? parsed : parsed.ids ?? [];
|
|
508
|
-
const fraction = Number(text(flags, "fraction") ?? "0.2");
|
|
509
|
-
if (!Number.isFinite(fraction) || fraction < 0 || fraction > 1)
|
|
510
|
-
throw new Error("--fraction must be between 0 and 1");
|
|
511
|
-
ids = chooseSealed(list, fraction, int(flags, "seed") ?? 20261004);
|
|
512
|
-
}
|
|
513
|
-
sealIds(dir, ids);
|
|
514
|
-
return {
|
|
515
|
-
schemaVersion: SCHEMA_VERSION,
|
|
516
|
-
ok: true,
|
|
517
|
-
command: "seal",
|
|
518
|
-
summary: `Sealed ${ids.length} ids.`,
|
|
519
|
-
next: "status, PROBATIO.md, and the queue omit them.",
|
|
520
|
-
nextCall: null,
|
|
521
|
-
sealedCount: ids.length,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
function queueSeedCommand(flags) {
|
|
525
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
526
|
-
const written = seedQueue(dir, texts(flags, "id"));
|
|
527
|
-
return {
|
|
528
|
-
schemaVersion: SCHEMA_VERSION,
|
|
529
|
-
ok: true,
|
|
530
|
-
command: "queue.seed",
|
|
531
|
-
summary: `Seeded ${written.length} queue items.`,
|
|
532
|
-
next: "Claim one.",
|
|
533
|
-
nextCall: null,
|
|
534
|
-
seeded: written,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function queueClaimCommand(flags) {
|
|
538
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
539
|
-
const now = int(flags, "now") ?? Date.now();
|
|
540
|
-
const lease = int(flags, "lease-ms") ?? 600_000;
|
|
541
|
-
const result = claimItem(dir, requireText(flags, "id"), requireText(flags, "agent"), now, lease);
|
|
542
|
-
return {
|
|
543
|
-
schemaVersion: SCHEMA_VERSION,
|
|
544
|
-
ok: result.ok,
|
|
545
|
-
command: "queue.claim",
|
|
546
|
-
summary: result.summary,
|
|
547
|
-
next: result.ok ? "Work the claim, then check-kill it." : "Pick another item.",
|
|
548
|
-
nextCall: null,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
function queueReapCommand(flags) {
|
|
552
|
-
const dir = path.resolve(requireText(flags, "state"));
|
|
553
|
-
const now = int(flags, "now") ?? Date.now();
|
|
554
|
-
const returned = reapClaims(dir, now);
|
|
555
|
-
return {
|
|
556
|
-
schemaVersion: SCHEMA_VERSION,
|
|
557
|
-
ok: true,
|
|
558
|
-
command: "queue.reap",
|
|
559
|
-
summary: `Returned ${returned.length} expired claims.`,
|
|
560
|
-
next: returned.length === 0 ? "Nothing expired." : "The expired claims are back in the queue.",
|
|
561
|
-
nextCall: null,
|
|
562
|
-
returned,
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
async function checkKillCommand(flags, id) {
|
|
566
|
-
if (!id)
|
|
567
|
-
throw new Error("check-kill needs a mutant id");
|
|
568
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
569
|
-
const patchDirs = texts(flags, "patches").map((dir) => path.resolve(dir));
|
|
570
|
-
if (patchDirs.length === 0)
|
|
571
|
-
throw new Error("--patches is required");
|
|
572
|
-
const build = buildCommand(flags);
|
|
573
|
-
return checkKill(id, {
|
|
574
|
-
packageDir,
|
|
575
|
-
repoDir: path.resolve(text(flags, "repo") ?? gitRoot(packageDir)),
|
|
576
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
577
|
-
patchDirs,
|
|
578
|
-
outDir: path.resolve(requireText(flags, "out")),
|
|
579
|
-
direction: "forward",
|
|
580
|
-
workers: 1,
|
|
581
|
-
concurrency: 1,
|
|
582
|
-
maxMinutes: int(flags, "max-minutes") ?? 3,
|
|
583
|
-
maxMutants: null,
|
|
584
|
-
build,
|
|
585
|
-
testsDir: text(flags, "tests-dir") ?? "tests",
|
|
586
|
-
unset: ["SOLARI_API_KEY", "AUSPEX_LIVE"],
|
|
587
|
-
suiteTimeoutMs: int(flags, "suite-timeout-ms") ?? 60_000,
|
|
588
|
-
testTimeoutMs: int(flags, "test-timeout-ms") ?? 30_000,
|
|
589
|
-
confirm: bool(flags, "confirm", true),
|
|
590
|
-
affected: false,
|
|
591
|
-
agent: text(flags, "agent") ?? null,
|
|
592
|
-
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
async function ledgerCheckCommand(flags) {
|
|
596
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
597
|
-
return checkLedger({
|
|
598
|
-
packageDir,
|
|
599
|
-
repoDir: path.resolve(text(flags, "repo") ?? gitRoot(packageDir)),
|
|
600
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
601
|
-
ledgerDir: path.resolve(packageDir, text(flags, "ledger") ?? "ledger"),
|
|
602
|
-
outDir: path.resolve(requireText(flags, "out")),
|
|
603
|
-
update: bool(flags, "update", false),
|
|
604
|
-
message: text(flags, "message") ?? "",
|
|
605
|
-
concurrency: int(flags, "concurrency") ?? 3,
|
|
606
|
-
suiteTimeoutMs: int(flags, "suite-timeout-ms") ?? 600_000,
|
|
607
|
-
testTimeoutMs: int(flags, "test-timeout-ms") ?? 60_000,
|
|
608
|
-
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
async function goldenRecordCommand(flags) {
|
|
612
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
613
|
-
return recordGoldens({
|
|
614
|
-
packageDir,
|
|
615
|
-
repoDir: path.resolve(text(flags, "repo") ?? gitRoot(packageDir)),
|
|
616
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
617
|
-
modules: texts(flags, "module"),
|
|
618
|
-
tests: texts(flags, "tests"),
|
|
619
|
-
outDir: text(flags, "dir") ?? "tests/golden",
|
|
620
|
-
timeoutMs: int(flags, "suite-timeout-ms") ?? 600_000,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
async function goldenCompareCommand(flags) {
|
|
624
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
625
|
-
return compareGoldens({
|
|
626
|
-
packageDir,
|
|
627
|
-
repoDir: path.resolve(text(flags, "repo") ?? gitRoot(packageDir)),
|
|
628
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
629
|
-
modules: texts(flags, "module"),
|
|
630
|
-
tests: texts(flags, "tests"),
|
|
631
|
-
golden: texts(flags, "golden"),
|
|
632
|
-
outDir: path.resolve(requireText(flags, "out")),
|
|
633
|
-
operators: operatorSet(flags),
|
|
634
|
-
timeoutMs: int(flags, "suite-timeout-ms") ?? 600_000,
|
|
635
|
-
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
async function verifyCommand(flags) {
|
|
639
|
-
const packageDir = path.resolve(requireText(flags, "package"));
|
|
640
|
-
const operators = operatorSet(flags);
|
|
641
|
-
const envelope = await verifyChange({
|
|
642
|
-
packageDir,
|
|
643
|
-
repoDir: path.resolve(text(flags, "repo") ?? gitRoot(packageDir)),
|
|
644
|
-
outDir: path.resolve(requireText(flags, "out")),
|
|
645
|
-
base: text(flags, "base") ?? "HEAD",
|
|
646
|
-
commit: text(flags, "commit") ?? "HEAD",
|
|
647
|
-
maxMutants: int(flags, "max-mutants") ?? 4,
|
|
648
|
-
maxMinutes: int(flags, "max-minutes") ?? 3,
|
|
649
|
-
maxTests: int(flags, "max-tests") ?? null,
|
|
650
|
-
operators,
|
|
651
|
-
onProgress: (line) => process.stderr.write(`${line}\n`),
|
|
652
|
-
});
|
|
653
|
-
return { ...envelope, operators };
|
|
654
|
-
}
|
|
655
|
-
/** `--operators core` (default) or `--operators wide`. Anything else is a usage error, not a silent default. */
|
|
656
|
-
function operatorSet(flags) {
|
|
657
|
-
const value = text(flags, "operators") ?? "core";
|
|
658
|
-
if (value !== "core" && value !== "wide")
|
|
659
|
-
throw new Error("--operators must be core or wide");
|
|
660
|
-
return value;
|
|
661
|
-
}
|
|
662
|
-
/** The published JSON Schema of one command's output. MCP-only agents read the contract this way. */
|
|
663
|
-
function schemaCommand(name) {
|
|
664
|
-
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "schemas");
|
|
665
|
-
const commands = existsSync(dir)
|
|
666
|
-
? readdirSync(dir).filter((file) => file.endsWith(".schema.json")).map((file) => file.slice(0, -".schema.json".length)).sort()
|
|
667
|
-
: [];
|
|
668
|
-
if (!name) {
|
|
669
|
-
return {
|
|
670
|
-
schemaVersion: SCHEMA_VERSION,
|
|
671
|
-
ok: commands.length > 0,
|
|
672
|
-
command: "schema",
|
|
673
|
-
summary: `${commands.length} commands have a published schema.`,
|
|
674
|
-
next: "Pass one name, for example: probatio schema mutate.run",
|
|
675
|
-
nextCall: null,
|
|
676
|
-
commands,
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
if (!commands.includes(name)) {
|
|
680
|
-
return {
|
|
681
|
-
schemaVersion: SCHEMA_VERSION,
|
|
682
|
-
ok: false,
|
|
683
|
-
command: "schema",
|
|
684
|
-
summary: `No schema named ${name}.`,
|
|
685
|
-
next: `Use one of: ${commands.join(", ")}.`,
|
|
686
|
-
nextCall: null,
|
|
687
|
-
commands,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
return {
|
|
691
|
-
schemaVersion: SCHEMA_VERSION,
|
|
692
|
-
ok: true,
|
|
693
|
-
command: "schema",
|
|
694
|
-
summary: `JSON Schema for ${name}, schemaVersion ${SCHEMA_VERSION}.`,
|
|
695
|
-
next: "Validate a command's stdout against it. A new field does not change schemaVersion. A renamed or removed one does.",
|
|
696
|
-
nextCall: null,
|
|
697
|
-
commands,
|
|
698
|
-
schema: JSON.parse(readFileSync(path.join(dir, `${name}.schema.json`), "utf8")),
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
function gitRoot(cwd) {
|
|
702
|
-
const result = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
703
|
-
if (result.status !== 0)
|
|
704
|
-
throw new Error("package is not inside a git repository. Pass --repo.");
|
|
705
|
-
return result.stdout.trim();
|
|
706
|
-
}
|
|
707
|
-
function finish(envelope, human) {
|
|
708
|
-
process.stdout.write(render(envelope, human));
|
|
709
|
-
process.exit(envelope.ok ? 0 : 1);
|
|
710
|
-
}
|
|
711
|
-
/** Omitted `--build` and `--no-build` both mean no build. `--build <cmd>` opts in. */
|
|
712
|
-
function buildCommand(flags) {
|
|
713
|
-
const values = flags.get("build");
|
|
714
|
-
if (!values || values.length === 0)
|
|
715
|
-
return null;
|
|
716
|
-
const last = values[values.length - 1];
|
|
717
|
-
if (typeof last !== "string")
|
|
718
|
-
return null;
|
|
719
|
-
const parts = last.split(" ").filter(Boolean);
|
|
720
|
-
return parts.length > 0 ? parts : null;
|
|
721
|
-
}
|