oxmutate 1.1.0 → 1.2.1

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 CHANGED
@@ -189,7 +189,7 @@ setting a value inside the worker that already has the tests loaded.
189
189
  | Re-parse | Every instrumented file is parsed again before it runs; one that does not parse stops the run (exit 3) instead of scoring a broken file |
190
190
  | Canary | The unmutated test must still pass after mutants run in a context, with its `beforeEach`/`afterEach` hooks re-run around every run, file-level ones included; if a mutant left a cache, a registry or a fixture behind, the context restarts and the suspect verdicts run again. A kill that follows unchecked runs is checked at once and decided again from clean state, so no kill rests on another mutant's leftovers |
191
191
  | Survivor confirmation | A survivor is re-run with the mutant armed across its covering tests in one file, as Stryker arms it, so a mutant that only fails across tests is killed |
192
- | Watchdog | A synchronous runaway is stopped and its server replaced; the stop is re-run at 3x the budget before it is reported as a timeout |
192
+ | Watchdog | A synchronous runaway is stopped and its server replaced; the stop is re-run at 3x the budget before it is reported as a timeout. A stop whose worker was idle inside a test body (its event loop still turning, by a heartbeat, and its server under 25% of a core) is a hang, not a slow test: it is a Timeout as it stands, the way Stryker scores it, with no 3x re-run |
193
193
  | Static mutants | Code that runs at import is decided in a live worker: the mutant is armed, the module registry reset and the test file collected again, against the files that load it. A file whose collection changes globals, `process.env` or a built-in prototype, or a mutant that leaves a trace, goes back to a fresh worker per mutant (`OXMUTATE_ISOLATE_STATIC=1` sends all of them there) |
194
194
  | Flaky tests | Tests that behave differently across two dry runs have their mutants re-run; a verdict that changes is reported `flaky` |
195
195
  | No orphans | Every Vitest server exits when its run does, killed or not |
@@ -261,7 +261,8 @@ head-to-head runs (`bench/burgee-pair.mts`, concurrency 4, one tool after the ot
261
261
 
262
262
  The ceiling depends on the suite as much as the tool: controlroom's own `vitest run` takes ~8 s and
263
263
  every fresh worker imports React and Ink, so even zero overhead would make it ~6x. The figures to
264
- publish will come from the `oxmutate-bench.yml` lane on a runner nothing else shares.
264
+ publish come from `bench/burgee-pair.mts`, run locally on a quiet machine: no mutation run happens
265
+ in GitHub Actions (`D-20261101-no-mutation-in-gha`).
265
266
 
266
267
  ## Benchmarks
267
268
 
package/dist/bin.js CHANGED
@@ -2,4 +2,3 @@
2
2
  import { run } from 'burgee';
3
3
  import { program } from './cli.js';
4
4
  await run(program);
5
- //# sourceMappingURL=bin.js.map
package/dist/checker.js CHANGED
@@ -1,46 +1,22 @@
1
- /**
2
- * The type checker (Stryker's `checkers: ['typescript']`): a surviving mutant that does not
3
- * type-check is a CompileError, not a survivor. A survivor is what someone writes a test for, and
4
- * a test cannot kill a program the compiler refuses; Stryker's TypeScript checker exists for the
5
- * same reason.
6
- *
7
- * It checks survivors only, where Stryker checks every mutant before running it. A killed mutant
8
- * that would not have compiled changes nothing anyone acts on, and a full project type check per
9
- * mutant is the costliest step there is. The difference: such a mutant is Killed here where Stryker
10
- * would score it CompileError, which can move a score that enables the checker by its share.
11
- *
12
- * The project's own compiler, `tsgo` when installed and `tsc` otherwise, over its own tsconfig.
13
- * Survivors are checked in batches with at most one mutant per file, and each error is laid at the
14
- * mutant in the file it names. An error in a file no mutant of the batch touched (a type that
15
- * flowed across files) sends that batch back one mutant at a time, so no verdict is a guess.
16
- */
17
1
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
18
2
  import path from 'node:path';
19
3
  import { ambientRuntime, isWindows, run as spawn } from 'bellpull';
20
4
  import { makeSandbox } from './command-runner.js';
21
5
  const OUTPUT_TAIL = 1500;
22
- /**
23
- * `src/a.ts(3,7): error TS2322: …` — tsc's and tsgo's `--pretty false` shape. No anchors: the lazy
24
- * first group already makes the leftmost match start the line, and the greedy last one ends it.
25
- */
26
6
  const DIAGNOSTIC = /(.+?)\((\d+),(\d+)\): error (TS\d+: .*)/;
27
- /** The source with one mutant applied. Columns are UTF-16 and 1-based, as JavaScript counts. */
28
7
  export function applyMutant(source, m) {
29
8
  const starts = [0];
30
9
  for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1))
