oxmutate 1.5.0 → 2.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  <a href="https://www.npmjs.com/package/oxmutate?activeTab=code"><img src="https://img.shields.io/npm/unpacked-size/oxmutate?style=flat-square" alt="Unpacked size of the latest oxmutate release on npm" /></a>
21
21
  <a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/dependencies-2%20in%20family%2C%200%20outside-0a6b47?style=flat-square" alt="Two dependencies, all in the burgee family (bellpull, burgee), none outside it" /></a>
22
22
  <a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/types-included-blue?style=flat-square" alt="TypeScript types included for every entry point" /></a>
23
- <a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/Node.js-20.19%2B%20%7C%2022.13%2B-green?style=flat-square" alt="Node.js 20.19+ or 22.13+" /></a>
23
+ <a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/package.json"><img src="https://img.shields.io/badge/Node.js-22.13%2B-green?style=flat-square" alt="Node.js 22.13+" /></a>
24
24
  <a href="https://github.com/ofri-peretz/burgee/blob/main/packages/oxmutate/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" alt="License: MIT" /></a>
25
25
  <a href="https://www.npmjs.com/package/oxmutate#provenance"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fregistry.npmjs.org%2Foxmutate%2Flatest&query=%24.dist.attestations.provenance~&label=npm&style=flat-square&color=0a6b47" alt="npm provenance of the latest release, read live from its registry attestation" /></a>
26
26
  </p>
@@ -335,7 +335,7 @@ The `oxmutate` bin is that API, declared once on burgee.
335
335
 
336
336
  ## Requirements
337
337
 
338
- Node 20.19 or later on the 20 line, or 22.13 or later. Vitest 4.1 or later in the project, for the
338
+ Node 22.13 or later. Vitest 4.1 or later in the project, for the
339
339
  default runner. macOS (arm64, x64), Linux (glibc x64 and arm64, musl x64) or Windows (x64).
340
340
 
341
341
  ## Where it sits
