oxmutate 1.2.0 → 1.2.2

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/dist/scheduler.js CHANGED
@@ -1,13 +1,3 @@
1
- /**
2
- * The server pool and its work-stealing queue.
3
- *
4
- * A server is a child process holding one warm Vitest (`child.ts`). Work is a queue of units;
5
- * an idle server takes the next one. A unit whose mutant runs away is killed with its server,
6
- * its unfinished part is split and put back at the front of the queue for the other servers,
7
- * and a spare server, already warm, takes the dead one's place. One unlucky test file can no
8
- * longer serialize a run behind its runaways (es-toolkit, 2026-10-26: 12 runaways in one static
9
- * shard set the whole mutation phase at 42.9 s while the other shards finished in 8 s).
10
- */
11
1
  import { fork } from 'node:child_process';
12
2
  import { readFileSync, writeFileSync } from 'node:fs';
13
3
  import path from 'node:path';
@@ -15,13 +5,9 @@ import { fileURLToPath } from 'node:url';
15
5
  import { beatFile, classifyStall, CPU_WINDOW_MS, cpuShare, readBeat } from './heartbeat.js';
16
6
  import { env, stderr } from './runtime.js';
17
7
  const childJs = fileURLToPath(new URL('./child.js', import.meta.url));
18
- /** The tail of a server's stderr kept for an error message. */
19
8
  const STDERR_TAIL = 4000;
20
- /** How often the watchdog reads a running unit's progress line. */
21
9
  const WATCHDOG_POLL_MS = 50;
22
- /** How long a server asked to close gets before it is killed (it gives itself the same). */
23
10
  const CLOSE_GRACE_MS = 2000;