31
10
  starts.push(i + 1);
32
- // Stryker disable next-line NumericLiteral: `starts` holds every line the checker reports, so the fallback never runs
33
11
  const at = (p) => (starts[p.line - 1] ?? 0) + p.column - 1;
34
12
  return source.slice(0, at(m.start)) + m.replacement + source.slice(at(m.end));
35
13
  }
36
- /** Errors per file (repository-relative), from the compiler's output. */
37
14
  export function parseDiagnostics(output, cwd) {
38
15
  const errors = new Map();
39
16
  for (const line of output.split('\n')) {
40
17
  const hit = DIAGNOSTIC.exec(line.trim());
41
18
  if (!hit)
42
19
  continue;
43
- // Stryker disable next-line StringLiteral: all four groups take part in every match, so the defaults are never used; they are there for the type
44
20
  const [, name = '', lineNo = '', , message = ''] = hit;
45
21
  const file = path.relative(cwd, path.resolve(cwd, name)).split(path.sep).join('/');
46
22
  if (!errors.has(file))
@@ -48,7 +24,6 @@ export function parseDiagnostics(output, cwd) {
48
24
  }
49
25
  return errors;
50
26
  }
51
- /** Batches of survivors with at most one per file, so each error names its mutant. */
52
27
  export function batches(items) {
53
28
  const out = [];
54
29
  for (const m of items) {
@@ -60,7 +35,6 @@ export function batches(items) {
60
35
  }
61
36
  return out;
62
37
  }
63
- /** npm's .bin entry for tsgo: `tsgo.cmd` on Windows, where the extensionless file beside it is a shell script. */
64
38
  export const tsgoBin = (windows) => (windows ? 'tsgo.cmd' : 'tsgo');
65
39
  function compiler(o) {
66
40
  if (o.command)
@@ -68,12 +42,10 @@ function compiler(o) {
68
42
  const tsgo = path.join(o.root, 'node_modules', '.bin', tsgoBin(isWindows(ambientRuntime())));
69
43
  return existsSync(tsgo) ? [tsgo] : ['npx', 'tsc'];
70
44
  }
71
- /** The undetected mutants that do not type-check, each with the compiler's first error in its file. */
72
45
  export async function checkSurvivors(o, survivors) {
73
46
  const failing = new Map();
74
47
  if (survivors.length === 0)
75
48
  return failing;
76
- // Stryker disable next-line StringLiteral: compiler() never returns an empty list, so the default is never taken; it is there for the type
77
49
  const [command = '', ...prefix] = compiler(o);
78
50
  const args = [...prefix, '--noEmit', '--pretty', 'false', '-p', o.tsconfig];
79
51
  const sandboxes = Array.from({ length: Math.max(1, Math.min(o.concurrency, survivors.length)) }, () => makeSandbox(o.root));
@@ -82,7 +54,6 @@ export async function checkSurvivors(o, survivors) {
82
54
  const r = await spawn(command, args, { cwd: sandbox, runtime: ambientRuntime() });
83
55
  return { ok: r.ok, errors: parseDiagnostics(`${r.stdout}\n${r.stderr}`, sandbox), output: `${r.stdout}${r.stderr}` };
84
56
  };
85
- // Stryker disable next-line StringLiteral: there is always at least one sandbox, so the fallback is never used; it is there for the type
86
57
  const base = await check(sandboxes[0] ?? '');
87
58
  if (!base.ok)
88
59
  throw new Error(`the project does not type-check with no mutant applied (\`${[command, ...args].join(' ')}\`): ${base.output.trim().slice(-OUTPUT_TAIL)}. fix: make the type check pass first, or drop checkers`);
@@ -98,7 +69,7 @@ export async function checkSurvivors(o, survivors) {
98
69
  const touched = new Set(batch.map((m) => m.file));
99
70
  if (batch.length > 1 && [...r.errors.keys()].some((f) => !touched.has(f))) {
100
71
  for (const m of batch)
101
- await run(sandbox, [m]); // an error that crossed files: one at a time
72
+ await run(sandbox, [m]);
102
73
  return;
103
74
  }
104
75
  for (const m of batch) {
@@ -115,9 +86,7 @@ export async function checkSurvivors(o, survivors) {
115
86
  return failing;
116
87
  }
117
88
  finally {
118
- // No `force`: each sandbox was made above and nothing else removes it.
119
89
  for (const s of sandboxes)
120
90
  rmSync(s, { recursive: true });
121
91
  }
122
92
  }
123
- //# sourceMappingURL=checker.js.map
package/dist/child.js CHANGED
@@ -1,24 +1,14 @@
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
1
  import { readFileSync, writeFileSync } from 'node:fs';
14
2
  import path from 'node:path';
15
3
  import { fileURLToPath } from 'node:url';
16
4
  import { ExitCode } from 'burgee';
5
+ import { BEAT_ENV, beatFile, CPU_SAMPLE_MS } from './heartbeat.js';
17
6
  import { resolveEsm } from './resolve.js';
18
- import { ACTIVE_MUTANT_ENV, argv, env, exit } from './runtime.js';
7
+ import { ACTIVE_MUTANT_ENV, argv, env, exit, pid } from './runtime.js';
19
8
  import { instrumentedSources } from './vite-plugin.js';
20
9
  const config = JSON.parse(readFileSync(argv[2] ?? '', 'utf8'));
21
10
  env['NODE_ENV'] ??= 'test';
11
+ env[BEAT_ENV] = beatFile(path.dirname(config.instrumentedMap), pid);
22
12
  const vitestNode = (await import(resolveEsm('vitest/node', config.root)));
23
13
  const runner = fileURLToPath(new URL('./runner.js', import.meta.url));
24
14
  const silent = { onInit() { }, onFinished() { } };
@@ -34,25 +24,13 @@ const vitest = await vitestNode.createVitest('test', {
34
24
  reporters: env['OXMUTATE_DEBUG_VITEST'] ? ['default'] : [silent],
35
25
  coverage: { enabled: false },
36
26
  passWithNoTests: true,
37
- // Bail is ours (per mutant); Vitest must run every test it is handed.
38
27
  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
28
  fsModuleCache: false,
44
29
  experimental: { fsModuleCache: false },
45
30
  ...(config.inlineDeps && { server: { deps: { inline: true } } }),
46
- },
47
- // A private Vite cache per run, so no cache outside this run can answer for a mutated file.
48
- { plugins: [instrumentedSources(config.instrumentedMap)], cacheDir: path.join(path.dirname(config.instrumentedMap), 'vite-cache') });
49
- // A config with `projects` resolves each project's `runner` and `isolate` from its own inline
50
- // config, never from these options, so its tests ran without oxmutate's runner and the dry run
51
- // recorded none (burgee, weekly run 40783405734, 2026-10-28). Each project keeps its own pool:
52
- // burgee's run in forks, and its process.env lock fails under threads.
31
+ }, { plugins: [instrumentedSources(config.instrumentedMap)], cacheDir: path.join(path.dirname(config.instrumentedMap), 'vite-cache') });
53
32
  for (const project of vitest.projects)
54
33
  Object.assign(project.config, { runner, isolate: true });
55
- /** Has the runner recorded a kill for this mutant in this unit's results yet? */
56
34
  function killed(results, id) {
57
35
  let text = '';
58
36
  try {
@@ -63,17 +41,11 @@ function killed(results, id) {
63
41
  }
64
42
  return text.split('\n').some((line) => line.includes(`"id":"${id}"`) && line.includes('"killed":true'));
65
43
  }
66
- /** An `isolated` unit: its mutants armed one after another, each across its files. */
67
44
  async function runIsolated(unit) {
68
- // Stop a file at its first failing test: one failure kills the mutant, and the rest of the
69
- // file is wasted time (burgee/paratext hyperlinks.test.ts: 2.3 s a run, the first failure
70
- // typically in its first tests). Read per start, so dry and warm runs still run everything
71
- // (measured on Vitest 4.1, 2026-10-27).
72
45
  vitest.config.bail = 1;
73
46
  try {
74
47
  for (const item of unit.batch ?? []) {
75
48
  env[ACTIVE_MUTANT_ENV] = item.id;
76
- // The runaway guard warm runs have: a loop the mutant causes stops at the limit, not the watchdog.
77
49
  if (item.hitLimit)
78
50
  env['OXMUTATE_HIT_LIMIT'] = String(item.hitLimit);
79
51
  else
@@ -83,8 +55,6 @@ async function runIsolated(unit) {
83
55
  else
84
56
  delete env['OXMUTATE_ONLY_TESTS'];
85
57
  writeFileSync(unit.progress, `${item.id} ${String(Date.now())}\n`);
86
- // One file at a time, cheapest first (the coordinator orders them), stopping at the first
87
- // file that kills the mutant: Stryker runs every file for every isolated mutant.
88
58
  for (const file of item.files) {
89
59
  await vitest.start([file]);
90
60
  if (killed(unit.results, item.id))
@@ -101,8 +71,6 @@ async function runIsolated(unit) {
101
71
  writeFileSync(unit.progress, '');
102
72
  }
103
73
  async function runUnit(unit) {
104
- // Workers copy the environment when they start, so each run sees the unit's settings
105
- // (verified across re-runs of one instance, 2026-10-26).
106
74
  env['OXMUTATE_MODE'] = unit.mode;
107
75
  env['OXMUTATE_RESULTS'] = unit.results;
108
76
  env['OXMUTATE_PROGRESS'] = unit.progress;
@@ -120,27 +88,26 @@ async function runUnit(unit) {
120
88
  if (unit.mode === 'isolated')
121
89
  await runIsolated(unit);
122
90
  else {
123
- // A dry run names itself in the progress file, so the watchdog holds it to its budget.
124
91
  if (unit.mode === 'dry')
125
92
  writeFileSync(unit.progress, `dry ${String(Date.now())}\n`);
126
93
  await vitest.start(unit.files.length ? unit.files : undefined);
127
94
  }
128
95
  }
129
- /** How long `close` waits for `vitest.close()` before the server exits regardless. */
130
96
  const CLOSE_GRACE_MS = 2000;
131
97
  process.on('message', (msg) => {
132
98
  if (msg.type === 'run' && msg.unit) {
133
99
  runUnit(msg.unit).then(() => process.send?.({ type: 'done' }), (error) => process.send?.({ type: 'done', error: error instanceof Error ? error.message : String(error) }));
134
100
  }
135
101
  else if (msg.type === 'close') {
136
- // A worker thread stuck in a runaway mutant never lets `vitest.close()` settle: exit anyway.
137
102
  setTimeout(() => exit(ExitCode.OK), CLOSE_GRACE_MS).unref();
138
103
  void vitest.close().finally(() => exit(ExitCode.OK));
139
104
  }
140
105
  });
141
- // The coordinator is gone (finished, crashed or killed): nothing will read this server's results,
142
- // and a worker thread may still be looping in a mutant. Two such servers were found spinning at
143
- // 100% CPU 11 hours after their run ended, and slowed the next run 50x (2026-10-27).
144
106
  process.on('disconnect', () => exit(ExitCode.OK));
107
+ const sendCpu = () => {
108
+ const { user, system } = process.cpuUsage();
109
+ process.send?.({ type: 'cpu', at: Date.now(), us: user + system });
110
+ };
111
+ sendCpu();
112
+ setInterval(sendCpu, CPU_SAMPLE_MS).unref();
145
113
  process.send?.({ type: 'ready' });
146
- //# sourceMappingURL=child.js.map
package/dist/cli.js CHANGED
@@ -1,9 +1,3 @@
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
1
  import { existsSync, readFileSync } from 'node:fs';
8
2
  import path from 'node:path';
9
3
  import { defineCommand, defineProgram, ExitCode } from 'burgee';
@@ -31,7 +25,6 @@ function survivorRecord(m) {
31
25
  function summaryData(r, written) {
32
26
  const count = (s) => r.mutants.filter((m) => m.status === s).length;
33
27
  const data = {
34
- // burgee N7: an idempotent command says whether it changed anything (reports were written).
35
28
  changed: written.length > 0,
36
29
  version: VERSION,
37
30
  score: Number(r.score.mutationScore.toFixed(2)),
@@ -50,8 +43,6 @@ function summaryData(r, written) {
50
43
  data[TEXT] = () => textSummary(r);
51
44
  return data;
52
45
  }
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
46
  const WEAKENED_SHOWN = 3;
56
47
  function overridesFrom(o, root) {
57
48
  const overrides = {};
@@ -162,4 +153,3 @@ export const program = defineProgram({
162
153
  description: 'Mutation testing on the oxc engine: Stryker-compatible, built for CI and coding agents',
163
154
  commands: [runCommand, survivorsCommand],
164
155
  });
165
- //# sourceMappingURL=cli.js.map
@@ -1,27 +1,9 @@
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
1
  import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
14
2
  import path from 'node:path';
15
3
  import { ambientRuntime, run as spawn } from 'bellpull';
16
4
  import { ACTIVE_MUTANT_ENV, env } from './runtime.js';
17
5
  const SANDBOX_DIR = '.oxmutate-tmp';
18
- /** Stryker's default `ignorePatterns`, plus this sandbox. */
19
6
  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
7
  export function makeSandbox(root) {
26
8
  const parent = path.join(root, SANDBOX_DIR);
27
9
  mkdirSync(parent, { recursive: true });
@@ -32,21 +14,13 @@ export function makeSandbox(root) {
32
14
  return sandbox;
33
15
  }
34
16
  const MS_PER_MINUTE = 60_000;
35
- /** How much of a failing dry run's output the error keeps. */
36
17
  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
18
  const PROCESS_SLACK_MS = 5000;
43
- /** Runs the command over every mutant not already decided; returns the dry run's time. */
44
19
  export async function runCommandMutants(o, mutants) {
45
20
  const sandbox = makeSandbox(o.root);
46
21
  try {
47
22
  for (const [file, code] of Object.entries(o.codeMap))
48
23
  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
24
  const [command = '', ...args] = o.command.trim().split(/\s+/);
51
25
  const once = (timeout, id) => spawn(command, args, { cwd: sandbox, runtime: ambientRuntime(), timeout, env: id ? { ...env, [ACTIVE_MUTANT_ENV]: id } : { ...env } });
52
26
  const dryBudget = o.dryRunTimeoutMinutes * MS_PER_MINUTE;
@@ -80,4 +54,3 @@ export async function runCommandMutants(o, mutants) {
80
54
  rmSync(sandbox, { recursive: true, force: true });
81
55
  }
82
56
  }
83
- //# sourceMappingURL=command-runner.js.map
package/dist/config.js CHANGED
@@ -1,16 +1,8 @@
1
- /**
2
- * Options, read from `stryker.config.*` as it stands (migration is a no-op: the file Stryker reads
3
- * is the file oxmutate reads) and from `oxmutate.config.*`, which wins where both set a key.
4
- * Every key Stryker documents that oxmutate does not use is reported, never silently dropped
5
- * (spec R15).
6
- */
7
1
  import { existsSync, readFileSync } from 'node:fs';
8
2
  import { availableParallelism } from 'node:os';
9
3
  import path from 'node:path';
10
4
  import { pathToFileURL } from 'node:url';
11
- import {} from './native.js';
12
5
  import { resolveEsm } from './resolve.js';
13
- import {} from './spec-oracle.js';
14
6
  const NOT_NEEDED = {
15
7
  inPlace: 'oxmutate never copies or rewrites the project; instrumented sources are served from memory',
16
8
  tempDirName: 'no sandbox directory exists',
@@ -35,10 +27,6 @@ const NOT_NEEDED = {
35
27
  force: 'use --no-incremental',
36
28
  checkerNodeArgs: 'not implemented',
37
29
  };
38
- /**
39
- * Defaults where the config sets nothing. Stryker's, except the per-mutant slack: Stryker's
40
- * `timeoutMS` is 5000, and a warm run needs far less (cli.ts `--mutant-timeout`).
41
- */
42
30
  const MAX_DEFAULT_CONCURRENCY = 8;
43
31
  const DEFAULT_TIMEOUT_MS = 500;
44
32
  const DEFAULT_TIMEOUT_FACTOR = 1.5;
@@ -48,7 +36,6 @@ const DEFAULT_DRY_RUN_MINUTES = 5;
48
36
  const USED = new Set(['mutate', 'vitest', 'concurrency', 'timeoutMS', 'timeoutFactor', 'thresholds', 'ignoreStatic', 'reporters', 'jsonReporter', 'htmlReporter', 'incremental', 'incrementalFile', '$schema', 'mutator', 'dryRunOnly', 'dryRunTimeoutMinutes', 'buildCommand', 'testRunnerNodeArgs', 'allowEmpty', 'warnings', 'testRunner', 'commandRunner', 'checkers', 'tsconfigFile']);
49
37
  async function loadFile(file) {
50
38
  if (file.endsWith('.json')) {
51
- // Stryker disable next-line StringLiteral: without an encoding readFileSync returns a Buffer, which JSON.parse reads as UTF-8 text all the same
52
39
  return JSON.parse(readFileSync(file, 'utf8'));
53
40
  }
54
41
  const mod = (await import(pathToFileURL(file).href));
@@ -63,17 +50,9 @@ function find(root, base) {
63
50
  }
64
51
  return undefined;
65
52
  }
66
- /** The Vite (and Vite+) config files, in the order Vite and Vitest look for them. */
67
53
  const VITE_CONFIGS = ['vite.config', 'vitest.config'].flatMap((base) => ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'].map((ext) => `${base}${ext}`));
68
- /**
69
- * The `mutate` block of the project's Vite config — the Vite+ shape, one file for test, lint, fmt
70
- * and mutate. Loaded with the project's own Vite (`loadConfigFromFile`), so a TypeScript config and
71
- * its imports work as Vite sees them. Inside the block the file list is `include`, as Vitest names
72
- * it; Stryker's `mutate` is accepted too. No Vite installed, or no block: nothing.
73
- */
74
54
  async function fromViteConfig(root) {
75
55
  const file = VITE_CONFIGS.map((f) => path.join(root, f)).find((f) => existsSync(f));
76
- // Stryker disable next-line ConditionalExpression: without the guard Vite searches the root for the same names, finds none, and returns null, which returns undefined below
77
56
  if (!file)
78
57
  return undefined;
79
58
  let vite;
@@ -83,14 +62,11 @@ async function fromViteConfig(root) {
83
62
  catch {
84
63
  return undefined;
85
64
  }
86
- // Stryker disable next-line StringLiteral: the log level only decides whether Vite prints, and a config that loads prints nothing at any level
87
65
  const level = 'silent';
88
66
  const loaded = await vite.loadConfigFromFile({ command: 'serve', mode: 'test' }, file, root, level);
89
- // Stryker disable next-line OptionalChaining: given a file that exists, Vite returns a config or throws; null is its answer only when it found no file
90
67
  const block = loaded?.config['mutate'];
91
68
  if (!block || typeof block !== 'object')
92
69
  return undefined;
93
- // `include` is the Vitest spelling; Stryker's `mutate`, when given too, is spread over it.
94
70
  const { include, ...rest } = block;
95
71
  return { file, block: { mutate: include, ...rest } };
96
72
  }
@@ -140,7 +116,6 @@ export async function loadOptions(root, overrides = {}, configFile) {
140
116
  allowEmpty: raw['allowEmpty'] === true,
141
117
  specOracles: raw['specOracles'] ?? [],
142
118
  ignore: raw['ignore'] ?? [],
143
- // Stryker disable next-line ArrayDeclaration: a placeholder element is not 'typescript' either, so includes() is false as for an empty list
144
119
  typeCheck: (raw['checkers'] ?? []).includes('typescript'),
145
120
  tsconfigFile: raw['tsconfigFile'] ?? 'tsconfig.json',
146
121
  typeChecker: typeof raw['typeChecker'] === 'string' ? raw['typeChecker'] : undefined,
@@ -149,17 +124,14 @@ export async function loadOptions(root, overrides = {}, configFile) {
149
124
  ...overrides,
150
125
  };
151
126
  }
152
- // One `\s` after next-line is enough: the list's class takes any further whitespace, and is trimmed.
153
127
  const DIRECTIVE = /\/\/\s*(?:Stryker|oxmutate)\s+disable\s+next-line\s([\w,\s]+?|all)\s*:\s*(.*)/i;
154
128
  const NO_REASON = /\/\/\s*(?:Stryker|oxmutate)\s+disable\s+next-line\s([\w,\s]+?|all)\s*$/i;
155
- /** Disable directives in a file. A directive without a reason is an error (burgee's own rule). */
156
129
  export function disables(source, file) {
157
130
  const out = [];
158
131
  const errors = [];
159
132
  source.split('\n').forEach((line, i) => {
160
133
  const m = DIRECTIVE.exec(line);
161
134
  if (m) {
162
- // Stryker disable next-line StringLiteral: both groups take part in every match, so the fallback is never used; it is there for the type
163
135
  const [list, reason] = [(m[1] ?? '').trim(), (m[2] ?? '').trim()];
164
136
  out.push({ line: i + 2, mutators: list.toLowerCase() === 'all' ? 'all' : list.split(',').map((s) => s.trim()).filter(Boolean), reason });
165
137
  }
@@ -169,4 +141,3 @@ export function disables(source, file) {
169
141
  });
170
142
  return { disables: out, errors };
171
143
  }
172
- //# sourceMappingURL=config.js.map
@@ -36,10 +36,16 @@ export interface RunStats {
36
36
  instrumentMs: number;
37
37
  dryRunMs: number;
38
38
  mutationMs: number;
39
+ /** Static mutants decided in live workers (section 5b), between the warm and the isolated phases. */
40
+ staticMs: number;
41
+ /** Unarmed re-collections in that phase, the guard each live static verdict waits on (runner.ts). */
42
+ staticRecollections: number;
39
43
  isolatedMs: number;
40
44
  wallMs: number;
41
45
  testExecutions: number;
42
46
  watchdogKills: number;
47
+ /** Of `watchdogKills`, the stops whose worker was idle, waiting (heartbeat.ts). In an isolated run each is a Timeout as it stands. */
48
+ idleStalls: number;
43
49
  crashes: number;
44
50
  canaryFiles: number;
45
51
  /** Contexts restarted because a mutant left state behind (the per-execution canary fired). */