oxmutate 0.0.0 → 1.0.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +376 -4
  3. package/binding/oxmutate.darwin-arm64.node +0 -0
  4. package/binding/oxmutate.darwin-x64.node +0 -0
  5. package/binding/oxmutate.linux-arm64-gnu.node +0 -0
  6. package/binding/oxmutate.linux-x64-gnu.node +0 -0
  7. package/binding/oxmutate.linux-x64-musl.node +0 -0
  8. package/binding/oxmutate.win32-x64-msvc.node +0 -0
  9. package/dist/bin.js +5 -0
  10. package/dist/checker.js +122 -0
  11. package/dist/child.js +125 -0
  12. package/dist/cli.js +165 -0
  13. package/dist/command-runner.js +83 -0
  14. package/dist/config.d.ts +141 -0
  15. package/dist/config.js +172 -0
  16. package/dist/coordinator.d.ts +83 -0
  17. package/dist/coordinator.js +653 -0
  18. package/dist/files.js +62 -0
  19. package/dist/frozen.d.ts +19 -0
  20. package/dist/frozen.js +47 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +9 -0
  23. package/dist/lanes.d.ts +38 -0
  24. package/dist/lanes.js +92 -0
  25. package/dist/native.d.ts +36 -0
  26. package/dist/native.js +40 -0
  27. package/dist/platform.js +27 -0
  28. package/dist/progress.js +40 -0
  29. package/dist/reports.d.ts +20 -0
  30. package/dist/reports.js +225 -0
  31. package/dist/resolve.js +43 -0
  32. package/dist/runner.d.ts +49 -0
  33. package/dist/runner.js +270 -0
  34. package/dist/runtime.js +24 -0
  35. package/dist/scheduler.js +170 -0
  36. package/dist/spec-oracle.d.ts +69 -0
  37. package/dist/spec-oracle.js +68 -0
  38. package/dist/spec.d.ts +21 -0
  39. package/dist/spec.js +75 -0
  40. package/dist/vite-plugin.js +18 -0
  41. package/dist/vite.d.ts +16 -0
  42. package/dist/vite.js +3 -0
  43. package/package.json +81 -6
  44. package/index.js +0 -1