24
- /** The mutant a unit's progress file says is running, if any. */
25
11
  function inFlight(progress) {
26
12
  try {
27
13
  return readFileSync(progress, 'utf8').trim().split(' ')[0] || undefined;
@@ -31,15 +17,12 @@ function inFlight(progress) {
31
17
  }
32
18
  }
33
19
  const PERCENT = 100;
34
- /** How many CPU samples a server keeps: a little over the window. */
35
20
  const CPU_SAMPLES_KEPT = 8;
36
21
  class Server {
37
22
  child;
38
23
  ready;
39
24
  stderr = '';
40
- /** Where this server's test workers beat (heartbeat.ts). */
41
25
  beat;
42
- /** The server's latest CPU readings, oldest first (child.ts sends them). */
43
26
  cpu = [];
44
27
  onMessage;
45
28
  onExit;
@@ -98,7 +81,6 @@ class Server {
98
81
  const [id = '', since = '0'] = line.split(' ');
99
82
  const now = Date.now();
100
83
  if (now - Number(since) > budget(id)) {
101
- // Read before the kill: an idle stop is a Timeout as it stands, a busy one is re-run (heartbeat.ts).
102
84
  const beat = readBeat(this.beat, now);
103
85
  const share = cpuShare(this.cpu, now);
104
86
  const stall = classifyStall({ beat, cpuShare: share });
@@ -161,7 +143,6 @@ export function createPool(config, size, workDir, tag) {
161
143
  for (const w of waiters.splice(0))
162
144
  w();
163
145
  };
164
- /** The next unit `prepare` keeps, or undefined when the queue is empty. */
165
146
  const take = () => {
166
147
  for (let unit = queue.shift(); unit; unit = queue.shift())
167
148
  if (!prepare || prepare(unit))
@@ -206,4 +187,3 @@ export function createPool(config, size, workDir, tag) {
206
187
  },
207
188
  };
208
189
  }
209
- //# sourceMappingURL=scheduler.js.map
@@ -1,35 +1,15 @@
1
- /**
2
- * Spec oracles (roadmap M3.5): a formal model's traces, replayed against the code with each mutant
3
- * armed, as a second and independent verdict beside the tests.
4
- *
5
- * The contract is the trace, not the method. A backend's only job is to leave `*.itf.json` files
6
- * (the Informal Trace Format) in a directory:
7
- *
8
- * - `quint`: `quint run --mbt` simulates the model and writes the traces, with each step's action
9
- * and nondeterministic picks, so a replay test knows what to call.
10
- * - `itf`: a directory some other tool already filled (Apalache writes ITF too; so can a
11
- * property-based generator). This is also how a test pins its traces.
12
- *
13
- * The project's replay tests (spec.ts) turn a trace into calls and assertions. Which mutants a
14
- * replay kills is reported per oracle as the F metrics of metrics.md: spec-kill share (F1), the
15
- * test-spec gap (F2: the spec kills, the tests miss, with the trace that did it), spec escapes
16
- * (F3), spec cost per mutant (F4) and mapping coverage (F6).
17
- */
18
1
  import { mkdirSync } from 'node:fs';
19
2
  import path from 'node:path';
20
3
  import { ambientRuntime, run as spawn } from 'bellpull';
21
4
  const DEFAULT_TRACES = 100;
22
5
  const DEFAULT_STEPS = 20;
23
6
  const DEFAULT_QUINT = 'npx -y @informalsystems/quint@0.33.0';
24
- /** How much of a failing quint run's output the error keeps. */
25
7
  const OUTPUT_TAIL = 1000;
26
- /** The oracle's traces on disk: generated into `work` for `quint`, the given directory for `itf`. */
27
8
  export async function tracesFor(oracle, root, work) {
28
9
  if (oracle.backend === 'itf')
29
10
  return path.resolve(root, oracle.dir ?? '');
30
11
  const dir = path.join(work, `spec-${oracle.name}`);
31
12
  mkdirSync(dir, { recursive: true });
32
- // Stryker disable next-line StringLiteral: split always returns at least one element, so the default is never taken; it is there for the type
33
13
  const [command = '', ...prefix] = (oracle.quint ?? DEFAULT_QUINT).split(/\s+/);
34
14
  const n = String(oracle.traces ?? DEFAULT_TRACES);
35
15
  const args = [...prefix, 'run', path.resolve(root, oracle.model ?? ''), '--mbt', '--n-traces', n, '--max-samples', n, '--max-steps', String(oracle.maxSteps ?? DEFAULT_STEPS), '--out-itf', path.join(dir, 't{seq}.itf.json')];
@@ -45,7 +25,6 @@ const share = (n, of) => (of ? (PERCENT * n) / of : 0);
45
25
  const located = ({ id, file, line, mutatorName, replacement }) => ({ id, file, line, mutatorName, replacement });
46
26
  const median = (xs) => {
47
27
  const sorted = [...xs].sort((a, b) => a - b);
48
- // Stryker disable next-line NumericLiteral: guarded by `sorted.length`, so the fallback never runs
49
28
  return sorted.length ? (sorted[Math.floor(sorted.length / 2)] ?? 0) : 0;
50
29
  };
51
30
  const testsMiss = (m) => m.status === 'Survived' || m.status === 'NoCoverage';
@@ -60,10 +39,8 @@ export function summarizeSpec({ oracle, modelledFiles, traces, mutants, testName
60
39
  modelled: here.length,
61
40
  mappingCoverage: share(reached.length, here.length),
62
41
  specKillShare: share(killed.length, here.length),
63
- // Stryker disable next-line OptionalChaining: every mutant in `killed` has spec.status Killed, so spec is defined; `?.` is only for the type
64
42
  testSpecGap: killed.filter(testsMiss).map((m) => ({ ...located(m), trace: testName(m.spec?.killedBy?.[0] ?? '') })),
65
43
  specEscapes: here.filter((m) => testsMiss(m) && m.spec?.status !== 'Killed').map(located),
66
44
  msPerSpecExecution: median(specMs),
67
45
  };
68
46
  }
69
- //# sourceMappingURL=spec-oracle.js.map
package/dist/spec.js CHANGED
@@ -1,31 +1,8 @@
1
- /**
2
- * `oxmutate/spec`: what a replay test imports to drive the code under test through a formal
3
- * model's traces.
4
- *
5
- * A spec oracle (spec-oracle.ts) asks a backend for traces of a model: Quint's `quint run --mbt`,
6
- * or a directory of ITF files any tool wrote (Apalache emits the same format). A replay test,
7
- * written once per modelled component, takes each trace, performs each step's action on the real
8
- * code, and asserts the model's state. Under oxmutate those tests run with each mutant armed, on a
9
- * verdict track of their own: a mutant a replay test catches is `killedBySpec`, whatever the
10
- * ordinary tests say about it.
11
- *
12
- * import { loadTraces } from 'oxmutate/spec';
13
- * const traces = loadTraces('save-restore');
14
- * it.skipIf(traces.length === 0).each(traces)('replays $name', (trace) => { … });
15
- *
16
- * Outside an oxmutate run there are no traces, so the replay tests skip.
17
- */
18
1
  import { readdirSync, readFileSync } from 'node:fs';
19
2
  import path from 'node:path';
20
3
  import { env } from './runtime.js';
21
- /** JSON map of oracle name → directory of `*.itf.json` traces, set by oxmutate for its servers. */
22
4
  export const SPEC_TRACES_ENV = 'OXMUTATE_SPEC_TRACES';
23
5
  const META = new Set(['#meta', 'mbt::actionTaken', 'mbt::nondetPicks']);
24
- /**
25
- * ITF values as plain JavaScript: `#bigint` as a number (a bigint past 2^53), `#set` and `#tup`
26
- * as arrays, `#map` as a Map. A `JSON.parse` reviver, so it runs bottom-up with no recursion of
27
- * its own: children are already decoded when their parent is.
28
- */
29
6
  function revive(_key, v) {
30
7
  if (!v || typeof v !== 'object' || Array.isArray(v))
31
8
  return v;
@@ -42,7 +19,6 @@ function revive(_key, v) {
42
19
  return new Map(o['#map']);
43
20
  return v;
44
21
  }
45
- /** One ITF value, decoded as in a trace file. */
46
22
  export const decode = (v) => JSON.parse(JSON.stringify(v), revive);
47
23
  const pick = (v) => {
48
24
  if (!v || typeof v !== 'object' || !('tag' in v))
@@ -50,9 +26,7 @@ const pick = (v) => {
50
26
  const variant = v;
51
27
  return variant.tag === 'Some' ? variant.value : undefined;
52
28
  };
53
- /** One ITF file as steps. */
54
29
  export function readTrace(file) {
55
- // Stryker disable next-line StringLiteral: without an encoding readFileSync returns a Buffer, which JSON.parse reads as UTF-8 text all the same
56
30
  const itf = JSON.parse(readFileSync(file, 'utf8'), revive);
57
31
  const steps = itf.states.map((s) => ({
58
32
  action: String(s['mbt::actionTaken'] ?? ''),
@@ -61,7 +35,6 @@ export function readTrace(file) {
61
35
  }));
62
36
  return { name: path.basename(file), steps };
63
37
  }
64
- /** The traces oxmutate generated for this oracle, in file order; none outside an oxmutate run. */
65
38
  export function loadTraces(oracle) {
66
39
  const dirs = env[SPEC_TRACES_ENV] ? JSON.parse(env[SPEC_TRACES_ENV]) : {};
67
40
  const dir = dirs[oracle];
@@ -72,4 +45,3 @@ export function loadTraces(oracle) {
72
45
  .sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
73
46
  .map((f) => readTrace(path.join(dir, f)));
74
47
  }
75
- //# sourceMappingURL=spec.js.map
@@ -1,7 +1,3 @@
1
- /**
2
- * Serves instrumented sources to Vitest from memory, in place of the files on disk. No sandbox
3
- * copy: the project is run where it is, and only the mutated files' contents differ.
4
- */
5
1
  import { readFileSync } from 'node:fs';
6
2
  import path from 'node:path';
7
3
  export function instrumentedSources(mapPath) {
@@ -15,4 +11,3 @@ export function instrumentedSources(mapPath) {
15
11
  },
16
12
  };
17
13
  }
18
- //# sourceMappingURL=vite-plugin.js.map
package/dist/vite.js CHANGED
@@ -1,3 +1 @@
1
- /** Types a `mutate` block written apart from `defineConfig` (in a shared file, say); returns it unchanged. */
2
1
  export const defineMutate = (config) => config;
3
- //# sourceMappingURL=vite.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxmutate",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Mutation testing for JavaScript and TypeScript on the oxc engine, run warm inside the project's own Vitest, with CI lanes, SARIF and an agent surface. No dependency outside the burgee family. A Stryker migration path.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "!dist/**/*.test.*"
65
65
  ],
66
66
  "scripts": {
67
- "build": "tsc -p tsconfig.build.json && node ../../scripts/declaration-reach.mts --prune",
67
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/strip-comments.mjs dist && node ../../scripts/declaration-reach.mts --prune",
68
68
  "build:native": "cd native && cargo build --release --features napi --lib && node ../scripts/copy-binding.mjs",
69
69
  "typecheck": "tsc -p tsconfig.json --noEmit",
70
70
  "test": "vitest run && vitest run --config compat/vitest.config.ts",