testguard-cli 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-09-17
11
+
12
+ ### Added
13
+
14
+ - **jest runner.** `--runner vitest|jest|auto` (default `auto`: the first
15
+ that resolves in the project). jest runs with `--ci --json --runTestsByPath`
16
+ so file arguments are exact paths, not regexes; its `Exceeded timeout`
17
+ wording is recognised as a timeout, never a kill; `__tests__/` files are
18
+ collected for escalation. `run.runner.name` records which runner produced
19
+ the evidence.
20
+ - A CommonJS edition of the known-answer fixture (`fixtures/known-answer-jest`)
21
+ with the same claims and the same expected verdicts; the acceptance suite
22
+ and CI prove jest against it exactly as vitest is proven against the ESM
23
+ one.
24
+
25
+ ### Changed
26
+
27
+ - Runners are now modules behind one interface (`src/probe/runners/`); the
28
+ vitest path is unchanged and still reproduces its oracle.
29
+
10
30
  ## [0.3.1] - 2026-09-17
11
31
 
12
32
  Dogfooding `init` and `status` on this repository.
package/README.md CHANGED
@@ -52,7 +52,7 @@ tests were written against the survivors, 39/39 were killed.
52
52
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
53
53
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
54
54
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
55
- | GitHub Action | `uses: raccioly/testguard@v0.3.1` — see [`action.yml`](./action.yml) |
55
+ | GitHub Action | `uses: raccioly/testguard@v0.4.0` — see [`action.yml`](./action.yml) |
56
56
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
57
57
 
58
58
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
@@ -111,6 +111,11 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
111
111
  files that import the fault's target, by relative path or resolved alias.
112
112
  `NOCOVER` then means exactly "no test file imports this source".
113
113
 
114
+ Runners: **vitest** and **jest** (`--runner auto` picks the first that
115
+ resolves; both read the same jest-compatible JSON report). Anything else
116
+ goes through `--runner-cmd`. Each runner is proven against its own copy
117
+ of the known-answer fixture.
118
+
114
119
  Practical loop: first pass `--no-escalate` (escalation re-runs the whole
115
120
  suite N times per survivor); iterate on one claim with `--claim <ID>` and
