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.
- package/CHANGELOG.md +104 -0
- package/README.md +84 -1097
- package/package.json +1 -1
- package/spec/GATE-SEMANTICS.md +90 -24
- package/spec/README.md +4 -2
- package/spec/lib/origins.mjs +19 -0
- package/spec/lib/validate.mjs +22 -1
- package/spec/schemas/claims.schema.json +1 -15
- package/spec/schemas/common.schema.json +16 -0
- package/spec/schemas/evidence.schema.json +3 -16
- package/spec/schemas/ignore.schema.json +2 -2
- package/spec/schemas/status.schema.json +23 -0
- package/src/brief/brief.mjs +18 -0
- package/src/cli.mjs +34 -10
- package/src/commands/admit.mjs +8 -1
- package/src/commands/baseline.mjs +16 -2
- package/src/commands/brief.mjs +2 -2
- package/src/commands/claims.mjs +1 -1
- package/src/commands/concerns.mjs +2 -10
- package/src/commands/gate.mjs +2 -4
- package/src/commands/init.mjs +12 -2
- package/src/commands/probe.mjs +13 -5
- package/src/commands/replay.mjs +9 -2
- package/src/commands/runner-options.mjs +28 -0
- package/src/commands/sweep.mjs +8 -1
- package/src/gate/changed.mjs +11 -0
- package/src/git.mjs +12 -2
- package/src/init/init.mjs +169 -74
- package/src/probe/attribution.mjs +5 -0
- package/src/probe/contention.mjs +14 -3
- package/src/probe/cost.mjs +7 -1
- package/src/probe/policy-freshness.mjs +5 -1
- package/src/probe/probe.mjs +31 -5
- package/src/probe/runners/python.mjs +19 -5
- package/src/probe/universe.mjs +14 -2
- package/src/replay/replay.mjs +9 -8
- package/src/supply/concerns.mjs +14 -8
- package/src/sweep/sweep.mjs +2 -0
package/src/replay/replay.mjs
CHANGED
|
@@ -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);
|
package/src/supply/concerns.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
95
|
-
//
|
|
96
|
-
// added, and a schema that refuses unknown properties is right to
|
|
97
|
-
// them. Validating what we constructed instead of what the user
|
|
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),
|
package/src/sweep/sweep.mjs
CHANGED
|
@@ -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,
|