testguard-cli 0.18.3 → 0.19.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 +89 -0
- package/README.md +84 -1097
- package/package.json +1 -1
- package/spec/GATE-SEMANTICS.md +72 -24
- package/spec/README.md +4 -2
- package/spec/lib/validate.mjs +22 -1
- package/spec/schemas/evidence.schema.json +2 -2
- 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/probe/probe.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { hashFile, sha256 } from '../util/hash.mjs';
|
|
|
21
21
|
import { fingerprint } from '../../spec/lib/fingerprint.mjs';
|
|
22
22
|
import { recordedOriginSummary } from '../../spec/lib/origins.mjs';
|
|
23
23
|
import { persistenceSignalsFor } from '../supply/persistence.mjs';
|
|
24
|
-
import { collectOwnedManifests, hashNativeTestUniverse } from './universe.mjs';
|
|
24
|
+
import { collectOwnedManifests, hashNativeTestUniverse, hashCustomCommandUniverse } from './universe.mjs';
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
/** Is `child` the same file as `root`, or under it? Both must already be real paths. */
|
|
@@ -62,6 +62,27 @@ export function sameWorkerPolicy(previousRun, { workers = 1, serial = workers ==
|
|
|
62
62
|
return previousRun?.workers === (serial ? 1 : workers) && Boolean(previousRun?.serial) === serial;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* What the evidence says ran: the engine's name, the version it reported, and
|
|
67
|
+
* where it was resolved from (`project`, `path`, `builtin`). A custom command
|
|
68
|
+
* resolves nothing, so it claims no source.
|
|
69
|
+
*/
|
|
70
|
+
export function runnerRecord(name, version, source) {
|
|
71
|
+
return { name, ...(version ? { version } : {}), ...(source ? { source } : {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reuse cannot stand in for a measurement by a different runner. pytest and
|
|
76
|
+
* unittest are one adapter and two engines; a global vitest of the same
|
|
77
|
+
* version is a different binary from the project's. Evidence that recorded no
|
|
78
|
+
* runner cannot vouch for this one.
|
|
79
|
+
*/
|
|
80
|
+
export function sameRunner(previousRun, current) {
|
|
81
|
+
const recorded = previousRun?.runner;
|
|
82
|
+
if (!recorded) return false;
|
|
83
|
+
return recorded.name === current.name && recorded.version === current.version && recorded.source === current.source;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* Probe every fault of every claim. Returns a spec-conformant evidence
|
|
67
88
|
* document; writing it is the caller's job.
|
|
@@ -205,13 +226,14 @@ export async function probe({
|
|
|
205
226
|
// Contention: name it before the first run, and record it, so a later
|
|
206
227
|
// reader of a TIMEOUT or FLAKY-DEFENDER knows the machine was loaded.
|
|
207
228
|
const contention = detectContention();
|
|
208
|
-
if (contention.detected) onWarn(contentionWarning(contention));
|
|
229
|
+
if (contention.detected) onWarn(contentionWarning(contention, { serial, workers, custom: Boolean(runnerCommand) }));
|
|
209
230
|
|
|
210
231
|
const iso = mode === 'worktree' ? createScratch({ repoRoot: root, projectDir, ref: snapshot ?? ref, scratchBase, nodeModules }) : inPlace({ repoRoot: root, projectDir });
|
|
211
232
|
const isoReal = realpathSync(iso.projectDir);
|
|
212
233
|
const records = [];
|
|
213
234
|
let runnerVersion;
|
|
214
235
|
let runnerSource;
|
|
236
|
+
let recordedRunner;
|
|
215
237
|
let primaryManifest;
|
|
216
238
|
let runner = RUNNERS[runnerName === 'auto' ? 'vitest' : runnerName];
|
|
217
239
|
// A runner with more than one engine (Python: pytest or unittest) is recorded
|
|
@@ -236,7 +258,8 @@ export async function probe({
|
|
|
236
258
|
primaryManifest = sel.manifest;
|
|
237
259
|
if (sel.engine) engines.set(sel.runner, sel.engine);
|
|
238
260
|
}
|
|
239
|
-
|
|
261
|
+
recordedRunner = runnerRecord(labelOf(runner), runnerVersion ?? readRunnerVersion(projectDir, runner.name), runnerSource);
|
|
262
|
+
runnersUsed.set(recordedRunner.name, recordedRunner.version);
|
|
240
263
|
// Files an owning runner (Playwright) claims run under it, whatever the
|
|
241
264
|
// project runner is; it must resolve before the first such defender runs.
|
|
242
265
|
const owned = OWNED_RUNNERS.filter((r) => r !== runner
|
|
@@ -283,7 +306,7 @@ export async function probe({
|
|
|
283
306
|
? [...new Set([...runner.tests(iso.projectDir).filter((t) => !owned.some((r) => r.owns(iso.projectDir, t))), ...owned.flatMap((r) => r.tests(iso.projectDir))])]
|
|
284
307
|
: [...ownerByFile.keys()]).sort());
|
|
285
308
|
const testUniverseHash = commandTemplate
|
|
286
|
-
?
|
|
309
|
+
? hashCustomCommandUniverse(allTests, commandTemplate)
|
|
287
310
|
: hashNativeTestUniverse(manifests);
|
|
288
311
|
const baselineCache = new Map();
|
|
289
312
|
const discoveryHashCache = new Map();
|
|
@@ -295,6 +318,7 @@ export async function probe({
|
|
|
295
318
|
const fatalEditFor = (file) => runnerFor(iso.projectDir, file, runner).fatalEdit?.();
|
|
296
319
|
const prior = previous && previous.run.confirmRuns === confirmRuns
|
|
297
320
|
&& sameWorkerPolicy(previous.run, { workers, serial, runnerCommand })
|
|
321
|
+
&& sameRunner(previous.run, recordedRunner)
|
|
298
322
|
? new Map(previous.records.map((r) => [`${r.claim.id}/${r.subject.id}`, r]))
|
|
299
323
|
: new Map();
|
|
300
324
|
const partitionConfigured = (files) => {
|
|
@@ -403,7 +427,9 @@ export async function probe({
|
|
|
403
427
|
startedAt,
|
|
404
428
|
finishedAt: new Date().toISOString(),
|
|
405
429
|
repo: { head, dirty, ...(snapshot ? { snapshot } : {}), ...(ignoredDirty.length ? { ignoredDirty } : {}) },
|
|
406
|
-
|
|
430
|
+
// Where the runner came from is part of what was measured: recorded for
|
|
431
|
+
// every resolved runner, as the schema documents, not only the builtin.
|
|
432
|
+
runner: recordedRunner,
|
|
407
433
|
...(runnersUsed.size > 1 ? { runners: [...runnersUsed].map(([n, v]) => ({ name: n, ...(v ? { version: v } : {}) })) } : {}),
|
|
408
434
|
confirmRuns,
|
|
409
435
|
measurements,
|
|
@@ -2,7 +2,7 @@ import { performance } from 'node:perf_hooks';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { registerChild } from './lifecycle.mjs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { runProcess, listTestFiles, firstInformativeLine, terminateProcessTree } from './shared.mjs';
|
|
8
8
|
|
|
@@ -69,6 +69,20 @@ export const testGlobs = ['**/test_*.py', '**/test*.py', '**/*_test.py'];
|
|
|
69
69
|
const PY_FILE = /\.py$/;
|
|
70
70
|
const TIMEOUT = /\bTimeout >|\btimed out\b|\bTimeoutError\b/i;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The interpreter the operator named, with `--python` or `TESTGUARD_PYTHON` —
|
|
74
|
+
* both the same way, as a shell would. A bare command name (`python3`, no path
|
|
75
|
+
* separator) is looked up on PATH: resolving it against the working directory
|
|
76
|
+
* produced `<cwd>/python3`, which is never an interpreter. Anything with a
|
|
77
|
+
* separator is a path, resolved once against the working directory. An empty
|
|
78
|
+
* value names nothing.
|
|
79
|
+
*/
|
|
80
|
+
export function explicitInterpreter(value) {
|
|
81
|
+
if (typeof value !== 'string' || value === '') return undefined;
|
|
82
|
+
const bare = !value.includes('/') && !(process.platform === 'win32' && value.includes('\\'));
|
|
83
|
+
return bare ? { path: value, source: 'path' } : { path: resolve(value), source: 'project' };
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/**
|
|
73
87
|
* Interpreters to try, best first: the active virtualenv, the project's own,
|
|
74
88
|
* then PATH.
|
|
@@ -82,8 +96,8 @@ function candidates(projectDir, python) {
|
|
|
82
96
|
const out = [];
|
|
83
97
|
const win = process.platform === 'win32';
|
|
84
98
|
const binOf = (root) => (win ? join(root, 'Scripts', 'python.exe') : join(root, 'bin', 'python'));
|
|
85
|
-
const explicit = python ?? process.env.TESTGUARD_PYTHON;
|
|
86
|
-
if (explicit) return [{
|
|
99
|
+
const explicit = explicitInterpreter(python ?? process.env.TESTGUARD_PYTHON);
|
|
100
|
+
if (explicit) return [{ ...explicit, explicit: true }];
|
|
87
101
|
if (process.env.VIRTUAL_ENV) out.push({ path: binOf(process.env.VIRTUAL_ENV), source: 'project' });
|
|
88
102
|
for (const dir of ['.venv', 'venv', '.env']) out.push({ path: binOf(join(projectDir, dir)), source: 'project' });
|
|
89
103
|
out.push({ path: win ? 'python.exe' : 'python3', source: 'path' });
|
|
@@ -159,9 +173,9 @@ export function resolveInterpreter({ projectDir, python, budgetMs = 30_000, budg
|
|
|
159
173
|
if (answer) return { ...candidate, ...answer };
|
|
160
174
|
tried.push(candidate.path);
|
|
161
175
|
}
|
|
162
|
-
const explicit = python ?? process.env.TESTGUARD_PYTHON;
|
|
176
|
+
const explicit = explicitInterpreter(python ?? process.env.TESTGUARD_PYTHON);
|
|
163
177
|
return { error: explicit
|
|
164
|
-
? `${explicit} is not a usable Python interpreter`
|
|
178
|
+
? `${explicit.path} is not a usable Python interpreter${explicit.source === 'path' ? ' (looked up on PATH)' : ''}`
|
|
165
179
|
: `no usable Python interpreter (tried ${tried.join(', ') || 'nothing'}); install Python 3.8+, or point --python / TESTGUARD_PYTHON at one` };
|
|
166
180
|
})();
|
|
167
181
|
interpreters.set(key, task);
|
package/src/probe/universe.mjs
CHANGED
|
@@ -18,6 +18,17 @@ export function hashNativeTestUniverse(manifests) {
|
|
|
18
18
|
return sha256(JSON.stringify(bound));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A custom `--runner-cmd` has no native listing, so its universe is the static
|
|
23
|
+
* file set — and the command itself. The files alone cannot tell a broken
|
|
24
|
+
* command from the fixed one, and a verdict measured by one is not an answer
|
|
25
|
+
* from the other. The parsed argv is hashed, so reformatting whitespace is not
|
|
26
|
+
* a change and the command text never appears in the evidence.
|
|
27
|
+
*/
|
|
28
|
+
export function hashCustomCommandUniverse(files, commandTemplate) {
|
|
29
|
+
return sha256(JSON.stringify({ schemaVersion: 2, source: 'custom-command-static', command: commandTemplate, files }));
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
/** Shared owning-adapter collection; check failures retain existing probe semantics. */
|
|
22
33
|
export async function collectOwnedManifests(runner, primaryManifest, { projectDir, sourceDir, python, budgetMs, budgetFor, assertOpen = () => {} }) {
|
|
23
34
|
const owned = OWNED_RUNNERS.filter((r) => r !== runner
|
|
@@ -41,7 +52,8 @@ export async function readNativeTestUniverse(options) {
|
|
|
41
52
|
options.assertOpen?.();
|
|
42
53
|
if (selected.error) throw new DiscoveryError(selected.error);
|
|
43
54
|
const collection = await collectOwnedManifests(selected.runner, selected.manifest, options);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
55
|
+
const identity = (name, version, source) => ({ name, version, ...(source ? { source } : {}) });
|
|
56
|
+
const primaryRunner = identity(selected.engine ?? selected.runner.name, selected.version, selected.source);
|
|
57
|
+
const runners = [primaryRunner, ...[...collection.ownedChecked].filter(([, check]) => check.ok).map(([runner, check]) => identity(check.engine ?? runner.name, check.version, check.source))];
|
|
46
58
|
return { ...collection, primary: selected.runner, primaryRunner, runners, testUniverseHash: hashNativeTestUniverse(collection.manifests) };
|
|
47
59
|
}
|
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,
|