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.
Files changed (74) hide show
  1. package/README.md +10 -198
  2. package/bin/probatio.js +52 -0
  3. package/package.json +21 -29
  4. package/LICENSE +0 -21
  5. package/dist/cli.js +0 -721
  6. package/dist/contract.js +0 -32
  7. package/dist/flags.js +0 -87
  8. package/dist/golden/check.js +0 -188
  9. package/dist/golden/record.js +0 -395
  10. package/dist/golden/recorder.mjs +0 -78
  11. package/dist/ledger/build.js +0 -348
  12. package/dist/ledger/check.js +0 -133
  13. package/dist/matrix/report.js +0 -256
  14. package/dist/mcp.js +0 -188
  15. package/dist/memory/state.js +0 -222
  16. package/dist/mutate/affected.js +0 -173
  17. package/dist/mutate/child-lines.mjs +0 -39
  18. package/dist/mutate/coverage-map.js +0 -229
  19. package/dist/mutate/csharp-coverage.js +0 -161
  20. package/dist/mutate/fail-reporter.mjs +0 -65
  21. package/dist/mutate/find.js +0 -13
  22. package/dist/mutate/generate.js +0 -156
  23. package/dist/mutate/go-coverage.js +0 -100
  24. package/dist/mutate/ids.js +0 -22
  25. package/dist/mutate/java-coverage.js +0 -217
  26. package/dist/mutate/kill-label.js +0 -34
  27. package/dist/mutate/load-failure.mjs +0 -40
  28. package/dist/mutate/mocha-coverage.cjs +0 -77
  29. package/dist/mutate/node-batch-loader.mjs +0 -99
  30. package/dist/mutate/node-batch.mjs +0 -167
  31. package/dist/mutate/node-coverage.mjs +0 -77
  32. package/dist/mutate/operators.js +0 -181
  33. package/dist/mutate/patch.js +0 -136
  34. package/dist/mutate/precise-lines.mjs +0 -304
  35. package/dist/mutate/probatio-jacoco-run.java +0 -516
  36. package/dist/mutate/project-config.js +0 -58
  37. package/dist/mutate/py-pytest-reporter.py +0 -145
  38. package/dist/mutate/py-reporter.py +0 -79
  39. package/dist/mutate/run.js +0 -1335
  40. package/dist/mutate/rust-coverage.js +0 -144
  41. package/dist/mutate/sealed.js +0 -118
  42. package/dist/mutate/suite-decision.js +0 -130
  43. package/dist/mutate/suites.js +0 -1524
  44. package/dist/mutate/tally.js +0 -240
  45. package/dist/mutate/text-operators.js +0 -637
  46. package/dist/mutate/titles.js +0 -23
  47. package/dist/node-version.js +0 -13
  48. package/dist/swarm/check-kill.js +0 -114
  49. package/dist/swarm/queue.js +0 -68
  50. package/dist/verify/change.js +0 -340
  51. package/schemas/check-kill.schema.json +0 -93
  52. package/schemas/findings.add.schema.json +0 -53
  53. package/schemas/gap.fix.schema.json +0 -53
  54. package/schemas/gap.revert.schema.json +0 -53
  55. package/schemas/golden.check.schema.json +0 -89
  56. package/schemas/golden.compare.schema.json +0 -151
  57. package/schemas/golden.record.schema.json +0 -126
  58. package/schemas/guard.check.schema.json +0 -53
  59. package/schemas/ledger.build.schema.json +0 -132
  60. package/schemas/ledger.check.schema.json +0 -143
  61. package/schemas/matrix.report.schema.json +0 -129
  62. package/schemas/mcp.schema.json +0 -53
  63. package/schemas/mutate.generate.schema.json +0 -127
  64. package/schemas/mutate.help.schema.json +0 -53
  65. package/schemas/mutate.run.schema.json +0 -272
  66. package/schemas/mutate.sealed.schema.json +0 -53
  67. package/schemas/mutate.tally.schema.json +0 -137
  68. package/schemas/queue.claim.schema.json +0 -53
  69. package/schemas/queue.reap.schema.json +0 -71
  70. package/schemas/queue.seed.schema.json +0 -71
  71. package/schemas/schema.schema.json +0 -74
  72. package/schemas/seal.schema.json +0 -69
  73. package/schemas/status.schema.json +0 -103
  74. package/schemas/verify-change.schema.json +0 -222
