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