116
121
  either `--include-dirty` or `--in-place` (only fault target files must be
@@ -215,14 +220,14 @@ through every verdict.
215
220
 
216
221
  ## Status
217
222
 
218
- **v0.3.** Seven commands, vitest runner, hand-authored faults plus a
219
- mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
223
+ **v0.4.** Seven commands, vitest and jest runners, hand-authored faults plus
224
+ a mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
220
225
  spine — six JSON Schemas shared with the other Guard tools — is under
221
226
  [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
222
227
 
223
- Not yet: test generation (the two-gate acceptance loop), other runners,
224
- AST-aware producers, and calibration of fault classes against real escaped
225
- bugs. Each is designed for; none is claimed.
228
+ Not yet: test generation (the two-gate acceptance loop), runners beyond
229
+ vitest and jest, AST-aware producers, and calibration of fault classes
230
+ against real escaped bugs. Each is designed for; none is claimed.
226
231
 
227
232
  ## Licence
228
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testguard-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Proves a test suite defends the claims a project makes: injects the faults those claims forbid and reports every one the tests fail to detect.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,7 @@
46
46
  "quality-gate"
47
47
  ],
48
48
  "devDependencies": {
49
+ "jest": "^30.5.1",
49
50
  "vitest": "^5.0.1"
50
51
  },
51
52
  "bin": {
package/src/cli.mjs CHANGED
@@ -38,6 +38,7 @@ probe
38
38
  --include-dirty probe the working tree (a snapshot commit) instead of HEAD; uncommitted tests count
39
39
  --verbose also print each killed fault (default: only unproven ones, plus a count)
40
40
  --confirm below 3 is PROVISIONAL: verdicts print with "?", evidence goes to .testguard/evidence-provisional.json
41
+ --runner <name> vitest | jest | auto (default: auto — first of vitest, jest that resolves)
41
42
  --runner-cmd "<cmd>" custom runner; must contain {files} and {out}, e.g. "pnpm vitest run {files} --reporter=json --outputFile={out}"
42
43
  --node-modules <dir> node_modules to link into the scratch worktree (or TESTGUARD_NODE_MODULES)
43
44
  --in-place mutate the working tree instead of a scratch worktree
@@ -82,6 +83,7 @@ export async function main(argv, io = { out: (s) => process.stdout.write(s + '\n
82
83
  force: { type: 'boolean', default: false },
83
84
  verbose: { type: 'boolean', default: false },
84
85
  'runner-cmd': { type: 'string' },
86
+ runner: { type: 'string', default: 'auto' },
85
87
  'node-modules': { type: 'string' },
86
88
  'in-place': { type: 'boolean', default: false },
87
89
  'no-escalate': { type: 'boolean', default: false },
@@ -114,6 +116,10 @@ export async function main(argv, io = { out: (s) => process.stdout.write(s + '\n
114
116
  io.err(USAGE);
115
117
  return 3;
116
118
  }
119
+ if (!['vitest', 'jest', 'auto'].includes(values.runner)) {
120
+ io.err('--runner must be vitest, jest or auto');
121
+ return 3;
122
+ }
117
123
  if (!['critical', 'high', 'medium', 'low'].includes(values.severity)) {
118
124
  io.err('--severity must be one of critical, high, medium, low');
119
125
  return 3;
@@ -1,7 +1,7 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { loadClaims, defaultClaimsPath } from '../claims/load.mjs';
3
3
  import { scanAnnotations, reconcile } from '../claims/annotations.mjs';
4
- import { resolveDefenders } from '../probe/runner-vitest.mjs';
4
+ import { resolveDefenders } from '../probe/runners/shared.mjs';
5
5
  import { discoverDefenders } from '../probe/discover.mjs';
6
6
 
7
7
  export async function claimsCommand({ projectDir, values }, io) {
@@ -42,6 +42,7 @@ export async function probeCommand({ projectDir, values, version }, io) {
42
42
  mode: values['in-place'] ? 'in-place' : 'worktree',
43
43
  ref: values.ref,
44
44
  runnerCommand: values['runner-cmd'],
45
+ runnerName: values.runner,
45
46
  nodeModules: values['node-modules'] ? resolve(values['node-modules']) : process.env.TESTGUARD_NODE_MODULES,
46
47
  only,
47
48
  escalate: !values['no-escalate'],
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { listTestFiles } from './runner-vitest.mjs';
2
+ import { listTestFiles } from './runners/shared.mjs';
3
3
  import { fileImports } from './rank.mjs';
4
4
 
5
5
  /**
@@ -6,7 +6,8 @@ import { repoRoot as gitRoot, headSha, isDirty, snapshotWorkingTree } from '../g
6
6
  import { discoverDefenders } from './discover.mjs';
7
7
  import { createScratch, inPlace, PreconditionError } from './worktree.mjs';
8
8
  import { applyFault, locate } from './inject.mjs';
9
- import * as vitest from './runner-vitest.mjs';
9
+ import { resolveDefenders, parseCommandTemplate } from './runners/shared.mjs';
10
+ import { selectRunner, RUNNERS } from './runners/index.mjs';
10
11
  import { classify, shouldStopEarly } from './classify.mjs';
11
12
  import { blastRadius, rank } from './rank.mjs';
12
13
  import { hashFile, sha256 } from '../util/hash.mjs';
@@ -14,9 +15,9 @@ import { fingerprint } from '../../spec/lib/fingerprint.mjs';
14
15
 
15
16
  const isKill = (r) => r.outcome === 'fail' && r.assertionFailures > 0;
16
17
 
17
- function readRunnerVersion(projectDir) {
18
+ function readRunnerVersion(projectDir, name = 'vitest') {
18
19
  try {
19
- return JSON.parse(readFileSync(createRequire(join(projectDir, 'noop.js')).resolve('vitest/package.json'), 'utf8')).version;
20
+ return JSON.parse(readFileSync(createRequire(join(projectDir, 'noop.js')).resolve(`${name}/package.json`), 'utf8')).version;
20
21
  } catch {
21
22
  return undefined;
22
23
  }
@@ -34,6 +35,7 @@ export async function probe({
34
35
  ref = 'HEAD',
35
36
  budgetMs = 120_000,
36
37
  runnerCommand,
38
+ runnerName = 'auto',
37
39
  nodeModules,
38
40
  only,
39
41
  includeDirty = false,
@@ -75,7 +77,7 @@ export async function probe({
75
77
  const watched = new Set(targets);
76
78
  for (const claim of claims.claims) {
77
79
  if (selected && !selected.has(claim.id)) continue;
78
- const declared = claim.defendedBy?.length ? vitest.resolveDefenders(projectDir, claim.defendedBy) : claim.faults.flatMap((f) => discoverDefenders(projectDir, f.file));
80
+ const declared = claim.defendedBy?.length ? resolveDefenders(projectDir, claim.defendedBy) : claim.faults.flatMap((f) => discoverDefenders(projectDir, f.file));
79
81
  for (const d of declared) watched.add(relative(root, join(projectDir, d)));
80
82
  for (const g of claim.defendedBy ?? []) if (!g.includes('*')) watched.add(relative(root, join(projectDir, g)));
81
83
  }
@@ -89,27 +91,29 @@ export async function probe({
89
91
  const iso = mode === 'worktree' ? createScratch({ repoRoot: root, projectDir, ref: snapshot ?? ref, scratchBase, nodeModules }) : inPlace({ repoRoot: root, projectDir });
90
92
  const records = [];
91
93
  let runnerVersion;
94
+ let runner = RUNNERS[runnerName === 'auto' ? 'vitest' : runnerName];
92
95
  try {
93
- const commandTemplate = runnerCommand ? vitest.parseCommandTemplate(runnerCommand) : undefined;
96
+ const commandTemplate = runnerCommand ? parseCommandTemplate(runnerCommand) : undefined;
94
97
  if (!commandTemplate) {
95
98
  // An unresolvable runner is a precondition failure, not a flaky defender.
96
- const check = await vitest.checkRunner({ projectDir: iso.projectDir });
97
- if (!check.ok) {
98
- throw new PreconditionError(`test runner is not resolvable in the ${mode === 'worktree' ? 'scratch worktree' : 'project'} (${check.message}). ` +
99
+ const sel = await selectRunner({ projectDir: iso.projectDir, name: runnerName });
100
+ if (sel.error) {
101
+ throw new PreconditionError(`test runner is not resolvable in the ${mode === 'worktree' ? 'scratch worktree' : 'project'} (${sel.error}). ` +
99
102
  (mode === 'worktree' ? 'No usable node_modules was linked: pass --node-modules <path>, or run with --in-place.' : 'Install dependencies first.'));
100
103
  }
101
- runnerVersion = check.version;
104
+ runner = sel.runner;
105
+ runnerVersion = sel.version;
102
106
  }
103
- const allTests = vitest.listTestFiles(iso.projectDir);
107
+ const allTests = runner.tests(iso.projectDir);
104
108
  const baselineCache = new Map();
105
109
  const prior = previous && previous.run.confirmRuns === confirmRuns
106
110
  ? new Map(previous.records.map((r) => [`${r.claim.id}/${r.subject.id}`, r]))
107
111
  : new Map();
108
- const runDefenders = (files) => vitest.runVitest({ projectDir: iso.projectDir, files, budgetMs, commandTemplate });
112
+ const runDefenders = (files) => runner.run({ projectDir: iso.projectDir, files, budgetMs, commandTemplate });
109
113
 
110
114
  for (const claim of claims.claims) {
111
115
  if (selected && !selected.has(claim.id)) continue;
112
- const declared = claim.defendedBy?.length ? vitest.resolveDefenders(iso.projectDir, claim.defendedBy) : null;
116
+ const declared = claim.defendedBy?.length ? resolveDefenders(iso.projectDir, claim.defendedBy) : null;
113
117
  for (const fault of claim.faults) {
114
118
  const defenders = declared ?? discoverDefenders(iso.projectDir, fault.file);
115
119
  const stage = (name, i, n) => onStage({ claimId: claim.id, faultId: fault.id, stage: name, i, n });
@@ -130,7 +134,7 @@ export async function probe({
130
134
  startedAt,
131
135
  finishedAt: new Date().toISOString(),
132
136
  repo: { head, dirty: isDirty(root), ...(snapshot ? { snapshot } : {}) },
133
- runner: { name: vitest.name, ...((runnerVersion ?? readRunnerVersion(projectDir)) ? { version: runnerVersion ?? readRunnerVersion(projectDir) } : {}) },
137
+ runner: { name: runner.name, ...((runnerVersion ?? readRunnerVersion(projectDir, runner.name)) ? { version: runnerVersion ?? readRunnerVersion(projectDir, runner.name) } : {}) },
134
138
  confirmRuns,
135
139
  ...(confirmRuns < 3 ? { provisional: true } : {}),
136
140
  mode,
@@ -0,0 +1,20 @@
1
+ import * as vitest from './vitest.mjs';
2
+ import * as jest from './jest.mjs';
3
+
4
+ export const RUNNERS = { vitest, jest };
5
+
6
+ /**
7
+ * Pick the runner: an explicit name, or the first of vitest, jest that is
8
+ * resolvable from the project. Returns { runner, version } or { error }.
9
+ */
10
+ export async function selectRunner({ projectDir, name = 'auto' }) {
11
+ const candidates = name === 'auto' ? [vitest, jest] : [RUNNERS[name]];
12
+ if (!candidates[0]) return { error: `unknown runner "${name}"; use vitest, jest or auto` };
13
+ const messages = [];
14
+ for (const r of candidates) {
15
+ const c = await r.check({ projectDir });
16
+ if (c.ok) return { runner: r, version: c.version };
17
+ messages.push(`${r.name}: ${c.message}`);
18
+ }
19
+ return { error: messages.join('; ') };
20
+ }
@@ -0,0 +1,9 @@
1
+ import { TEST_GLOBS, npx, runProcess, checkBinary, listTestFiles } from './shared.mjs';
2
+
3
+ export const name = 'jest';
4
+ // jest's default testMatch also collects anything under __tests__/.
5
+ export const testGlobs = [...TEST_GLOBS, '**/__tests__/**/*.js', '**/__tests__/**/*.mjs', '**/__tests__/**/*.cjs', '**/__tests__/**/*.ts', '**/__tests__/**/*.tsx', '**/__tests__/**/*.jsx'];
6
+ export const check = (opts) => checkBinary({ ...opts, bin: 'jest' });
7
+ export const tests = (projectDir) => listTestFiles(projectDir, testGlobs);
8
+ // --runTestsByPath: positionals are exact paths, not regexes — a path with `+` or `(` would otherwise silently match nothing.
9
+ export const run = (opts) => runProcess({ ...opts, argv: (files, outFile) => [npx, 'jest', '--ci', '--json', `--outputFile=${outFile}`, '--runTestsByPath', ...files] });
@@ -3,12 +3,11 @@ import { existsSync, readFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { randomBytes } from 'node:crypto';
6
- import { matchGlobs } from '../util/glob.mjs';
6
+ import { matchGlobs } from '../../util/glob.mjs';
7
7
 
8
- const TEST_GLOBS = ['**/*.test.js', '**/*.test.mjs', '**/*.test.cjs', '**/*.test.ts', '**/*.test.mts', '**/*.test.tsx', '**/*.test.jsx',
8
+ export const TEST_GLOBS = ['**/*.test.js', '**/*.test.mjs', '**/*.test.cjs', '**/*.test.ts', '**/*.test.mts', '**/*.test.tsx', '**/*.test.jsx',
9
9
  '**/*.spec.js', '**/*.spec.mjs', '**/*.spec.cjs', '**/*.spec.ts', '**/*.spec.mts', '**/*.spec.tsx', '**/*.spec.jsx'];
10
-
11
- export const name = 'vitest';
10
+ export const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
12
11
 
13
12
  /**
14
13
  * Split a runner command template into argv. Supports double and single
@@ -45,14 +44,13 @@ export function parseCommandTemplate(template) {
45
44
  export const expandCommand = (words, files, outFile) => words.flatMap((w) => (w === '{files}' ? files : [w.replaceAll('{out}', outFile)]));
46
45
 
47
46
  /**
48
- * Can the runner be resolved at all from this directory? Run before any
49
- * verdict is attempted: an unresolvable runner is a precondition failure,
50
- * never a statement about the tests.
47
+ * Can `bin` be resolved at all from this directory? Run before any verdict
48
+ * is attempted: an unresolvable runner is a precondition failure, never a
49
+ * statement about the tests.
51
50
  */
52
- export function checkRunner({ projectDir, budgetMs = 60_000, env = process.env }) {
53
- const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
51
+ export function checkBinary({ projectDir, bin, budgetMs = 60_000, env = process.env }) {
54
52
  return new Promise((resolve) => {
55
- const child = spawn(npx, ['--no-install', 'vitest', '--version'], { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...env, CI: '1' } });
53
+ const child = spawn(npx, ['--no-install', bin, '--version'], { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...env, CI: '1' } });
56
54
  let out = '';
57
55
  let err = '';
58
56
  child.on('error', (e) => resolve({ ok: false, message: e.message }));
@@ -61,7 +59,7 @@ export function checkRunner({ projectDir, budgetMs = 60_000, env = process.env }
61
59
  const timer = setTimeout(() => child.kill('SIGKILL'), budgetMs);
62
60
  child.on('close', (code) => {
63
61
  clearTimeout(timer);
64
- const version = out.trim().replace(/^vitest\//, '');
62
+ const version = out.trim().replace(/^vitest\//, '').split('\n').pop();
65
63
  resolve(code === 0 && version ? { ok: true, version } : { ok: false, message: (err || out).trim().split('\n').filter(Boolean).slice(-1)[0] ?? `exit ${code}` });
66
64
  });
67
65
  });
@@ -70,7 +68,7 @@ export function checkRunner({ projectDir, budgetMs = 60_000, env = process.env }
70
68
  /** Defender globs → existing files. Empty result is the `nocover` signal. */
71
69
  export const resolveDefenders = (projectDir, globs) => matchGlobs(projectDir, globs ?? []);
72
70
 
73
- export const listTestFiles = (projectDir) => matchGlobs(projectDir, TEST_GLOBS);
71
+ export const listTestFiles = (projectDir, globs = TEST_GLOBS) => matchGlobs(projectDir, globs);
74
72
 
75
73
  /**
76
74
  * Turn vitest's JSON report into a spec `testRun`.
@@ -80,11 +78,16 @@ export const listTestFiles = (projectDir) => matchGlobs(projectDir, TEST_GLOBS);
80
78
  * can kill a fault. A suite that fails to load, or a test that times out, is
81
79
  * not evidence that the suite defends the claim.
82
80
  */
81
+ function firstInformativeLine(message) {
82
+ const lines = message.split('\n').map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').trim()).filter(Boolean);
83
+ return lines.find((l) => /error|syntax|unexpected|expected .+ but found|cannot find|failed to parse|transform failed/i.test(l) && !/^●/.test(l)) ?? lines[0] ?? '';
84
+ }
85
+
83
86
  export function parseReport(report, durationMs) {
84
87
  const files = report.testResults ?? [];
85
88
  const loadFailed = files.some((f) => f.status === 'failed' && (f.assertionResults?.length ?? 0) === 0);
86
89
  const results = files.flatMap((f) => (f.assertionResults ?? []).map((t) => ({ ...t, id: `${f.name}::${t.fullName ?? t.title ?? ''}` }))).filter((t) => t.status === 'failed');
87
- const timeouts = results.filter((t) => (t.failureMessages ?? []).some((m) => /timed out/i.test(m))).length;
90
+ const timeouts = results.filter((t) => (t.failureMessages ?? []).some((m) => /timed out|exceeded timeout/i.test(m))).length;
88
91
  const failedTests = results.map((t) => t.id);
89
92
  const assertionFailures = results.length - timeouts;
90
93
  const tests = { total: report.numTotalTests ?? 0, passed: report.numPassedTests ?? 0, failed: report.numFailedTests ?? 0 };
@@ -95,20 +98,25 @@ export function parseReport(report, durationMs) {
95
98
  else outcome = 'fail';
96
99
 
97
100
  const run = { outcome, tests, assertionFailures, durationMs };
98
- const loadMessage = loadFailed ? (files.find((f) => f.message)?.message ?? '').split('\n')[0] : undefined;
101
+ // jest opens a load failure with "● Test suite failed to run" and puts the
102
+ // cause lines below; vitest puts it on the first line. Take the first line
103
+ // that names an error, else the first non-empty one.
104
+ const loadMessage = loadFailed ? firstInformativeLine(files.find((f) => f.message)?.message ?? '') : undefined;
99
105
  return { run, timeouts, loadMessage, failedTests };
100
106
  }
101
107
 
102
108
  /**
103
- * Run vitest on `files` inside `projectDir` with a hard wall-clock budget.
104
- * The budget matters because a synchronous infinite loop is immune to
105
- * vitest's own test timeout; only killing the process ends it.
109
+ * Run a test process on `files` inside `projectDir` with a hard wall-clock
110
+ * budget and read back its jest-compatible JSON report. The budget matters
111
+ * because a synchronous infinite loop is immune to any runner's own test
112
+ * timeout; only killing the process ends it. `argv(files, outFile)` is the
113
+ * runner's command line; `command` (tests) or `commandTemplate` (--runner-cmd)
114
+ * override it.
106
115
  */
107
- export function runVitest({ projectDir, files, budgetMs = 120_000, command, commandTemplate }) {
108
- const outFile = join(tmpdir(), `testguard-vitest-${randomBytes(6).toString('hex')}.json`);
109
- const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
116
+ export function runProcess({ projectDir, files, budgetMs = 120_000, command, commandTemplate, argv }) {
117
+ const outFile = join(tmpdir(), `testguard-run-${randomBytes(6).toString('hex')}.json`);
110
118
  const [cmd, ...args] = command
111
- ?? (commandTemplate ? expandCommand(commandTemplate, files, outFile) : [npx, 'vitest', 'run', ...files, '--reporter=json', `--outputFile=${outFile}`]);
119
+ ?? (commandTemplate ? expandCommand(commandTemplate, files, outFile) : argv(files, outFile));
112
120
  const started = Date.now();
113
121
 
114
122
  return new Promise((resolve) => {
@@ -0,0 +1,7 @@
1
+ import { TEST_GLOBS, npx, runProcess, checkBinary, listTestFiles } from './shared.mjs';
2
+
3
+ export const name = 'vitest';
4
+ export const testGlobs = TEST_GLOBS;
5
+ export const check = (opts) => checkBinary({ ...opts, bin: 'vitest' });
6
+ export const tests = (projectDir) => listTestFiles(projectDir, testGlobs);
7
+ export const run = (opts) => runProcess({ ...opts, argv: (files, outFile) => [npx, 'vitest', 'run', ...files, '--reporter=json', `--outputFile=${outFile}`] });
@@ -4,7 +4,7 @@ import { readSpecDoc } from '../evidence/writer.mjs';
4
4
  import { loadClaims, defaultClaimsPath } from '../claims/load.mjs';
5
5
  import { gate } from '../baseline/baseline.mjs';
6
6
  import { hashFile, sha256 } from '../util/hash.mjs';
7
- import { resolveDefenders } from '../probe/runner-vitest.mjs';
7
+ import { resolveDefenders } from '../probe/runners/shared.mjs';
8
8
  import { discoverDefenders } from '../probe/discover.mjs';
9
9
  import { sortForReport } from '../render.mjs';
10
10