@@ -1,114 +0,0 @@
1
- import { existsSync, readFileSync, realpathSync } from "node:fs";
2
- import path from "node:path";
3
- import { SCHEMA_VERSION } from "../contract.js";
4
- import { directImporters } from "../mutate/affected.js";
5
- import { filesInDiff } from "../mutate/patch.js";
6
- import { killLabel } from "../mutate/kill-label.js";
7
- import { normalizeResult, runMutants } from "../mutate/run.js";
8
- import { campaignSummary, UNVIABLE_NEXT } from "../mutate/suite-decision.js";
9
- /** The gap is done only when this mutant is killed and the baseline suite is green. */
10
- export async function checkKill(id, options) {
11
- const importers = importersOf(id, options);
12
- if (importers.length === 0) {
13
- return {
14
- schemaVersion: SCHEMA_VERSION,
15
- ok: false,
16
- command: "check-kill",
17
- summary: `${id} is not done. no direct importer in ${options.testsDir}`,
18
- next: "The task is not done.",
19
- nextCall: null,
20
- done: false,
21
- outcome: null,
22
- suiteGreen: false,
23
- };
24
- }
25
- const report = await runMutants({ ...options, onlyTests: importers, onlyPatch: id, affected: false });
26
- const baselineFile = path.join(options.outDir, "baseline.json");
27
- let suiteGreen = false;
28
- if (existsSync(baselineFile)) {
29
- const baseline = JSON.parse(readFileSync(baselineFile, "utf8"));
30
- suiteGreen = baseline.ok === true;
31
- }
32
- // A red baseline stops inside the run. Do not read a result that names the broken test.
33
- if (!report.ok) {
34
- return {
35
- schemaVersion: SCHEMA_VERSION,
36
- ok: false,
37
- command: "check-kill",
38
- summary: `${id} is not done. ${report.summary}`,
39
- next: "The task is not done.",
40
- nextCall: null,
41
- done: false,
42
- outcome: null,
43
- suiteGreen,
44
- killedBy: [],
45
- };
46
- }
47
- const resultFile = path.join(options.outDir, "results", `${id}.json`);
48
- let outcome = null;
49
- let killedBy = [];
50
- if (existsSync(resultFile)) {
51
- const result = normalizeResult(JSON.parse(readFileSync(resultFile, "utf8")));
52
- outcome = result.outcome ?? null;
53
- killedBy = Array.isArray(result.killedBy) ? result.killedBy : [];
54
- }
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;
59
- const head = checkHead(report);
60
- return {
61
- schemaVersion: SCHEMA_VERSION,
62
- ok: done,
63
- command: "check-kill",
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.`,
69
- next: label?.next ?? (done ? "The gap-fixing task is done." : "The task is not done."),
70
- nextCall: null,
71
- done,
72
- outcome,
73
- cause: label?.cause ?? null,
74
- killedBy,
75
- suiteGreen,
76
- };
77
- }
78
- /** Same direct-importer set verify-change runs for a diff that touches this mutant's file. */
79
- function importersOf(id, options) {
80
- const patch = options.patchDirs.map((dir) => path.join(dir, `${id}.patch`)).find((file) => existsSync(file));
81
- if (!patch)
82
- return [];
83
- const rels = filesInDiff(readFileSync(patch, "utf8")).map((item) => packageRelative(options.repoDir, options.packageDir, item.file));
84
- return directImporters(options.packageDir, rels, options.testsDir);
85
- }
86
- function checkHead(report) {
87
- const finished = report.killed + report.unviable + report.survived + report.flaky + report.timeouts + report.errors + report.noCoverage;
88
- return campaignSummary({
89
- noCoverage: report.noCoverage,
90
- survived: report.survived,
91
- killed: report.killed,
92
- unviable: report.unviable,
93
- flaky: report.flaky,
94
- timeouts: report.timeouts,
95
- errors: report.errors,
96
- finished,
97
- });
98
- }
99
- function packageRelative(repoDir, packageDir, file) {
100
- const norm = file.split(path.sep).join("/");
101
- let repo = repoDir;
102
- let pkg = packageDir;
103
- try {
104
- repo = realpathSync(repoDir);
105
- pkg = realpathSync(packageDir);
106
- }
107
- catch {
108
- // A missing path still has a relative prefix.
109
- }
110
- const prefix = path.relative(repo, pkg).split(path.sep).join("/");
111
- if (prefix && prefix !== "." && !prefix.startsWith("..") && norm.startsWith(`${prefix}/`))
112
- return norm.slice(prefix.length + 1);
113
- return norm;
114
- }
@@ -1,68 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import path from "node:path";
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
- }
10
- export function seedQueue(root, ids) {
11
- const sealed = new Set(loadSealed(root));
12
- const queue = path.join(root, "queue");
13
- mkdirSync(queue, { recursive: true });
14
- const written = [];
15
- for (const id of [...ids].sort()) {
16
- if (sealed.has(id) || !safeFilePart(id))
17
- continue;
18
- const dest = path.join(queue, `${id}.json`);
19
- if (existsSync(dest))
20
- continue;
21
- writeFileSync(dest, `${JSON.stringify({ id })}\n`);
22
- written.push(id);
23
- }
24
- return written;
25
- }
26
- /** Claim by atomic rename. A second claim fails because the queue file is already gone. */
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 };
30
- const source = path.join(root, "queue", `${id}.json`);
31
- const claimed = path.join(root, "claimed");
32
- const dest = path.join(claimed, `${agent}-${id}.json`);
33
- if (!existsSync(source))
34
- return { ok: false, summary: `${id} is not in the queue.`, file: null };
35
- mkdirSync(claimed, { recursive: true });
36
- // Rename first. Writing the body before the rename lets the other claim overwrite the winner.
37
- try {
38
- renameSync(source, dest);
39
- }
40
- catch {
41
- return { ok: false, summary: `${id} was claimed by someone else.`, file: null };
42
- }
43
- const body = { id, agent, leasedUntil: now + leaseMs };
44
- writeFileSync(dest, `${JSON.stringify(body)}\n`);
45
- return { ok: true, summary: `${agent} claimed ${id} until ${body.leasedUntil}.`, file: dest };
46
- }
47
- /** Move claims whose lease has expired back to the queue. */
48
- export function reapClaims(root, now) {
49
- const claimed = path.join(root, "claimed");
50
- const queue = path.join(root, "queue");
51
- if (!existsSync(claimed))
52
- return [];
53
- mkdirSync(queue, { recursive: true });
54
- const returned = [];
55
- for (const name of readdirSync(claimed).sort()) {
56
- if (!name.endsWith(".json"))
57
- continue;
58
- const file = path.join(claimed, name);
59
- const body = JSON.parse(readFileSync(file, "utf8"));
60
- if (typeof body.leasedUntil !== "number" || body.leasedUntil > now)
61
- continue;
62
- if (!body.id || !safeFilePart(body.id))
63
- continue;
64
- renameSync(file, path.join(queue, `${body.id}.json`));
65
- returned.push(body.id);
66
- }
67
- return returned.sort();
68
- }
@@ -1,340 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
2
- import path from "node:path";
3
- import { SCHEMA_VERSION } from "../contract.js";
4
- import { asTable } from "../golden/check.js";
5
- import { affectedTests, directImporters } from "../mutate/affected.js";
6
- import { findInSource } from "../mutate/find.js";
7
- import { mutantId } from "../mutate/ids.js";
8
- import { forwardDiff, git } from "../mutate/patch.js";
9
- import { runMutants } from "../mutate/run.js";
10
- import { WHOLE_PROGRAM_TEST, leadSummary } from "../mutate/suite-decision.js";
11
- import { discoverSuite } from "../mutate/suites.js";
12
- /** Diff-scoped mutants, the tests that can see the edit, and golden contract rows that changed. */
13
- export async function verifyChange(options) {
14
- const repo = realpathSync(path.resolve(options.repoDir));
15
- const packageDir = realpathSync(path.resolve(options.packageDir));
16
- const prefix = packagePrefix(repo, packageDir);
17
- // The package may sit inside a large checkout. Diff that directory only.
18
- // A whole-repo unified diff can exceed the pipe and come back as a failed spawn.
19
- const scope = prefix ? ["--", prefix] : [];
20
- const diff = git(repo, ["diff", "-U0", "--no-renames", options.base, options.commit, ...scope]);
21
- if (diff.status !== 0)
22
- return fail(options.outDir, diff.stderr.trim() || "git diff failed");
23
- const ranges = parseUnified(diff.stdout);
24
- const names = git(repo, ["diff", "--name-only", "--no-renames", options.base, options.commit, ...scope]);
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
- }
31
- const goldenContract = goldenDiffs(repo, options.base, options.commit, changed);
32
- const packageFiles = [];
33
- const chosen = [];
34
- const libImported = testsImportLib(packageDir);
35
- for (const [file, lines] of ranges) {
36
- const rel = toPackage(prefix, file);
37
- if (!rel || !isVerifySource(rel, packageDir, libImported))
38
- continue;
39
- packageFiles.push(rel);
40
- const text = showFile(repo, options.commit, file);
41
- if (!text)
42
- continue;
43
- const found = findInSource(rel, text, options.operators ?? "core");
44
- const lineSet = new Set(lines);
45
- for (const point of found.points) {
46
- if (!lineSet.has(point.line))
47
- continue;
48
- chosen.push({ rel, label: file, text, point });
49
- }
50
- }
51
- chosen.sort((a, b) => a.rel.localeCompare(b.rel) || a.point.line - b.point.line || a.point.op.localeCompare(b.point.op) || a.point.start - b.point.start);
52
- const capped = chosen.slice(0, options.maxMutants);
53
- const mutantsDir = path.join(options.outDir, "mutants");
54
- mkdirSync(mutantsDir, { recursive: true });
55
- const written = [];
56
- const notRun = chosen.slice(capped.length).map(locate);
57
- for (const item of capped) {
58
- const located = locate(item);
59
- const mutated = item.text.slice(0, item.point.start) + item.point.replacement + item.text.slice(item.point.end);
60
- const body = forwardDiff(item.label, item.text, mutated);
61
- if (!body) {
62
- notRun.push(located);
63
- continue;
64
- }
65
- writeFileSync(path.join(mutantsDir, `${located.id}.patch`), body);
66
- written.push(located);
67
- }
68
- writeFileSync(path.join(mutantsDir, "mutants.json"), `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, mutants: written }, null, 2)}\n`);
69
- const affectedMap = affectedTests(packageDir, { testsDir: "tests" });
70
- const affected = [...new Set(packageFiles.flatMap((file) => affectedMap[file] ?? []))].sort();
71
- const direct = directImporters(packageDir, packageFiles, "tests");
72
- const ran = options.maxTests == null ? direct : direct.slice(0, options.maxTests);
73
- const notRan = direct.slice(ran.length);
74
- let caught = [];
75
- let missed = [];
76
- let other = [];
77
- let suiteCommand = "";
78
- const suite = discoverSuite(packageDir, "tests");
79
- if (written.length > 0 && suite && suite.files.length > 0) {
80
- const report = await runMutants({
81
- packageDir,
82
- repoDir: repo,
83
- commit: options.commit,
84
- patchDirs: [mutantsDir],
85
- outDir: path.join(options.outDir, "run"),
86
- direction: "forward",
87
- workers: 1,
88
- concurrency: 1,
89
- maxMinutes: options.maxMinutes,
90
- maxMutants: null,
91
- build: null,
92
- testsDir: "tests",
93
- unset: ["SOLARI_API_KEY", "AUSPEX_LIVE"],
94
- suiteTimeoutMs: 600_000,
95
- testTimeoutMs: 60_000,
96
- confirm: false,
97
- affected: false,
98
- agent: null,
99
- onProgress: options.onProgress,
100
- });
101
- suiteCommand = report.suiteCommand;
102
- if (!report.ok)
103
- return fail(options.outDir, report.summary, { affected, ran, notRan, goldenContract, notRun, next: report.next, suiteCommand });
104
- const byId = new Map(written.map((item) => [item.id, item]));
105
- const results = readOutcomes(path.join(options.outDir, "run"), written.map((item) => item.id));
106
- caught = results.filter((item) => item.outcome === "killed").map((item) => byId.get(item.id)).filter((item) => item !== undefined);
107
- missed = results.filter((item) => item.outcome === "survived").map((item) => byId.get(item.id)).filter((item) => item !== undefined);
108
- other = results
109
- .filter((item) => item.outcome !== "killed" && item.outcome !== "survived")
110
- .map((item) => ({ id: item.id, outcome: item.outcome, file: byId.get(item.id)?.file ?? "", line: byId.get(item.id)?.line ?? 0 }));
111
- const executed = executedNames(results);
112
- const envelope = bodyOf(options.outDir, true, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, executed);
113
- writeFileSync(path.join(options.outDir, "verify.json"), `${JSON.stringify(envelope, null, 2)}\n`);
114
- return envelope;
115
- }
116
- notRun.push(...written);
117
- const envelope = bodyOf(options.outDir, true, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, []);
118
- writeFileSync(path.join(options.outDir, "verify.json"), `${JSON.stringify(envelope, null, 2)}\n`);
119
- return envelope;
120
- }
121
- function bodyOf(outDir, ok, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, executed) {
122
- const uncovered = other.filter((item) => item.outcome === "no coverage");
123
- const summary = ok
124
- ? leadSummary(uncovered.length, missed.length, "missed", `${caught.length} caught, ${notRun.length} not run, ${affected.length} affected tests, ${goldenContract.length} golden contract changes.`)
125
- : "verify-change did not finish.";
126
- const first = missed[0] ?? uncovered[0] ?? notRun[0] ?? caught[0];
127
- return {
128
- schemaVersion: SCHEMA_VERSION,
129
- ok,
130
- command: "verify-change",
131
- summary,
132
- next: first ? `${first.id} at ${first.file}:${first.line}` : "No diff-scoped mutant.",
133
- nextCall: null,
134
- affected,
135
- ran,
136
- notRan,
137
- suiteCommand,
138
- executed,
139
- confirmed: false,
140
- caught,
141
- missed,
142
- other,
143
- notRun,
144
- goldenContract,
145
- rest: notRun.length,
146
- full: path.join(outDir, "verify.json"),
147
- };
148
- }
149
- function locate(item) {
150
- return {
151
- id: mutantId(item.rel, item.point.start, item.point.end, item.point.op),
152
- file: item.rel,
153
- line: item.point.line,
154
- op: item.point.op,
155
- };
156
- }
157
- function fail(outDir, summary, extra) {
158
- mkdirSync(outDir, { recursive: true });
159
- const notRun = extra?.notRun ?? [];
160
- return {
161
- schemaVersion: SCHEMA_VERSION,
162
- ok: false,
163
- command: "verify-change",
164
- summary,
165
- next: extra?.next ?? "Fix that and run the same command again.",
166
- nextCall: null,
167
- affected: extra?.affected ?? [],
168
- ran: extra?.ran ?? [],
169
- notRan: extra?.notRan ?? [],
170
- suiteCommand: extra?.suiteCommand ?? "",
171
- executed: [],
172
- confirmed: false,
173
- caught: [],
174
- missed: [],
175
- other: [],
176
- notRun,
177
- goldenContract: extra?.goldenContract ?? [],
178
- rest: notRun.length,
179
- full: path.join(outDir, "verify.json"),
180
- };
181
- }
182
- function readOutcomes(outDir, ids) {
183
- const file = path.join(outDir, "results");
184
- const out = [];
185
- for (const id of ids) {
186
- try {
187
- const result = JSON.parse(readFileSync(path.join(file, `${id}.json`), "utf8"));
188
- out.push({ id, outcome: result.outcome, selectedTests: result.selectedTests ?? [] });
189
- }
190
- catch {
191
- out.push({ id, outcome: "missing", selectedTests: [] });
192
- }
193
- }
194
- return out;
195
- }
196
- function executedNames(results) {
197
- return [...new Set(results.flatMap((item) => item.selectedTests))].filter((name) => name.length > 0 && name !== WHOLE_PROGRAM_TEST).sort();
198
- }
199
- function goldenDiffs(repo, base, commit, files) {
200
- const found = [];
201
- for (const file of files) {
202
- if (!file.endsWith(".golden.json") && !file.includes("/golden/"))
203
- continue;
204
- if (!file.endsWith(".json"))
205
- continue;
206
- const before = asTable(showFile(repo, base, file));
207
- const after = asTable(showFile(repo, commit, file));
208
- const ids = [...new Set([...Object.keys(before.rows), ...Object.keys(after.rows)])].sort();
209
- for (const row of ids) {
210
- const left = contractOf(before.rows[row]);
211
- const right = contractOf(after.rows[row]);
212
- if (JSON.stringify(left) === JSON.stringify(right))
213
- continue;
214
- const fields = [...new Set([...Object.keys(left), ...Object.keys(right)])].filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).sort();
215
- found.push({ file, row, fields });
216
- }
217
- }
218
- return found;
219
- }
220
- function contractOf(row) {
221
- if (!row)
222
- return {};
223
- const out = {};
224
- for (const key of Object.keys(row).sort()) {
225
- if (key === "next" || key === "nextLead")
226
- continue;
227
- out[key] = row[key];
228
- }
229
- return out;
230
- }
231
- function parseUnified(diff) {
232
- const lines = new Map();
233
- let file = null;
234
- for (const line of diff.split(/\r?\n/)) {
235
- const plus = line.match(/^\+\+\+ ([^\t]+)/);
236
- if (plus) {
237
- let name = plus[1];
238
- if (name === "/dev/null") {
239
- file = null;
240
- continue;
241
- }
242
- if (name.startsWith("a/") || name.startsWith("b/"))
243
- name = name.slice(2);
244
- file = name;
245
- if (!lines.has(file))
246
- lines.set(file, []);
247
- continue;
248
- }
249
- const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
250
- if (!hunk || !file)
251
- continue;
252
- const start = Number(hunk[1]);
253
- const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
254
- const list = lines.get(file) ?? [];
255
- for (let offset = 0; offset < count; offset++)
256
- list.push(start + offset);
257
- lines.set(file, list);
258
- }
259
- return lines;
260
- }
261
- function showFile(repo, commit, file) {
262
- const result = git(repo, ["show", `${commit}:${file}`]);
263
- return result.status === 0 ? result.stdout : "";
264
- }
265
- function packagePrefix(repo, packageDir) {
266
- const rel = path.relative(repo, packageDir);
267
- if (rel.startsWith(".."))
268
- throw new Error("package is outside the repository");
269
- return rel.split(path.sep).join("/");
270
- }
271
- function toPackage(prefix, file) {
272
- if (!prefix)
273
- return file;
274
- if (!file.startsWith(`${prefix}/`))
275
- return null;
276
- return file.slice(prefix.length + 1);
277
- }
278
- const SKIPPED_SOURCE = new Set(["node_modules", "dist", "docs", "tests", "test"]);
279
- /** src/, a lib/ tree the tests import, or a Go file beside go.mod. Tests, docs, dist, and node_modules stay out. */
280
- function isVerifySource(rel, packageDir, libImported) {
281
- if (rel.endsWith(".d.ts"))
282
- return false;
283
- const parts = rel.split("/");
284
- if (parts.some((part) => SKIPPED_SOURCE.has(part)))
285
- return false;
286
- if (rel.startsWith("src/"))
287
- return true;
288
- if (libImported && rel.startsWith("lib/"))
289
- return true;
290
- if (!rel.includes("/") && rel.endsWith(".go") && !rel.endsWith("_test.go") && existsSync(path.join(packageDir, "go.mod")))
291
- return true;
292
- return false;
293
- }
294
- function testsImportLib(packageDir) {
295
- const pattern = /(?:require\s*\(\s*|from\s+)['"](?:@[^'"]+\/)?(?:\.\.\/|\.\/)*lib\//;
296
- for (const dirName of ["tests", "test"]) {
297
- const dir = path.join(packageDir, dirName);
298
- if (!existsSync(dir))
299
- continue;
300
- if (walkText(dir).some((text) => pattern.test(text)))
301
- return true;
302
- }
303
- return false;
304
- }
305
- function walkText(dir) {
306
- const out = [];
307
- const visit = (current) => {
308
- let entries;
309
- try {
310
- entries = readdirSync(current);
311
- }
312
- catch {
313
- return;
314
- }
315
- for (const name of entries) {
316
- if (name === "node_modules" || name === "dist")
317
- continue;
318
- const full = path.join(current, name);
319
- let info;
320
- try {
321
- info = statSync(full);
322
- }
323
- catch {
324
- continue;
325
- }
326
- if (info.isDirectory())
327
- visit(full);
328
- else if (info.isFile() && info.size < 1_000_000) {
329
- try {
330
- out.push(readFileSync(full, "utf8"));
331
- }
332
- catch {
333
- // An unreadable test file does not count as an import of lib/.
334
- }
335
- }
336
- }
337
- };
338
- visit(dir);
339
- return out;
340
- }
@@ -1,93 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/IronAdamant/Probatio/blob/main/schemas/check-kill.schema.json",
4
- "title": "probatio check-kill",
5
- "description": "Done only when the mutant is killed (or does not build) and the suite is still green. Every command prints one JSON object. schemaVersion changes when a field is renamed, removed, or changes meaning. A new field does not change it.",
6
- "type": "object",
7
- "required": [
8
- "schemaVersion",
9
- "ok",
10
- "command",
11
- "summary",
12
- "next",
13
- "nextCall"
14
- ],
15
- "properties": {
16
- "schemaVersion": {
17
- "const": 2
18
- },
19
- "ok": {
20
- "type": "boolean"
21
- },
22
- "command": {
23
- "const": "check-kill"
24
- },
25
- "summary": {
26
- "type": "string"
27
- },
28
- "next": {
29
- "type": "string"
30
- },
31
- "nextCall": {
32
- "oneOf": [
33
- {
34
- "type": "null"
35
- },
36
- {
37
- "type": "object",
38
- "properties": {
39
- "argv": {
40
- "type": "array",
41
- "items": {
42
- "type": "string"
43
- }
44
- }
45
- },
46
- "required": [
47
- "argv"
48
- ]
49
- }
50
- ]
51
- },
52
- "done": {
53
- "type": "boolean"
54
- },
55
- "outcome": {
56
- "type": [
57
- "string",
58
- "null"
59
- ]
60
- },
61
- "cause": {
62
- "enum": [
63
- null,
64
- "test",
65
- "build"
66
- ]
67
- },
68
- "killedBy": {
69
- "type": "array",
70
- "items": {
71
- "type": "string"
72
- }
73
- },
74
- "suiteGreen": {
75
- "type": "boolean"
76
- }
77
- },
78
- "if": {
79
- "properties": {
80
- "ok": {
81
- "const": true
82
- }
83
- }
84
- },
85
- "then": {
86
- "required": [
87
- "done",
88
- "outcome",
89
- "killedBy",
90
- "suiteGreen"
91
- ]
92
- }
93
- }
@@ -1,53 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/IronAdamant/Probatio/blob/main/schemas/findings.add.schema.json",
4
- "title": "probatio findings.add",
5
- "description": "Appends a decision to the findings log. Every command prints one JSON object. schemaVersion changes when a field is renamed, removed, or changes meaning. A new field does not change it.",
6
- "type": "object",
7
- "required": [
8
- "schemaVersion",
9
- "ok",
10
- "command",
11
- "summary",
12
- "next",
13
- "nextCall"
14
- ],
15
- "properties": {
16
- "schemaVersion": {
17
- "const": 2
18
- },
19
- "ok": {
20
- "type": "boolean"
21
- },
22
- "command": {
23
- "const": "findings.add"
24
- },
25
- "summary": {
26
- "type": "string"
27
- },
28
- "next": {
29
- "type": "string"
30
- },
31
- "nextCall": {
32
- "oneOf": [
33
- {
34
- "type": "null"
35
- },
36
- {
37
- "type": "object",
38
- "properties": {
39
- "argv": {
40
- "type": "array",
41
- "items": {
42
- "type": "string"
43
- }
44
- }
45
- },
46
- "required": [
47
- "argv"
48
- ]
49
- }
50
- ]
51
- }
52
- }
53
- }