testguard-cli 0.18.3 → 0.20.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.
@@ -2,7 +2,7 @@ import { resetInterpreterCache } from '../probe/runners/python.mjs';
2
2
  import { existsSync, rmSync, realpathSync } from 'node:fs';
3
3
  import { basename, dirname, join, relative, resolve } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
- import { git, GitError, repoRoot, headSha } from '../git.mjs';
5
+ import { git, GitError, NO_HOOKS, repoRoot, headSha } from '../git.mjs';
6
6
  import { createScratch, PreconditionError } from '../probe/worktree.mjs';
7
7
  import { selectRunner, discoverRunnerManifest, RUNNERS, OWNED_RUNNERS, mergeRuns } from '../probe/runners/index.mjs';
8
8
  import { parseCommandTemplate } from '../probe/runners/shared.mjs';
@@ -90,6 +90,7 @@ export async function replay({
90
90
  runnerCommand,
91
91
  runnerName = 'auto',
92
92
  nodeModules,
93
+ python,
93
94
  scratchBase,
94
95
  limit,
95
96
  toolVersion = '0.0.0',
@@ -123,7 +124,7 @@ export async function replay({
123
124
  let universe;
124
125
  if (!commandTemplate) {
125
126
  const budgetFor = () => commandBudget?.runBudget(budgetMs) ?? budgetMs;
126
- const sel = await selectRunner({ projectDir: iso.projectDir, sourceDir: projectDir, name: runnerName, budgetMs: budgetFor(), budgetFor });
127
+ const sel = await selectRunner({ projectDir: iso.projectDir, sourceDir: projectDir, python, name: runnerName, budgetMs: budgetFor(), budgetFor });
127
128
  commandBudget?.assertOpen();
128
129
  if (sel.error) throw new PreconditionError(`test runner is not resolvable in the scratch worktree (${sel.error}). Pass --node-modules <path>.`);
129
130
  runnerUsed = sel.runner;
@@ -135,7 +136,7 @@ export async function replay({
135
136
  const primaryIsPython = ['python', 'pytest', 'unittest'].includes(runnerUsed.name);
136
137
  const owned = OWNED_RUNNERS.filter((candidate) => candidate !== runnerUsed && !(candidate.name === 'python' && primaryIsPython));
137
138
  for (const candidate of owned) {
138
- const check = await candidate.check({ projectDir: iso.projectDir, sourceDir: projectDir, budgetMs: budgetFor(), budgetFor });
139
+ const check = await candidate.check({ projectDir: iso.projectDir, sourceDir: projectDir, python, budgetMs: budgetFor(), budgetFor });
139
140
  commandBudget?.assertOpen();
140
141
  if (!check.ok) continue;
141
142
  try {
@@ -146,7 +147,7 @@ export async function replay({
146
147
  }
147
148
  universe = buildReplayRunnerUniverse(runnerUsed, manifests);
148
149
  }
149
- const record = await replayOne({ fix, iso, root, projectDir, confirmRuns, budgetMs, commandBudget, workers, serial, commandTemplate, runner: runnerUsed, universe, onStage, index: i + 1, total: selected.length });
150
+ const record = await replayOne({ fix, iso, root, projectDir, python, confirmRuns, budgetMs, commandBudget, workers, serial, commandTemplate, runner: runnerUsed, universe, onStage, index: i + 1, total: selected.length });
150
151
  records.push(record);
151
152
  onProgress(record);
152
153
  } finally {
@@ -213,7 +214,7 @@ export function partitionReplayDefenders(files, primary, universe) {
213
214
  return groups;
214
215
  }
215
216
 
216
- async function replayOne({ fix, iso, root, projectDir, confirmRuns, budgetMs, commandBudget, workers, serial, commandTemplate, runner, universe, onStage, index, total }) {
217
+ async function replayOne({ fix, iso, root, projectDir, python, confirmRuns, budgetMs, commandBudget, workers, serial, commandTemplate, runner, universe, onStage, index, total }) {
217
218
  // git reports paths from the REPOSITORY root; the runner and every file
218
219
  // operation here work inside the scratch worktree, relative to the PROJECT
219
220
  // directory. Map once, and refuse anything that would land outside the
@@ -262,7 +263,7 @@ async function replayOne({ fix, iso, root, projectDir, confirmRuns, budgetMs, co
262
263
  return { ...base, faultClass, verdict: 'unverifiable', reason: 'no-prior-version', runs: [{ outcome: 'error', durationMs: 0 }] };
263
264
  }
264
265
  try {
265
- if (existedBefore.length) git(['checkout', `${fix.commit}^`, '--', ...existedBefore.map(rel)], iso.projectDir);
266
+ if (existedBefore.length) git([...NO_HOOKS, 'checkout', `${fix.commit}^`, '--', ...existedBefore.map(rel)], iso.projectDir);
266
267
  for (const f of addedByTheFix) {
267
268
  const abs = join(iso.projectDir, rel(f));
268
269
  if (existsSync(abs)) rmSync(inside(abs), { force: true });
@@ -329,12 +330,12 @@ async function replayOne({ fix, iso, root, projectDir, confirmRuns, budgetMs, co
329
330
  // A custom command is one explicit execution boundary. Preserve that
330
331
  // contract exactly; TestGuard cannot infer several native engines from
331
332
  // an opaque user command.
332
- res = await runner.run({ projectDir: iso.projectDir, sourceDir: projectDir, files: related, budgetMs: commandBudget?.runBudget(budgetMs) ?? budgetMs, commandTemplate, workers, serial });
333
+ res = await runner.run({ projectDir: iso.projectDir, sourceDir: projectDir, python, files: related, budgetMs: commandBudget?.runBudget(budgetMs) ?? budgetMs, commandTemplate, workers, serial });
333
334
  } else {
334
335
  const parts = [];
335
336
  for (const [configuredRunner, files] of partitionReplayDefenders(related, runner, universe)) {
336
337
  commandBudget?.assertOpen();
337
- parts.push(await configuredRunner.run({ projectDir: iso.projectDir, sourceDir: projectDir, files, budgetMs: commandBudget?.runBudget(budgetMs) ?? budgetMs, workers, serial }));
338
+ parts.push(await configuredRunner.run({ projectDir: iso.projectDir, sourceDir: projectDir, python, files, budgetMs: commandBudget?.runBudget(budgetMs) ?? budgetMs, workers, serial }));
338
339
  commandBudget?.assertOpen();
339
340
  }
340
341
  res = mergeRuns(parts);
@@ -27,9 +27,10 @@
27
27
  * look; the probe says what it found; a human states the sentence worth
28
28
  * defending. Nothing here is written to `testguard.claims.json`.
29
29
  */
30
- import { existsSync, readFileSync } from 'node:fs';
30
+ import { existsSync } from 'node:fs';
31
31
  import { join } from 'node:path';
32
32
  import { saveSurface } from './savepath.mjs';
33
+ import { readSpecDoc } from '../evidence/writer.mjs';
33
34
  import { globToRegExp, walk } from '../util/glob.mjs';
34
35
 
35
36
  export const defaultConcernsPath = (projectDir) => join(projectDir, 'testguard.concerns.json');
@@ -80,8 +81,13 @@ export function loadConcerns(projectDir, { path } = {}) {
80
81
  let project = [];
81
82
  let source = null;
82
83
  if (existsSync(p)) {
83
- const doc = JSON.parse(readFileSync(p, 'utf8'));
84
- project = doc.concerns ?? [];
84
+ // Through the spec reader, for every caller: a file that does not parse or
85
+ // does not conform is a SpecDocError naming the file (exit 2), never a
86
+ // crash reported as a bug in this tool — and never a concern sweep quietly
87
+ // aims at nothing. A glob concern with no globs matches no file, and a
88
+ // sweep over no file reports clean.
89
+ const doc = readSpecDoc('concerns', p);
90
+ project = doc.concerns;
85
91
  source = p;
86
92
  }
87
93
  const overridden = new Set(project.map((c) => c.id));
@@ -91,11 +97,11 @@ export function loadConcerns(projectDir, { path } = {}) {
91
97
  ];
92
98
  return {
93
99
  concerns,
94
- // The project's own entries, exactly as written. Validation runs against
95
- // these: the enriched objects above carry a `builtin` marker this tool
96
- // added, and a schema that refuses unknown properties is right to reject
97
- // them. Validating what we constructed instead of what the user wrote
98
- // would report OUR bug as THEIR malformed file.
100
+ // The project's own entries, exactly as written — and as validated above,
101
+ // before enrichment: the objects in `concerns` carry a `builtin` marker
102
+ // this tool added, and a schema that refuses unknown properties is right to
103
+ // reject them. Validating what we constructed instead of what the user
104
+ // wrote would report OUR bug as THEIR malformed file.
99
105
  raw: project.map(({ builtin, ...c }) => c),
100
106
  source,
101
107
  shadowed: BUILTIN_CONCERNS.filter((c) => overridden.has(c.id)).map((c) => c.id),
@@ -169,6 +169,7 @@ export async function sweep({
169
169
  runnerCommand,
170
170
  runnerName,
171
171
  nodeModules,
172
+ python,
172
173
  toolVersion = '0.0.0',
173
174
  generatedAt = new Date().toISOString(),
174
175
  claimsPath,
@@ -274,6 +275,7 @@ export async function sweep({
274
275
  runnerCommand,
275
276
  runnerName,
276
277
  nodeModules,
278
+ python,
277
279
  toolVersion,
278
280
  onStage,
279
281
  onWarn,