package/dist/child.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * A Vitest server in its own process. It starts the project's Vitest once and then runs work
3
+ * units sent by the coordinator over IPC, re-running the same instance (~0.2 s a run against
4
+ * ~1.5 s for a fresh Vitest). The coordinator kills the process when a mutant loops
5
+ * synchronously, a hang that no in-worker timeout and no `cancelCurrentRun()` can interrupt
6
+ * (both measured, 2026-10-26), and hands the rest of the unit to another server.
7
+ *
8
+ * node child.js <server.json> IPC: ← { type: 'run', unit } → { type: 'ready' | 'done' }
9
+ *
10
+ * Vitest is resolved from the project being tested, like Stryker's vitest-runner does, so the
11
+ * project's own Vitest version and config run its tests.
12
+ */
13
+ import { readFileSync, writeFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { ExitCode } from 'burgee';
17
+ import { resolveEsm } from './resolve.js';
18
+ import { ACTIVE_MUTANT_ENV, argv, env, exit } from './runtime.js';
19
+ import { instrumentedSources } from './vite-plugin.js';
20
+ const config = JSON.parse(readFileSync(argv[2] ?? '', 'utf8'));
21
+ env['NODE_ENV'] ??= 'test';
22
+ const vitestNode = (await import(resolveEsm('vitest/node', config.root)));
23
+ const runner = fileURLToPath(new URL('./runner.js', import.meta.url));
24
+ const silent = { onInit() { }, onFinished() { } };
25
+ const vitest = await vitestNode.createVitest('test', {
26
+ root: config.root,
27
+ config: config.vitestConfig,
28
+ watch: false,
29
+ run: true,
30
+ pool: 'threads',
31
+ maxWorkers: config.workers,
32
+ isolate: true,
33
+ runner,
34
+ reporters: env['OXMUTATE_DEBUG_VITEST'] ? ['default'] : [silent],
35
+ coverage: { enabled: false },
36
+ passWithNoTests: true,
37
+ // Bail is ours (per mutant); Vitest must run every test it is handed.
38
+ bail: 0,
39
+ // Vitest 5's persistent module cache keys on the file on disk, not on what a plugin serves,
40
+ // so it served a previous run's instrumentation: every mutant whose id had changed since got
41
+ // no coverage (burgee/paratext, 2026-10-27: 6% agreement until this was off). Off under both
42
+ // spellings (top-level in Vitest 5, `experimental` in 4.x).
43
+ fsModuleCache: false,
44
+ experimental: { fsModuleCache: false },
45
+ },
46
+ // A private Vite cache per run, so no cache outside this run can answer for a mutated file.
47
+ { plugins: [instrumentedSources(config.instrumentedMap)], cacheDir: path.join(path.dirname(config.instrumentedMap), 'vite-cache') });
48
+ /** Has the runner recorded a kill for this mutant in this unit's results yet? */
49
+ function killed(results, id) {
50
+ let text = '';
51
+ try {
52
+ text = readFileSync(results, 'utf8');
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ return text.split('\n').some((line) => line.includes(`"id":"${id}"`) && line.includes('"killed":true'));
58
+ }
59
+ /** An `isolated` unit: its mutants armed one after another, each across its files. */
60
+ async function runIsolated(unit) {
61
+ // Stop a file at its first failing test: one failure kills the mutant, and the rest of the
62
+ // file is wasted time (burgee/paratext hyperlinks.test.ts: 2.3 s a run, the first failure
63
+ // typically in its first tests). Read per start, so dry and warm runs still run everything
64
+ // (measured on Vitest 4.1, 2026-10-27).
65
+ vitest.config.bail = 1;
66
+ try {
67
+ for (const item of unit.batch ?? []) {
68
+ env[ACTIVE_MUTANT_ENV] = item.id;
69
+ if (item.tests)
70
+ env['OXMUTATE_ONLY_TESTS'] = JSON.stringify(item.tests);
71
+ else
72
+ delete env['OXMUTATE_ONLY_TESTS'];
73
+ writeFileSync(unit.progress, `${item.id} ${String(Date.now())}\n`);
74
+ // One file at a time, cheapest first (the coordinator orders them), stopping at the first
75
+ // file that kills the mutant: Stryker runs every file for every isolated mutant.
76
+ for (const file of item.files) {
77
+ await vitest.start([file]);
78
+ if (killed(unit.results, item.id))
79
+ break;
80
+ }
81
+ }
82
+ delete env[ACTIVE_MUTANT_ENV];
83
+ delete env['OXMUTATE_ONLY_TESTS'];
84
+ }
85
+ finally {
86
+ vitest.config.bail = 0;
87
+ }
88
+ writeFileSync(unit.progress, '');
89
+ }
90
+ async function runUnit(unit) {
91
+ // Workers copy the environment when they start, so each run sees the unit's settings
92
+ // (verified across re-runs of one instance, 2026-10-26).
93
+ env['OXMUTATE_MODE'] = unit.mode;
94
+ env['OXMUTATE_RESULTS'] = unit.results;
95
+ env['OXMUTATE_PROGRESS'] = unit.progress;
96
+ if (unit.plan)
97
+ env['OXMUTATE_PLAN'] = unit.plan;
98
+ delete env[ACTIVE_MUTANT_ENV];
99
+ if (unit.mode === 'isolated')
100
+ await runIsolated(unit);
101
+ else {
102
+ // A dry run names itself in the progress file, so the watchdog holds it to its budget.
103
+ if (unit.mode === 'dry')
104
+ writeFileSync(unit.progress, `dry ${String(Date.now())}\n`);
105
+ await vitest.start(unit.files.length ? unit.files : undefined);
106
+ }
107
+ }
108
+ /** How long `close` waits for `vitest.close()` before the server exits regardless. */
109
+ const CLOSE_GRACE_MS = 2000;
110
+ process.on('message', (msg) => {
111
+ if (msg.type === 'run' && msg.unit) {
112
+ runUnit(msg.unit).then(() => process.send?.({ type: 'done' }), (error) => process.send?.({ type: 'done', error: error instanceof Error ? error.message : String(error) }));
113
+ }
114
+ else if (msg.type === 'close') {
115
+ // A worker thread stuck in a runaway mutant never lets `vitest.close()` settle: exit anyway.
116
+ setTimeout(() => exit(ExitCode.OK), CLOSE_GRACE_MS).unref();
117
+ void vitest.close().finally(() => exit(ExitCode.OK));
118
+ }
119
+ });
120
+ // The coordinator is gone (finished, crashed or killed): nothing will read this server's results,
121
+ // and a worker thread may still be looping in a mutant. Two such servers were found spinning at
122
+ // 100% CPU 11 hours after their run ended, and slowed the next run 50x (2026-10-27).
123
+ process.on('disconnect', () => exit(ExitCode.OK));
124
+ process.send?.({ type: 'ready' });
125
+ //# sourceMappingURL=child.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * oxmutate's command line, declared once on burgee: help, `--json`, `--schema`, `--format=agent`,
3
+ * completions and the MCP server are projected from this declaration (spec R17). Handlers return
4
+ * data; the text a person reads rides along under burgee's text symbol, so the agent surfaces
5
+ * carry exactly the data.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { defineCommand, defineProgram, ExitCode } from 'burgee';
10
+ import { loadOptions } from './config.js';
11
+ import { InstrumentFault, run } from './coordinator.js';
12
+ import { frozenOracle } from './frozen.js';
13
+ import { parseShard } from './lanes.js';
14
+ import { describe, textSummary, VERSION, writeReports } from './reports.js';
15
+ import { cwd, stderr } from './runtime.js';
16
+ const TEXT = Symbol.for('burgee.text');
17
+ function survivorRecord(m) {
18
+ return {
19
+ id: m.id,
20
+ file: m.file,
21
+ line: m.start.line,
22
+ column: m.start.column,
23
+ mutator: m.mutatorName,
24
+ replacement: m.replacement,
25
+ status: m.status,
26
+ coveredBy: m.coveredBy.length,
27
+ flaky: m.flaky ?? false,
28
+ fix: describe(m),
29
+ };
30
+ }
31
+ function summaryData(r, written) {
32
+ const count = (s) => r.mutants.filter((m) => m.status === s).length;
33
+ const data = {
34
+ // burgee N7: an idempotent command says whether it changed anything (reports were written).
35
+ changed: written.length > 0,
36
+ version: VERSION,
37
+ score: Number(r.score.mutationScore.toFixed(2)),
38
+ coveredScore: Number(r.score.covered.toFixed(2)),
39
+ mutants: r.mutants.length,
40
+ killed: count('Killed'),
41
+ timeout: count('Timeout'),
42
+ survived: count('Survived'),
43
+ noCoverage: count('NoCoverage'),
44
+ ignored: count('Ignored'),
45
+ errors: count('RuntimeError') + count('CompileError'),
46
+ stats: r.stats,
47
+ reports: written.map((w) => path.relative(r.root, w)),
48
+ survivors: r.mutants.filter((m) => m.status === 'Survived' || m.status === 'NoCoverage').map(survivorRecord),
49
+ };
50
+ data[TEXT] = () => textSummary(r);
51
+ return data;
52
+ }
53
+ /** The `run` flags that override a config key, as the `Partial<Options>` loadOptions applies last. */
54
+ /** How many weakened mutants the error line names; the JSON report has them all. */
55
+ const WEAKENED_SHOWN = 3;
56
+ function overridesFrom(o, root) {
57
+ const overrides = {};
58
+ if (typeof o['since'] === 'string')
59
+ overrides.since = o['since'];
60
+ if (typeof o['frozenOracle'] === 'string')
61
+ overrides.frozenOracle = o['frozenOracle'];
62
+ if (typeof o['shard'] === 'string')
63
+ overrides.shard = parseShard(o['shard']);
64
+ if (typeof o['concurrency'] === 'number')
65
+ overrides.concurrency = o['concurrency'];
66
+ if (typeof o['mutantTimeout'] === 'number')
67
+ overrides.timeoutMS = o['mutantTimeout'];
68
+ if (o['strykerCompat'] === true)
69
+ overrides.strykerCompat = true;
70
+ if (o['incremental'] === true)
71
+ overrides.incremental = true;
72
+ if (typeof o['flakyReruns'] === 'number')
73
+ overrides.flakyReruns = o['flakyReruns'];
74
+ if (typeof o['reporter'] === 'string')
75
+ overrides.reporters = o['reporter'].split(',').map((s) => s.trim());
76
+ if (typeof o['reportDir'] === 'string') {
77
+ overrides.jsonReport = path.resolve(root, o['reportDir'], 'mutation.json');
78
+ overrides.sarifReport = path.resolve(root, o['reportDir'], 'mutation.sarif');
79
+ overrides.htmlReport = path.resolve(root, o['reportDir'], 'mutation.html');
80
+ }
81
+ return overrides;
82
+ }
83
+ const runCommand = defineCommand({
84
+ name: 'run',
85
+ description: 'Mutation-test the project in the current directory',
86
+ effects: 'idempotent',
87
+ examples: [
88
+ { command: 'oxmutate run', description: 'every mutant, with the Stryker config if there is one' },
89
+ { command: 'oxmutate run --since origin/main --reporter sarif', description: 'the PR lane: only lines the change touched, annotated in code scanning' },
90
+ { command: 'oxmutate run --shard 2/8 --json', description: 'one cell of a CI matrix, as the --json envelope' },
91
+ ],
92
+ options: {
93
+ since: { type: 'string', description: 'only mutants on lines changed since this git ref (the PR lane)' },
94
+ frozenOracle: { type: 'string', description: "also score against this ref's tests; exit 1 if the change weakened them" },
95
+ shard: { type: 'string', description: 'run one deterministic share, i/n, for a CI matrix' },
96
+ concurrency: { type: 'number', description: 'worker processes (default: cores - 1, at most 8)' },
97
+ mutantTimeout: { type: 'number', description: 'absolute slack per mutant, ms (default 500; Stryker 5000)' },
98
+ strykerCompat: { type: 'boolean', description: 'reproduce Stryker 10.0.0 skips (no mutants inside `as` casts or enums)' },
99
+ incremental: { type: 'boolean', description: 'reuse verdicts whose source and covering tests did not change' },
100
+ flakyReruns: { type: 'number', description: 're-runs for verdicts with nondeterministic tests (default 2; 0 disables)' },
101
+ reporter: { type: 'string', description: 'comma-separated: clear-text, json, sarif, html (default clear-text,json)' },
102
+ break: { type: 'number', description: 'exit 1 when the mutation score is below this' },
103
+ reportDir: { type: 'string', description: 'write mutation.json, mutation.sarif and mutation.html here instead of the configured paths' },
104
+ config: { type: 'string', description: 'an oxmutate config file (JSON or JS) instead of oxmutate.config.*' },
105
+ },
106
+ run: async ({ options, exit }) => {
107
+ const root = cwd();
108
+ const o = options;
109
+ const options_ = await loadOptions(root, overridesFrom(o, root), typeof o['config'] === 'string' ? o['config'] : undefined);
110
+ if (typeof o['break'] === 'number')
111
+ options_.thresholds.break = o['break'];
112
+ let result;
113
+ try {
114
+ result = await run(options_);
115
+ if (options_.frozenOracle)
116
+ result.frozen = await frozenOracle(result, options_.frozenOracle);
117
+ }
118
+ catch (e) {
119
+ if (e instanceof InstrumentFault) {
120
+ stderr.write(`oxmutate: ${e.message}\n`);
121
+ exit(ExitCode.CONFIG);
122
+ }
123
+ throw e;
124
+ }
125
+ const written = writeReports(result);
126
+ const data = summaryData(result, written);
127
+ const weakened = result.frozen?.weakened ?? [];
128
+ if (weakened.length) {
129
+ stderr.write(`oxmutate: the change weakened its tests: ${String(weakened.length)} mutant(s) the tests at ${result.frozen?.base ?? ''} detect now survive (${weakened.slice(0, WEAKENED_SHOWN).map((w) => `${w.file}:${String(w.line)} ${w.mutatorName}`).join('; ')}). fix: restore the assertions the change removed, or replace them with ones that kill these mutants\n`);
130
+ data['exitCode'] = ExitCode.RUNTIME;
131
+ }
132
+ const floor = options_.thresholds.break;
133
+ if (floor !== null && result.score.mutationScore < floor) {
134
+ stderr.write(`oxmutate: mutation score ${result.score.mutationScore.toFixed(2)}% is below the break threshold ${String(floor)}%. fix: kill the survivors listed above, or lower thresholds.break deliberately\n`);
135
+ data['exitCode'] = ExitCode.RUNTIME;
136
+ }
137
+ return data;
138
+ },
139
+ });
140
+ const survivorsCommand = defineCommand({
141
+ name: 'survivors',
142
+ description: 'List the survivors of the last run, one record per mutant, each with a fix line',
143
+ effects: 'read_only',
144
+ examples: [{ command: 'oxmutate survivors --json', description: 'the list an agent writes killing tests from' }],
145
+ options: { report: { type: 'string', description: 'the JSON report to read (default reports/mutation/mutation.json)' } },
146
+ run: async ({ options }) => {
147
+ const file = path.resolve(String(options['report'] ?? 'reports/mutation/mutation.json'));
148
+ if (!existsSync(file))
149
+ throw new Error(`no report at ${file}. fix: run \`oxmutate run\` first`);
150
+ const report = JSON.parse(readFileSync(file, 'utf8'));
151
+ const out = Object.entries(report.files).flatMap(([f, x]) => x.mutants
152
+ .filter((m) => m.status === 'Survived' || m.status === 'NoCoverage')
153
+ .map((m) => survivorRecord({ ...m, file: f, start: m.location.start, end: m.location.end, coveredBy: m.coveredBy ?? [] })));
154
+ const data = { survivors: out };
155
+ data[TEXT] = () => (out.length ? out.map((s) => `${String(s.file)}:${String(s.line)} ${String(s.fix)}`).join('\n') : 'no survivors');
156
+ return data;
157
+ },
158
+ });
159
+ export const program = defineProgram({
160
+ name: 'oxmutate',
161
+ version: VERSION,
162
+ description: 'Mutation testing on the oxc engine: Stryker-compatible, built for CI and coding agents',
163
+ commands: [runCommand, survivorsCommand],
164
+ });
165
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The command runner (Stryker's `testRunner: 'command'`): any test command, one process per
3
+ * mutant, the escape hatch for a suite that is not Vitest. A non-zero exit kills the mutant, a run
4
+ * past its budget is a timeout, and a zero exit is a survivor. There is no coverage, so every
5
+ * mutant runs the whole command, as with Stryker's.
6
+ *
7
+ * An arbitrary command cannot be served sources from memory, so this is the one path with a
8
+ * sandbox: a copy of the project under `<root>/.oxmutate-tmp/`, with the instrumented files
9
+ * written into it. Inside the project, as Stryker's `.stryker-tmp` is, so module resolution walks
10
+ * up to the same `node_modules` (a monorepo's hoisted ones included). The armed mutant reaches the
11
+ * instrumented code through `__OXMUTATE_ACTIVE_MUTANT__`, which the runtime header reads.
12
+ */
13
+ import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { ambientRuntime, run as spawn } from 'bellpull';
16
+ import { ACTIVE_MUTANT_ENV, env } from './runtime.js';
17
+ const SANDBOX_DIR = '.oxmutate-tmp';
18
+ /** Stryker's default `ignorePatterns`, plus this sandbox. */
19
+ const SKIP = new Set(['node_modules', '.git', 'reports', 'coverage', '.stryker-tmp', SANDBOX_DIR]);
20
+ /**
21
+ * A copy of the project under `<root>/.oxmutate-tmp/`, for the paths that need files on disk (the
22
+ * command runner, the type checker). Inside the project so module resolution walks up to the same
23
+ * `node_modules`; entry by entry, since a directory cannot be copied into itself.
24
+ */
25
+ export function makeSandbox(root) {
26
+ const parent = path.join(root, SANDBOX_DIR);
27
+ mkdirSync(parent, { recursive: true });
28
+ const sandbox = mkdtempSync(path.join(parent, 'sandbox-'));
29
+ for (const entry of readdirSync(root))
30
+ if (!SKIP.has(entry))
31
+ cpSync(path.join(root, entry), path.join(sandbox, entry), { recursive: true });
32
+ return sandbox;
33
+ }
34
+ const MS_PER_MINUTE = 60_000;
35
+ /** How much of a failing dry run's output the error keeps. */
36
+ const OUTPUT_TAIL = 1500;
37
+ /**
38
+ * The least slack a process-per-mutant run gets: Stryker's own default `timeoutMS`. oxmutate's
39
+ * 500 ms default is sized for a warm in-process run; a fresh process's start-up varies by seconds
40
+ * on a loaded machine, and at 500 ms the e2e fixture read false timeouts (2026-10-27).
41
+ */
42
+ const PROCESS_SLACK_MS = 5000;
43
+ /** Runs the command over every mutant not already decided; returns the dry run's time. */
44
+ export async function runCommandMutants(o, mutants) {
45
+ const sandbox = makeSandbox(o.root);
46
+ try {
47
+ for (const [file, code] of Object.entries(o.codeMap))
48
+ writeFileSync(path.join(sandbox, path.relative(o.root, file)), code);
49
+ // Stryker splits the command on spaces and runs it without a shell; so does this.
50
+ const [command = '', ...args] = o.command.trim().split(/\s+/);
51
+ const once = (timeout, id) => spawn(command, args, { cwd: sandbox, runtime: ambientRuntime(), timeout, env: id ? { ...env, [ACTIVE_MUTANT_ENV]: id } : { ...env } });
52
+ const dryBudget = o.dryRunTimeoutMinutes * MS_PER_MINUTE;
53
+ const dry = await once(dryBudget);
54
+ if (!dry.ok) {
55
+ const why = dry.duration >= dryBudget ? `took longer than dryRunTimeoutMinutes (${String(o.dryRunTimeoutMinutes)})` : `exited ${String(dry.code)}`;
56
+ throw new Error(`the command \`${o.command}\` ${why} with no mutant active: ${(dry.stderr || dry.stdout).trim().slice(-OUTPUT_TAIL)}. fix: make the command pass on the unmutated project first`);
57
+ }
58
+ const budget = Math.max(o.timeoutMS, PROCESS_SLACK_MS) + o.timeoutFactor * dry.duration;
59
+ const queue = mutants.filter((m) => m.status !== 'Ignored');
60
+ let executions = 0;
61
+ const worker = async () => {
62
+ for (let m = queue.shift(); m; m = queue.shift()) {
63
+ const r = await once(budget, m.id);
64
+ executions++;
65
+ m.testsCompleted = 1;
66
+ if (r.ok)
67
+ m.status = 'Survived';
68
+ else if (r.signal !== null && r.duration >= budget) {
69
+ m.status = 'Timeout';
70
+ m.statusReason = `the command ran past ${String(Math.round(budget))} ms`;
71
+ }
72
+ else
73
+ m.status = 'Killed';
74
+ }
75
+ };
76
+ await Promise.all(Array.from({ length: Math.max(1, o.concurrency) }, worker));
77
+ return { dryMs: dry.duration, executions };
78
+ }
79
+ finally {
80
+ rmSync(sandbox, { recursive: true, force: true });
81
+ }
82
+ }
83
+ //# sourceMappingURL=command-runner.js.map
@@ -0,0 +1,141 @@
1
+ import { type IgnoreRule } from './native.js';
2
+ import { type SpecOracle } from './spec-oracle.js';
3
+ export interface Thresholds {
4
+ high: number;
5
+ low: number;
6
+ /** Fail the run (exit 1) below this score. `null`: never fail on score. */
7
+ break: number | null;
8
+ }
9
+ export interface Options {
10
+ root: string;
11
+ mutate: string[];
12
+ vitestConfig?: string | undefined;
13
+ concurrency: number;
14
+ timeoutMS: number;
15
+ timeoutFactor: number;
16
+ thresholds: Thresholds;
17
+ excludedMutations: string[];
18
+ /** Score static mutants (Stryker skips them with `ignoreStatic`). */
19
+ ignoreStatic: boolean;
20
+ strykerCompat: boolean;
21
+ reporters: string[];
22
+ jsonReport: string;
23
+ sarifReport: string;
24
+ htmlReport: string;
25
+ incremental: boolean;
26
+ incrementalFile: string;
27
+ /** Re-run a verdict whose covering tests behaved differently across two dry runs. */
28
+ flakyReruns: number;
29
+ shard?: {
30
+ index: number;
31
+ count: number;
32
+ };
33
+ /** Only mutants on lines changed since this git ref (the PR lane). */
34
+ since?: string | undefined;
35
+ /** Config keys read but not used, with the reason. */
36
+ unused: {
37
+ key: string;
38
+ why: string;
39
+ }[];
40
+ configFile?: string | undefined;
41
+ /** Stop after the dry run: proves the test setup runs under oxmutate (Stryker's `dryRunOnly`). */
42
+ dryRunOnly: boolean;
43
+ /** Budget for one dry run, in minutes (Stryker's default: 5). Over it, the run stops with exit 3. */
44
+ dryRunTimeoutMinutes: number;
45
+ /** Run once before the dry run, split on spaces with no shell, as Stryker runs it. */
46
+ buildCommand?: string | undefined;
47
+ /** Node flags for every Vitest server (Stryker's `testRunnerNodeArgs`). */
48
+ testRunnerNodeArgs: string[];
49
+ /** A dry run that finds no tests scores nothing instead of failing (Stryker's `allowEmpty`). */
50
+ allowEmpty: boolean;
51
+ /** `false` hides the line listing config keys oxmutate does not use (Stryker's `warnings`). */
52
+ warnUnused: boolean;
53
+ /** Also score the mutants against this ref's versions of the test files the change edited (R14). */
54
+ frozenOracle?: string | undefined;
55
+ /** Test file contents served in place of the files on disk (absolute path → source). */
56
+ testOverrides?: Record<string, string> | undefined;
57
+ /**
58
+ * Calls whose subtrees are not mutated: Stryker's `ignorers`, as data instead of plugins
59
+ * (`{ "call": "console.*", "reason": "log text is not behaviour" }`; `*` stops at a dot, `**` does not).
60
+ */
61
+ ignore: IgnoreRule[];
62
+ /** Type-check survivors with the project's compiler (Stryker's `checkers: ['typescript']`; checker.ts). */
63
+ typeCheck: boolean;
64
+ /** The tsconfig the checker uses (Stryker's `tsconfigFile`). */
65
+ tsconfigFile: string;
66
+ /** The compiler command (default: the project's tsgo, else `npx tsc`). */
67
+ typeChecker?: string | undefined;
68
+ /** Formal models whose traces are replayed against each mutant (spec-oracle.ts). */
69
+ specOracles: SpecOracle[];
70
+ /** `testRunner: 'command'`: run this command per mutant instead of the warm Vitest runner. */
71
+ commandRunner?: string | undefined;
72
+ }
73
+ /**
74
+ * The `mutate` block of a Vite / Vite+ config, and the shape of `oxmutate.config.*`: Stryker's own
75
+ * option names where Stryker has one, so a config moves across unchanged.
76
+ */
77
+ export interface MutateConfig {
78
+ /** Files to mutate (globs; `!` excludes). Stryker's `mutate`. Default: src, minus tests and .d.ts. */
79
+ include?: string[];
80
+ /** Stryker's spelling of `include`. */
81
+ mutate?: string[];
82
+ vitest?: {
83
+ configFile?: string;
84
+ };
85
+ concurrency?: number;
86
+ /** Absolute slack per mutant, ms (default 500; Stryker's is 5000). */
87
+ timeoutMS?: number;
88
+ timeoutFactor?: number;
89
+ thresholds?: {
90
+ high?: number;
91
+ low?: number;
92
+ break?: number | null;
93
+ };
94
+ mutator?: {
95
+ excludedMutations?: string[];
96
+ };
97
+ ignoreStatic?: boolean;
98
+ reporters?: ('clear-text' | 'json' | 'html' | 'sarif' | 'progress' | 'progress-append-only' | 'ci' | 'dots')[];
99
+ jsonReporter?: {
100
+ fileName?: string;
101
+ };
102
+ htmlReporter?: {
103
+ fileName?: string;
104
+ };
105
+ sarifReport?: string;
106
+ incremental?: boolean;
107
+ incrementalFile?: string;
108
+ flakyReruns?: number;
109
+ dryRunOnly?: boolean;
110
+ dryRunTimeoutMinutes?: number;
111
+ buildCommand?: string;
112
+ testRunnerNodeArgs?: string[];
113
+ allowEmpty?: boolean;
114
+ warnings?: boolean | {
115
+ unknownOptions?: boolean;
116
+ };
117
+ testRunner?: 'vitest' | 'command';
118
+ commandRunner?: {
119
+ command?: string;
120
+ };
121
+ checkers?: 'typescript'[];
122
+ tsconfigFile?: string;
123
+ typeChecker?: string;
124
+ /** Calls whose subtrees are not mutated: `{ call: 'console.*', reason }`. */
125
+ ignore?: IgnoreRule[];
126
+ /** Formal models replayed against each mutant (`quint` or `itf` traces). */
127
+ specOracles?: SpecOracle[];
128
+ strykerCompat?: boolean;
129
+ }
130
+ export declare function loadOptions(root: string, overrides?: Partial<Options>, configFile?: string): Promise<Options>;
131
+ /** Stryker's `disable next-line` directive, `Mutator[,Mutator]: <reason>` after it (also spelled `oxmutate disable`). */
132
+ export interface Disable {
133
+ line: number;
134
+ mutators: string[] | 'all';
135
+ reason: string;
136
+ }
137
+ /** Disable directives in a file. A directive without a reason is an error (burgee's own rule). */
138
+ export declare function disables(source: string, file: string): {
139
+ disables: Disable[];
140
+ errors: string[];
141
+ };