probatio 0.1.2 → 0.2.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 +109 -17
- package/dist/cli.js +122 -55
- package/dist/contract.js +7 -5
- package/dist/ledger/build.js +14 -0
- package/dist/mcp.js +98 -26
- package/dist/mutate/child-lines.mjs +23 -18
- package/dist/mutate/coverage-map.js +75 -18
- package/dist/mutate/fail-reporter.mjs +21 -0
- package/dist/mutate/generate.js +25 -4
- package/dist/mutate/load-failure.mjs +40 -0
- package/dist/mutate/mocha-coverage.cjs +7 -1
- package/dist/mutate/node-batch-loader.mjs +15 -1
- package/dist/mutate/node-batch.mjs +40 -3
- package/dist/mutate/node-coverage.mjs +7 -32
- package/dist/mutate/patch.js +2 -1
- package/dist/mutate/precise-lines.mjs +147 -31
- package/dist/mutate/probatio-jacoco-run.java +2 -2
- package/dist/mutate/project-config.js +58 -0
- package/dist/mutate/run.js +192 -32
- package/dist/mutate/rust-coverage.js +4 -19
- package/dist/mutate/sealed.js +94 -1
- package/dist/mutate/suite-decision.js +2 -1
- package/dist/mutate/suites.js +136 -163
- package/dist/mutate/tally.js +85 -20
- package/dist/node-version.js +13 -0
- package/dist/swarm/check-kill.js +14 -7
- package/dist/swarm/queue.js +13 -7
- package/dist/verify/change.js +5 -0
- package/package.json +6 -4
package/dist/swarm/check-kill.js
CHANGED
|
@@ -4,8 +4,8 @@ import { SCHEMA_VERSION } from "../contract.js";
|
|
|
4
4
|
import { directImporters } from "../mutate/affected.js";
|
|
5
5
|
import { filesInDiff } from "../mutate/patch.js";
|
|
6
6
|
import { killLabel } from "../mutate/kill-label.js";
|
|
7
|
-
import { runMutants } from "../mutate/run.js";
|
|
8
|
-
import { campaignSummary } from "../mutate/suite-decision.js";
|
|
7
|
+
import { normalizeResult, runMutants } from "../mutate/run.js";
|
|
8
|
+
import { campaignSummary, UNVIABLE_NEXT } from "../mutate/suite-decision.js";
|
|
9
9
|
/** The gap is done only when this mutant is killed and the baseline suite is green. */
|
|
10
10
|
export async function checkKill(id, options) {
|
|
11
11
|
const importers = importersOf(id, options);
|
|
@@ -48,18 +48,24 @@ export async function checkKill(id, options) {
|
|
|
48
48
|
let outcome = null;
|
|
49
49
|
let killedBy = [];
|
|
50
50
|
if (existsSync(resultFile)) {
|
|
51
|
-
const result = JSON.parse(readFileSync(resultFile, "utf8"));
|
|
51
|
+
const result = normalizeResult(JSON.parse(readFileSync(resultFile, "utf8")));
|
|
52
52
|
outcome = result.outcome ?? null;
|
|
53
53
|
killedBy = Array.isArray(result.killedBy) ? result.killedBy : [];
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
55
|
+
// A mutant that does not build needs no test. The task is closed, and it is not called a kill.
|
|
56
|
+
const unviableMutant = suiteGreen && outcome === "unviable";
|
|
57
|
+
const done = (suiteGreen && outcome === "killed") || unviableMutant;
|
|
58
|
+
const label = unviableMutant ? { cause: "build", next: UNVIABLE_NEXT } : done ? killLabel(killedBy) : null;
|
|
57
59
|
const head = checkHead(report);
|
|
58
60
|
return {
|
|
59
61
|
schemaVersion: SCHEMA_VERSION,
|
|
60
62
|
ok: done,
|
|
61
63
|
command: "check-kill",
|
|
62
|
-
summary:
|
|
64
|
+
summary: unviableMutant
|
|
65
|
+
? `${head} ${id} does not build. No test is needed.`
|
|
66
|
+
: done
|
|
67
|
+
? `${head} ${id} is dead and the suite is green.`
|
|
68
|
+
: `${head} ${id} is not done.`,
|
|
63
69
|
next: label?.next ?? (done ? "The gap-fixing task is done." : "The task is not done."),
|
|
64
70
|
nextCall: null,
|
|
65
71
|
done,
|
|
@@ -78,11 +84,12 @@ function importersOf(id, options) {
|
|
|
78
84
|
return directImporters(options.packageDir, rels, options.testsDir);
|
|
79
85
|
}
|
|
80
86
|
function checkHead(report) {
|
|
81
|
-
const finished = report.killed + report.survived + report.flaky + report.timeouts + report.errors + report.noCoverage;
|
|
87
|
+
const finished = report.killed + report.unviable + report.survived + report.flaky + report.timeouts + report.errors + report.noCoverage;
|
|
82
88
|
return campaignSummary({
|
|
83
89
|
noCoverage: report.noCoverage,
|
|
84
90
|
survived: report.survived,
|
|
85
91
|
killed: report.killed,
|
|
92
|
+
unviable: report.unviable,
|
|
86
93
|
flaky: report.flaky,
|
|
87
94
|
timeouts: report.timeouts,
|
|
88
95
|
errors: report.errors,
|
package/dist/swarm/queue.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { loadSealed } from "../memory/state.js";
|
|
4
|
+
/** One path segment. Rejects slashes and `..` so a claim cannot leave the queue directory. */
|
|
5
|
+
export function safeFilePart(value) {
|
|
6
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..")
|
|
7
|
+
return null;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
4
10
|
export function seedQueue(root, ids) {
|
|
5
11
|
const sealed = new Set(loadSealed(root));
|
|
6
12
|
const queue = path.join(root, "queue");
|
|
7
13
|
mkdirSync(queue, { recursive: true });
|
|
8
14
|
const written = [];
|
|
9
15
|
for (const id of [...ids].sort()) {
|
|
10
|
-
if (sealed.has(id))
|
|
16
|
+
if (sealed.has(id) || !safeFilePart(id))
|
|
11
17
|
continue;
|
|
12
18
|
const dest = path.join(queue, `${id}.json`);
|
|
13
19
|
if (existsSync(dest))
|
|
@@ -19,23 +25,23 @@ export function seedQueue(root, ids) {
|
|
|
19
25
|
}
|
|
20
26
|
/** Claim by atomic rename. A second claim fails because the queue file is already gone. */
|
|
21
27
|
export function claimItem(root, id, agent, now, leaseMs) {
|
|
28
|
+
if (!safeFilePart(id) || !safeFilePart(agent))
|
|
29
|
+
return { ok: false, summary: "agent and id must be a single file name.", file: null };
|
|
22
30
|
const source = path.join(root, "queue", `${id}.json`);
|
|
23
31
|
const claimed = path.join(root, "claimed");
|
|
24
32
|
const dest = path.join(claimed, `${agent}-${id}.json`);
|
|
25
33
|
if (!existsSync(source))
|
|
26
34
|
return { ok: false, summary: `${id} is not in the queue.`, file: null };
|
|
27
35
|
mkdirSync(claimed, { recursive: true });
|
|
28
|
-
|
|
29
|
-
body.id = id;
|
|
30
|
-
body.agent = agent;
|
|
31
|
-
body.leasedUntil = now + leaseMs;
|
|
32
|
-
writeFileSync(source, `${JSON.stringify(body)}\n`);
|
|
36
|
+
// Rename first. Writing the body before the rename lets the other claim overwrite the winner.
|
|
33
37
|
try {
|
|
34
38
|
renameSync(source, dest);
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
41
|
return { ok: false, summary: `${id} was claimed by someone else.`, file: null };
|
|
38
42
|
}
|
|
43
|
+
const body = { id, agent, leasedUntil: now + leaseMs };
|
|
44
|
+
writeFileSync(dest, `${JSON.stringify(body)}\n`);
|
|
39
45
|
return { ok: true, summary: `${agent} claimed ${id} until ${body.leasedUntil}.`, file: dest };
|
|
40
46
|
}
|
|
41
47
|
/** Move claims whose lease has expired back to the queue. */
|
|
@@ -53,7 +59,7 @@ export function reapClaims(root, now) {
|
|
|
53
59
|
const body = JSON.parse(readFileSync(file, "utf8"));
|
|
54
60
|
if (typeof body.leasedUntil !== "number" || body.leasedUntil > now)
|
|
55
61
|
continue;
|
|
56
|
-
if (!body.id)
|
|
62
|
+
if (!body.id || !safeFilePart(body.id))
|
|
57
63
|
continue;
|
|
58
64
|
renameSync(file, path.join(queue, `${body.id}.json`));
|
|
59
65
|
returned.push(body.id);
|
package/dist/verify/change.js
CHANGED
|
@@ -23,6 +23,11 @@ export async function verifyChange(options) {
|
|
|
23
23
|
const ranges = parseUnified(diff.stdout);
|
|
24
24
|
const names = git(repo, ["diff", "--name-only", "--no-renames", options.base, options.commit, ...scope]);
|
|
25
25
|
const changed = names.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
26
|
+
const status = git(repo, ["status", "--porcelain", ...scope]);
|
|
27
|
+
const dirty = status.stdout.trim().length > 0;
|
|
28
|
+
if (dirty && (options.base === options.commit || changed.length === 0)) {
|
|
29
|
+
return fail(options.outDir, "The working tree has uncommitted changes. verify-change did not score them. Commit the edit, or pass --base and --commit that name different commits with a diff.");
|
|
30
|
+
}
|
|
26
31
|
const goldenContract = goldenDiffs(repo, options.base, options.commit, changed);
|
|
27
32
|
const packageFiles = [];
|
|
28
33
|
const chosen = [];
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "probatio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Mutation runner for AI agents. Scores a test suite by the bugs it catches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"probatio": "dist/cli.js"
|
|
7
|
+
"probatio": "dist/cli.js",
|
|
8
|
+
"probatio-mcp": "dist/mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist"
|
|
@@ -19,12 +20,13 @@
|
|
|
19
20
|
"url": "https://github.com/IronAdamant/Probatio/issues"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"build": "tsc -p tsconfig.json && mkdir -p dist/mutate && cp src/mutate/fail-reporter.mjs src/mutate/node-coverage.mjs src/mutate/precise-lines.mjs src/mutate/child-lines.mjs src/mutate/mocha-coverage.cjs src/mutate/node-batch.mjs src/mutate/node-batch-loader.mjs src/mutate/py-reporter.py src/mutate/py-pytest-reporter.py src/mutate/probatio-jacoco-run.java dist/mutate/",
|
|
23
|
+
"build": "tsc -p tsconfig.json && mkdir -p dist/mutate && cp src/mutate/fail-reporter.mjs src/mutate/node-coverage.mjs src/mutate/precise-lines.mjs src/mutate/child-lines.mjs src/mutate/mocha-coverage.cjs src/mutate/node-batch.mjs src/mutate/node-batch-loader.mjs src/mutate/load-failure.mjs src/mutate/py-reporter.py src/mutate/py-pytest-reporter.py src/mutate/probatio-jacoco-run.java dist/mutate/",
|
|
24
|
+
"prepack": "npm run build",
|
|
23
25
|
"test": "tsx --test tests/*.test.ts",
|
|
24
26
|
"probatio": "tsx src/cli.ts"
|
|
25
27
|
},
|
|
26
28
|
"engines": {
|
|
27
|
-
"node": ">=22"
|
|
29
|
+
"node": ">=22.6"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"typescript": "^5.7.2"
|