Binary file
Binary file
@@ -45,6 +45,19 @@ export interface RunStats {
45
45
  isolatedMs: number;
46
46
  wallMs: number;
47
47
  testExecutions: number;
48
+ /** Warm units, send to outcome, summed over servers: what `mutationMs` is in server time. */
49
+ warmUnitMs: number;
50
+ /** Of `warmUnitMs`, the time inside armed test runs. The rest is starting workers, collecting files, canaries and hooks. */
51
+ warmArmedMs: number;
52
+ /** Isolated runs by what sent the mutant there: the parts of `isolatedRuns`. */
53
+ isolatedByCause: {
54
+ static: number;
55
+ stopped: number;
56
+ unfinished: number;
57
+ rerun: number;
58
+ confirm: number;
59
+ flaky: number;
60
+ };
48
61
  watchdogKills: number;
49
62
  /** Of `watchdogKills`, the stops whose worker was idle, waiting (heartbeat.ts). In an isolated run each is a Timeout as it stands. */
50
63
  idleStalls: number;
@@ -73,7 +73,7 @@ export async function run(options) {
73
73
  const { root } = options;
74
74
  const work = mkdtempSync(path.join(tmpdir(), 'oxmutate-'));
75
75
  const stats = {
76
- files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0,
76
+ files: 0, mutants: 0, instrumentMs: 0, dryRunMs: 0, mutationMs: 0, staticMs: 0, staticRecollections: 0, isolatedMs: 0, wallMs: 0, testExecutions: 0, warmUnitMs: 0, warmArmedMs: 0, isolatedByCause: { static: 0, stopped: 0, unfinished: 0, rerun: 0, confirm: 0, flaky: 0 },
77
77
  watchdogKills: 0, idleStalls: 0, deadlocks: 0, crashes: 0, canaryFiles: 0, pollutionRestarts: 0, diskWrites: 0, auditSampled: 0, auditDisagreed: 0, survivorsOverturned: 0, timeoutsOverturned: 0, compileErrors: 0, isolatedRuns: 0, reused: 0, flakyTests: 0, flakyMutants: 0, units: 0, serversSpawned: 0,
78
78
  };
79
79
  let unitSeq = 0;
@@ -189,6 +189,9 @@ export async function run(options) {
189
189
  if (fileErrors.length) {
190
190
  throw new InstrumentFault(`${String(fileErrors.length)} test file(s) failed to load during the dry run, so their tests would be missing from every verdict: ${fileErrors.slice(0, 3).map((e) => `${path.relative(root, e.file)}: ${e.error}`).join('; ')}`);
191
191
  }
192
+ const unhandled = recs.reduce((n, r) => n + (r.unhandled ?? 0), 0);
193
+ if (unhandled > 0)
194
+ throw new InstrumentFault(`the suite raised ${String(unhandled)} unhandled error(s) with no mutant active, so no verdict can be trusted. fix: make \`vitest run\` pass with no "Unhandled Errors" first`);
192
195
  return { tests: recs.flatMap((r) => r.tests ?? []), perTest, static: stat, runOnce: recs.flatMap((r) => r.runOnce ?? []), modules: Object.assign({}, ...recs.map((r) => r.modules ?? {})) };
193
196
  };
194
197
  progress.phase('dry run');
@@ -412,6 +415,7 @@ export async function run(options) {
412
415
  const killedSoFar = (spec) => new Set([...runaways, ...crashedIds, ...records.filter((r) => r.type === 'mutant' && r.status !== 'Survived' && Boolean(r.spec) === spec).map((r) => r.id ?? '')]);
413
416
  const afterUnit = async (unit, outcome) => {
414
417
  inFlight.delete(unit.results);
418
+ stats.warmUnitMs += outcome.ms ?? 0;
415
419
  const wrote = await diskWrittenDuring(unit);
416
420
  if (wrote)
417
421
  return wrote;
@@ -484,6 +488,7 @@ export async function run(options) {
484
488
  if (r.type === 'mutant' && r.id)
485
489
  (byMutant.get(r.id) ?? byMutant.set(r.id, []).get(r.id))?.push(r);
486
490
  stats.testExecutions = records.filter((r) => r.type === 'mutant').length;
491
+ stats.warmArmedMs = records.reduce((sum, r) => sum + (r.type === 'mutant' ? (r.ms ?? 0) : 0), 0);
487
492
  stats.canaryFiles = new Set(records.filter((r) => r.type === 'polluted' && r.file).map((r) => r.file ?? '')).size;
488
493
  const isolatedQueue = [];
489
494
  for (const m of toRun) {
@@ -496,6 +501,7 @@ export async function run(options) {
496
501
  continue;
497
502
  }
498
503
  else if (runaways.has(m.id)) {
504
+ stats.isolatedByCause.stopped++;
499
505
  isolatedQueue.push(m);
500
506
  }
501
507
  else if (crashedIds.has(m.id)) {
@@ -514,6 +520,7 @@ export async function run(options) {
514
520
  m.testsCompleted = recs.length;
515
521
  }
516
522
  else {
523
+ stats.isolatedByCause.unfinished++;
517
524
  isolatedQueue.push(m);
518
525
  }
519
526
  }
@@ -674,11 +681,13 @@ export async function run(options) {
674
681
  const s0 = performance.now();
675
682
  const staticBack = env['OXMUTATE_ISOLATE_STATIC'] ? staticMutants : await decideStaticLive(staticMutants);
676
683
  stats.staticMs = performance.now() - s0;
684
+ stats.isolatedByCause.static = staticBack.length;
677
685
  isolatedQueue.push(...staticBack);
678
686
  progress.phase(`isolated runs (static mutants and stragglers: ${String(isolatedQueue.length)})`);
679
687
  const i0 = performance.now();
680
688
  const first = await runIsolated(isolatedQueue);
681
689
  const stopped = isolatedQueue.filter((m) => first.get(m.id)?.status === 'Timeout' && !first.get(m.id)?.idle);
690
+ stats.isolatedByCause.rerun += stopped.length;
682
691
  const again = await runIsolated(stopped, RERUN_SLACK);
683
692
  for (const m of stopped) {
684
693
  const v = again.get(m.id);
@@ -698,8 +707,10 @@ export async function run(options) {
698
707
  }
699
708
  }
700
709
  const survivors = toRun.filter((m) => m.status === 'Survived' && new Set(m.coveredBy).size > 1);
710
+ stats.isolatedByCause.confirm += survivors.length;
701
711
  const confirmed = await runIsolated(survivors);
702
712
  const stoppedHere = survivors.filter((m) => confirmed.get(m.id)?.status === 'Timeout' && !confirmed.get(m.id)?.idle);
713
+ stats.isolatedByCause.rerun += stoppedHere.length;
703
714
  const recheck = await runIsolated(stoppedHere, RERUN_SLACK);
704
715
  for (const m of stoppedHere) {
705
716
  const v = recheck.get(m.id);
@@ -732,6 +743,7 @@ export async function run(options) {
732
743
  const flakyCandidates = toRun.filter((m) => m.coveredBy.some((t) => flakyTests.has(t)));
733
744
  const seen = new Map(flakyCandidates.map((m) => [m.id, new Set([m.status === 'Timeout' ? 'Killed' : m.status])]));
734
745
  for (let n = 1; n <= options.flakyReruns && flakyCandidates.length; n++) {
746
+ stats.isolatedByCause.flaky += flakyCandidates.length;
735
747
  const again = await runIsolated(flakyCandidates);
736
748
  for (const m of flakyCandidates) {
737
749
  const v = again.get(m.id);
package/dist/reports.d.ts CHANGED
@@ -6,7 +6,6 @@ export declare function strykerReport(r: RunResult): object;
6
6
  /** SARIF 2.1.0: one result per surviving or uncovered mutant, fingerprinted by stable id. */
7
7
  export declare function sarifReport(r: RunResult): object;
8
8
  export declare function describe(m: MutantResult): string;
9
- /** The terminal summary: the score, what to look at first, what the run corrected. */
10
9
  export declare function textSummary(r: RunResult, opts?: {
11
10
  survivors?: number;
12
11
  }): string;
package/dist/reports.js CHANGED
@@ -116,6 +116,16 @@ function frozenLine(r) {
116
116
  const sign = f.delta >= 0 ? '+' : '';
117
117
  return [` frozen oracle vs ${f.base}: ${f.headScore.toFixed(2)}% with the change's tests, ${f.baseScore.toFixed(2)}% with the base's (${sign}${f.delta.toFixed(2)}), ${String(f.weakened.length)} mutant(s) the change stopped detecting`];
118
118
  }
119
+ function whereItWent(s) {
120
+ const lines = [];
121
+ if (s.warmUnitMs > 0)
122
+ lines.push(` warm servers ran ${(s.warmUnitMs / MS_PER_S).toFixed(1)} s, ${(s.warmArmedMs / MS_PER_S).toFixed(1)} s of it inside test runs`);
123
+ const c = s.isolatedByCause;
124
+ const causes = [`static ${String(c.static)}`, `stopped ${String(c.stopped)}`, `unfinished ${String(c.unfinished)}`, `re-run ${String(c.rerun)}`, `survivors confirmed ${String(c.confirm)}`, `flaky ${String(c.flaky)}`];
125
+ if (s.isolatedRuns > 0)
126
+ lines.push(` isolated runs: ${causes.join(' · ')}`);
127
+ return lines;
128
+ }
119
129
  export function textSummary(r, opts = {}) {
120
130
  const count = (s) => r.mutants.filter((m) => m.status === s).length;
121
131
  const s = r.stats;
@@ -132,6 +142,7 @@ export function textSummary(r, opts = {}) {
132
142
  const corrected = corrections(s);
133
143
  if (corrected.length)
134
144
  lines.push(` corrected: ${corrected.join(' · ')}`);
145
+ lines.push(...whereItWent(s));
135
146
  lines.push(...frozenLine(r));
136
147
  lines.push(...specLines(r));
137
148
  const survivors = r.mutants.filter((m) => m.status === 'Survived' || m.status === 'NoCoverage');
package/dist/runner.d.ts CHANGED
@@ -95,6 +95,6 @@ export default class OxmutateRunner extends OxmutateRunner_base {
95
95
  private collectCopies;
96
96
  /** Reset per-try state (snapshot counters, assertion counts) the way Vitest does for a retry. */
97
97
  private retryReset;
98
- onAfterRunFiles(files: AnyTask[]): unknown;
98
+ onAfterRunFiles(files: AnyTask[]): Promise<unknown>;
99
99
  }
100
100
  export {};
package/dist/runner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { closeSync, openSync, readFileSync, writeSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { availableParallelism, loadavg } from 'node:os';
4
4
  import path from 'node:path';
@@ -53,6 +53,11 @@ if (mode === 'isolated' && env['OXMUTATE_HIT_LIMIT']) {
53
53
  ns.hitLimit = Number(env['OXMUTATE_HIT_LIMIT']);
54
54
  }
55
55
  const ranAway = () => ns.hitCount !== undefined && ns.hitLimit !== undefined && ns.hitCount > ns.hitLimit;
56
+ let unhandled = 0;
57
+ process.on('unhandledRejection', () => void unhandled++);
58
+ process.on('uncaughtException', () => void unhandled++);
59
+ const realSetImmediate = globalThis.setImmediate;
60
+ const settle = () => new Promise((resolve) => realSetImmediate(resolve));
56
61
  const deadlocks = mode === 'dry' ? undefined : watch();
57
62
  let deadlocked = false;
58
63
  let tornDown = false;
@@ -122,7 +127,7 @@ function takeCopy(test) {
122
127
  }
123
128
  const dryModules = new Map();
124
129
  const PROBE_ID = '__oxmutate_probe__';
125
- async function probe(test, fn) {
130
+ async function probe(test, fn, reset) {
126
131
  const file = test.file?.filepath;
127
132
  const scope = test.suite?.id ?? file;
128
133
  if (!file || !scope || probed.has(scope) || runOnce.has(file))
@@ -130,6 +135,7 @@ async function probe(test, fn) {
130
135
  probed.add(scope);
131
136
  await afterEach(test, test.context, []);
132
137
  const cleanups = await beforeEach(test, test.context);
138
+ await reset();
133
139
  ns.currentTestId = PROBE_ID;
134
140
  try {
135
141
  await fn();
@@ -147,9 +153,31 @@ async function probe(test, fn) {
147
153
  }
148
154
  const POLLUTION_ERROR_CHARS = 300;
149
155
  const FILE_ERROR_CHARS = 500;
156
+ let resultsFd;
157
+ let progressFd;
158
+ let markLength = 0;
150
159
  function emit(record) {
151
- if (resultsPath)
152
- appendFileSync(resultsPath, `${JSON.stringify(record)}\n`);
160
+ if (!resultsPath)
161
+ return;
162
+ resultsFd ??= openSync(resultsPath, 'a');
163
+ writeSync(resultsFd, `${JSON.stringify(record)}\n`);
164
+ }
165
+ function mark(text) {
166
+ if (!progressPath)
167
+ return;
168
+ progressFd ??= openSync(progressPath, 'w');
169
+ const padded = text.trimEnd().padEnd(markLength);
170
+ markLength = padded.length;
171
+ if (padded)
172
+ writeSync(progressFd, padded, 0);
173
+ }
174
+ function closeFiles() {
175
+ for (const fd of [resultsFd, progressFd])
176
+ if (fd !== undefined)
177
+ closeSync(fd);
178
+ resultsFd = undefined;
179
+ progressFd = undefined;
180
+ markLength = 0;
153
181
  }
154
182
  function chain(test) {
155
183
  const suites = [];
@@ -271,7 +299,7 @@ export default class OxmutateRunner extends Base {
271
299
  try {
272
300
  await guarded(test.id, fn);
273
301
  if (mode === 'dry')
274
- await probe(test, fn);
302
+ await probe(test, fn, () => this.retryReset(test, 1));
275
303
  }
276
304
  catch (e) {
277
305
  if (isDeadlock(e))
@@ -359,9 +387,13 @@ export default class OxmutateRunner extends Base {
359
387
  ns.hitLimit = plan.hitLimits[id] ?? Number.MAX_SAFE_INTEGER;
360
388
  let status = 'Survived';
361
389
  let deadlock = false;
390
+ const before = unhandled;
362
391
  try {
363
392
  await setup();
364
393
  await guarded(current.id, run);
394
+ await settle();
395
+ if (unhandled > before)
396
+ status = 'Killed';
365
397
  }
366
398
  catch (e) {
367
399
  deadlock = isDeadlock(e);
@@ -377,8 +409,7 @@ export default class OxmutateRunner extends Base {
377
409
  for (const id of ids) {
378
410
  if (done.has(id))
379
411
  continue;
380
- if (progressPath)
381
- writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
412
+ mark(`${id} ${String(realNow())}\n`);
382
413
  let { status, ms, deadlock } = await runArmed(id);
383
414
  if (status === 'Polluted')
384
415
  break;
@@ -402,14 +433,13 @@ export default class OxmutateRunner extends Base {
402
433
  break;
403
434
  }
404
435
  if (tornDown) {
405
- if (progressPath)
406
- writeFileSync(progressPath, '');
436
+ mark('');
407
437
  return;
408
438
  }
409
439
  if (!polluted)
410
440
  await canary();
411
- if (progressPath && ids.length)
412
- writeFileSync(progressPath, '');
441
+ if (ids.length)
442
+ mark('');
413
443
  if (attempt > 0)
414
444
  await fresh();
415
445
  try {
@@ -474,10 +504,14 @@ export default class OxmutateRunner extends Base {
474
504
  ns.hitCount = armed ? 0 : undefined;
475
505
  ns.hitLimit = armed ? (staticLimits[armed] ?? Number.MAX_SAFE_INTEGER) : undefined;
476
506
  deadlocked = false;
507
+ const before = unhandled;
477
508
  try {
478
509
  vi.resetModules();
479
510
  const [file] = await collectTests([filepath], this);
480
511
  const result = file ? await runCollected(file, runnable) : { failed: [filepath], ran: 0 };
512
+ await settle();
513
+ if (unhandled > before && result.failed.length === 0)
514
+ result.failed.push(filepath);
481
515
  return { ...result, timeout: ranAway() || deadlocked, ...(deadlocked ? { deadlock: true } : {}) };
482
516
  }
483
517
  finally {
@@ -495,8 +529,7 @@ export default class OxmutateRunner extends Base {
495
529
  };
496
530
  while (rest.length) {
497
531
  const id = rest.shift();
498
- if (progressPath)
499
- writeFileSync(progressPath, `${id} ${String(realNow())}\n`);
532
+ mark(`${id} ${String(realNow())}\n`);
500
533
  held.push({ id, verdict: await evaluate(id) });
501
534
  if (held.at(-1)?.verdict.deadlock)
502
535
  return back('torn down: a deadlocked run is still pending in this worker');
@@ -517,8 +550,7 @@ export default class OxmutateRunner extends Base {
517
550
  held = [];
518
551
  every = Math.min(STATIC_CANARY_MAX, every * 2);
519
552
  }
520
- if (progressPath)
521
- writeFileSync(progressPath, '');
553
+ mark('');
522
554
  vi.resetModules();
523
555
  }
524
556
  async onBeforeRunTask(test) {
@@ -562,7 +594,8 @@ export default class OxmutateRunner extends Base {
562
594
  async retryReset(test, attempt) {
563
595
  await super.onBeforeTryTask?.(test, { retry: attempt, repeats: 0 });
564
596
  }
565
- onAfterRunFiles(files) {
597
+ async onAfterRunFiles(files) {
598
+ await settle();
566
599
  if (mode === 'dry') {
567
600
  const tests = [];
568
601
  const visit = (t, file) => {
@@ -579,7 +612,7 @@ export default class OxmutateRunner extends Base {
579
612
  if (result?.state === 'fail' && !f.tasks?.length)
580
613
  fileErrors.push({ file: filepath, error: (result.errors?.[0]?.message ?? 'failed to collect').slice(0, FILE_ERROR_CHARS) });
581
614
  }
582
- emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules) });
615
+ emit({ type: 'dry', tests, coverage: ns.mutantCoverage, fileErrors, runOnce: [...runOnce], modules: Object.fromEntries(dryModules), unhandled });
583
616
  }
584
617
  else if (mode === 'isolated') {
585
618
  const failed = [];
@@ -597,9 +630,13 @@ export default class OxmutateRunner extends Base {
597
630
  if (f.result?.state === 'fail' && !failed.length)
598
631
  failed.push(f.id);
599
632
  }
633
+ const [first] = files;
634
+ if (unhandled > 0 && !failed.length && first)
635
+ failed.push(first.id);
600
636
  const timeout = ranAway() || deadlocked;
601
637
  emit({ type: 'isolated', id: env[ACTIVE_MUTANT_ENV] ?? ns.activeMutant, killed: failed.length > 0 && !timeout, failed, ran, ...(timeout && { timeout }), ...(deadlocked && { deadlock: true }) });
602
638
  }
639
+ closeFiles();
603
640
  return super.onAfterRunFiles?.(files);
604
641
  }
605
642
  }
package/dist/scheduler.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { fork } from 'node:child_process';
2
- import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { beatFile, classifyStall, CPU_WINDOW_MS, cpuShare, readBeat } from './heartbeat.js';
@@ -8,6 +8,7 @@ const childJs = fileURLToPath(new URL('./child.js', import.meta.url));
8
8
  const STDERR_TAIL = 4000;
9
9
  const WATCHDOG_POLL_MS = 50;
10
10
  const CLOSE_GRACE_MS = 2000;
11
+ const PROGRESS_BYTES = 256;
11
12
  function inFlight(progress) {
12
13
  try {
13
14
  return readFileSync(progress, 'utf8').trim().split(' ')[0] || undefined;
@@ -56,7 +57,9 @@ class Server {
56
57
  });
57
58
  }
58
59
  run(unit, budget) {
59
- writeFileSync(unit.progress, '');
60
+ const progress = openSync(unit.progress, 'w+');
61
+ const mark = Buffer.alloc(PROGRESS_BYTES);
62
+ const sent = performance.now();
60
63
  return new Promise((resolve) => {
61
64
  let settled = false;
62
65
  const finish = (o) => {
@@ -64,18 +67,13 @@ class Server {
64
67
  return;
65
68
  settled = true;
66
69
  clearInterval(timer);
70
+ closeSync(progress);
67
71
  this.onMessage = undefined;
68
72
  this.onExit = undefined;
69
- resolve(o);
73
+ resolve({ ...o, ms: performance.now() - sent });
70
74
  };
71
75
  const timer = setInterval(() => {
72
- let line = '';
73
- try {
74
- line = readFileSync(unit.progress, 'utf8').trim();
75
- }
76
- catch {
77
- return;
78
- }
76
+ const line = mark.toString('utf8', 0, readSync(progress, mark, 0, PROGRESS_BYTES, 0)).trim();
79
77
  if (!line)
80
78
  return;
81
79
  const [id = '', since = '0'] = line.split(' ');
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "oxmutate",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
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
+ "author": "Ofri Peretz <ofriperetzdev@gmail.com>",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/ofri-peretz/burgee.git",
@@ -14,8 +15,13 @@
14
15
  "homepage": "https://oxmutate.interlace.tools",
15
16
  "type": "module",
16
17
  "private": false,
18
+ "sideEffects": [
19
+ "./dist/bin.js",
20
+ "./dist/child.js",
21
+ "./dist/runner.js"
22
+ ],
17
23
  "engines": {
18
- "node": "^20.19.0 || >=22.13.0"
24
+ "node": ">=22.13.0"
19
25
  },
20
26
  "bin": {
21
27
  "oxmutate": "./dist/bin.js"
@@ -57,6 +63,24 @@
57
63
  ]
58
64
  }
59
65
  },
66
+ "keywords": [
67
+ "mutation-testing",
68
+ "mutation",
69
+ "mutation-score",
70
+ "stryker",
71
+ "vitest",
72
+ "oxc",
73
+ "test-quality",
74
+ "sarif",
75
+ "typescript",
76
+ "javascript",
77
+ "ci",
78
+ "cli"
79
+ ],
80
+ "publishConfig": {
81
+ "access": "public",
82
+ "provenance": true
83
+ },
60
84
  "files": [
61
85
  "dist",
62
86
  "binding/oxmutate.*.node",
@@ -75,8 +99,8 @@
75
99
  "compat:mutation": "vitest run --config compat/vitest.config.ts"
76
100
  },
77
101
  "dependencies": {
78
- "bellpull": "^1.0.10",
79
- "burgee": "^1.9.2"
102
+ "bellpull": "^2.0.0",
103
+ "burgee": "^2.0.0"
80
104
  },
81
105
  "peerDependencies": {
82
106
  "vitest": ">=4.1.0"