@orkestrel/scaffold 0.0.88 → 0.0.90
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/dist/agents/skills/orkestrel-harden/scripts/discovery.js +481 -80
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +32 -5
- package/dist/bin/main.js +4 -3
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +594 -83
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +2 -0
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +2 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +34 -5
- package/dist/host/claude/rules/tests.md +2 -0
- package/dist/host/claude/rules/workspace.md +1 -1
- package/dist/host/guides/scaffold.md +13 -3
- package/dist/host/manifest.json +11 -11
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/config.test.ts +17 -1
- package/dist/src/core/index.cjs +131 -105
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +7 -7
- package/dist/src/core/index.d.ts +7 -7
- package/dist/src/core/index.js +131 -105
- package/dist/src/core/index.js.map +1 -1
- package/package.json +4 -3
|
@@ -1,20 +1,149 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Let Vitest select each gate's tests, then compare its identities with each config/mode universe.
|
|
2
|
+
// Run from the checkout root:
|
|
3
3
|
// node .agents/skills/orkestrel-harden/scripts/discovery.ts [--config vite.config.ts] [--projects a,b] [--json]
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// Follow root npm script chains and forwarded arguments. Collect an unfiltered full listing for
|
|
5
|
+
// each config/mode/root/dir, including the base config. Pass gate arguments verbatim except the
|
|
6
|
+
// reporter, output-file, coverage, color, cache, silent, UI, watch, run-mode, inspector, and
|
|
7
|
+
// standalone options;
|
|
8
|
+
// use filesOnly for file selection and full collection for name, tag, line, and shard selection.
|
|
9
|
+
// Compare file/project/name/line/column identities without renaming projects. With --projects,
|
|
10
|
+
// ask Vitest for the reporting scope. Preserve duplicate names within a listing and merge across units.
|
|
11
|
+
// Ignore bench, list, mergeReports, listTags, and clearCache invocations as gates. Flag partly ungated rows, empty chained projects,
|
|
12
|
+
// and undiscovered files under tests/. Report marker counts and empty standalone workbenches.
|
|
13
|
+
// Retry when test files change during collection; refuse another change during that retry.
|
|
14
|
+
// Exit 0 with no flag, 3 with a flag, 2 for missing Vitest, unsupported related commands,
|
|
15
|
+
// CLI/help/argument/listing/output failures, repeated test-file changes, or filesystem/JSON errors;
|
|
16
|
+
// exit 64 on usage.
|
|
12
17
|
import { spawnSync } from 'node:child_process';
|
|
13
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
-
import {
|
|
18
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
15
21
|
import { listFiles, readMissingFlags, readOption, } from "../../orkestrel-dispatch/scripts/helpers.js";
|
|
16
22
|
const VITEST = 'node_modules/vitest/vitest.mjs';
|
|
17
23
|
const MARKERS = ['.skip(', '.todo(', '.skipIf(', '.runIf(', 'retry:', 'timeout:'];
|
|
24
|
+
const OMITTED = new Set([
|
|
25
|
+
'reporter',
|
|
26
|
+
'outputFile',
|
|
27
|
+
'coverage',
|
|
28
|
+
'color',
|
|
29
|
+
'cache',
|
|
30
|
+
'silent',
|
|
31
|
+
'ui',
|
|
32
|
+
'watch',
|
|
33
|
+
'run',
|
|
34
|
+
'inspect',
|
|
35
|
+
'inspectBrk',
|
|
36
|
+
'standalone',
|
|
37
|
+
]);
|
|
38
|
+
// Bounds one listing, so an option that waits for input exits 2 naming its gate instead of
|
|
39
|
+
// hanging the census.
|
|
40
|
+
const LISTING_TIMEOUT = 600_000;
|
|
41
|
+
function readCLIOptions() {
|
|
42
|
+
const help = spawnSync(process.execPath, [VITEST, 'list', '--help', '--expand-help'], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
if (help.status !== 0)
|
|
47
|
+
throw new Error(help.stderr || 'Cannot read Vitest CLI options');
|
|
48
|
+
const options = new Map();
|
|
49
|
+
for (const line of help.stdout.split(/\r\n|\n/u)) {
|
|
50
|
+
const match = /^\s+(?:(-\w), )?--([\w.-]+)(?: ([<[][^\n]*?[>\]]))?\s{2}/u.exec(line);
|
|
51
|
+
const name = match?.[2];
|
|
52
|
+
if (name === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
const option = { name: name.replace(/^no-/u, ''), argument: match?.[3] };
|
|
55
|
+
options.set(`--${name}`, option);
|
|
56
|
+
if (name.startsWith('no-'))
|
|
57
|
+
options.set(`--${name.slice(3)}`, option);
|
|
58
|
+
options.set(`--${name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)}`, option);
|
|
59
|
+
if (match?.[1] !== undefined)
|
|
60
|
+
options.set(match[1], option);
|
|
61
|
+
}
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
function findRunner(command) {
|
|
65
|
+
let index = 0;
|
|
66
|
+
const executable = basename(command[0] ?? '').replace(/\.(?:exe|cmd)$/u, '');
|
|
67
|
+
if (executable === 'npm') {
|
|
68
|
+
if (command[1] !== 'exec')
|
|
69
|
+
return -1;
|
|
70
|
+
index = 2;
|
|
71
|
+
}
|
|
72
|
+
else if (executable === 'node' || executable === 'npx')
|
|
73
|
+
index = 1;
|
|
74
|
+
if (index > 0) {
|
|
75
|
+
while (command[index]?.startsWith('-')) {
|
|
76
|
+
const option = command[index];
|
|
77
|
+
if (['-e', '--eval', '-p', '--print', '-c', '--check'].includes(option ?? ''))
|
|
78
|
+
return -1;
|
|
79
|
+
index += [
|
|
80
|
+
'-r',
|
|
81
|
+
'--require',
|
|
82
|
+
'--import',
|
|
83
|
+
'--loader',
|
|
84
|
+
'--experimental-loader',
|
|
85
|
+
'--max-old-space-size',
|
|
86
|
+
'--max_old_space_size',
|
|
87
|
+
'-p',
|
|
88
|
+
'--package',
|
|
89
|
+
].includes(option ?? '')
|
|
90
|
+
? 2
|
|
91
|
+
: 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return ['vitest', 'vitest.mjs', 'vitest.js'].includes(basename(command[index] ?? '').replace(/\.cmd$/u, ''))
|
|
95
|
+
? index
|
|
96
|
+
: -1;
|
|
97
|
+
}
|
|
98
|
+
function readSelection(argv, options) {
|
|
99
|
+
const selected = [];
|
|
100
|
+
for (let index = 0; index < argv.length; index++) {
|
|
101
|
+
const token = argv[index];
|
|
102
|
+
if (token === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
if (token === '--') {
|
|
105
|
+
selected.push(...argv.slice(index));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (!token.startsWith('-')) {
|
|
109
|
+
selected.push(token);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const equals = token.indexOf('=');
|
|
113
|
+
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
114
|
+
const positive = flag.replace(/^--no-/u, '--');
|
|
115
|
+
const option = options.get(positive) ?? options.get(positive.split('.').slice(0, 1).join('.'));
|
|
116
|
+
if (option === undefined || !OMITTED.has(option.name.split('.')[0] ?? '')) {
|
|
117
|
+
selected.push(token);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const next = argv[index + 1];
|
|
121
|
+
if (!flag.startsWith('--no-') &&
|
|
122
|
+
equals < 0 &&
|
|
123
|
+
next !== undefined &&
|
|
124
|
+
!next.startsWith('-') &&
|
|
125
|
+
(option.argument !== undefined || next === 'true' || next === 'false'))
|
|
126
|
+
index++;
|
|
127
|
+
}
|
|
128
|
+
return selected;
|
|
129
|
+
}
|
|
130
|
+
function readArguments(argv, names) {
|
|
131
|
+
const values = [];
|
|
132
|
+
for (let index = 0; index < argv.length; index++) {
|
|
133
|
+
const token = argv[index];
|
|
134
|
+
if (token === '--')
|
|
135
|
+
break;
|
|
136
|
+
if (token === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
const equals = token.indexOf('=');
|
|
139
|
+
if (!names.includes(equals < 0 ? token : token.slice(0, equals)))
|
|
140
|
+
continue;
|
|
141
|
+
const value = equals < 0 ? argv[index + 1] : token.slice(equals + 1);
|
|
142
|
+
if (value !== undefined && !value.startsWith('-'))
|
|
143
|
+
values.push(value);
|
|
144
|
+
}
|
|
145
|
+
return values;
|
|
146
|
+
}
|
|
18
147
|
function readScripts() {
|
|
19
148
|
const parsed = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
20
149
|
if (typeof parsed !== 'object' || parsed === null)
|
|
@@ -28,72 +157,169 @@ function readScripts() {
|
|
|
28
157
|
record[key] = value;
|
|
29
158
|
return record;
|
|
30
159
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
160
|
+
function readCommands(text) {
|
|
161
|
+
const commands = [];
|
|
162
|
+
let command = [];
|
|
163
|
+
for (const match of text.matchAll(/(?:[^\s"';&|]+|"[^"]*"|'[^']*')+|[;&|]+/gu)) {
|
|
164
|
+
if (/^[;&|]+$/u.test(match[0])) {
|
|
165
|
+
commands.push(command);
|
|
166
|
+
command = [];
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
command.push(match[0].replace(/["']/gu, ''));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
commands.push(command);
|
|
173
|
+
return commands;
|
|
174
|
+
}
|
|
175
|
+
function listInvocations(commands) {
|
|
176
|
+
const invoked = [];
|
|
177
|
+
for (const command of commands) {
|
|
178
|
+
const index = command.indexOf('npm');
|
|
179
|
+
const name = command[index + 2];
|
|
180
|
+
if (index < 0 || command[index + 1] !== 'run' || name === undefined)
|
|
181
|
+
continue;
|
|
182
|
+
const separator = command.indexOf('--', index + 3);
|
|
183
|
+
invoked.push({ name, args: separator < 0 ? [] : command.slice(separator + 1), ancestors: [] });
|
|
184
|
+
}
|
|
185
|
+
return invoked;
|
|
33
186
|
}
|
|
34
|
-
function readGates(scripts) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
187
|
+
function readGates(scripts, config, options) {
|
|
188
|
+
const commands = Object.fromEntries(Object.entries(scripts).map(([name, text]) => [name, readCommands(text)]));
|
|
189
|
+
const invoked = new Set(Object.values(commands)
|
|
190
|
+
.flatMap(listInvocations)
|
|
191
|
+
.map((entry) => entry.name));
|
|
192
|
+
const units = [];
|
|
37
193
|
const files = new Map();
|
|
38
194
|
for (const root of Object.keys(scripts).filter((name) => !invoked.has(name))) {
|
|
39
195
|
const visited = new Set();
|
|
40
|
-
const pending = [root];
|
|
196
|
+
const pending = [{ name: root, args: [], ancestors: [] }];
|
|
41
197
|
while (pending.length > 0) {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
198
|
+
const invocation = pending.pop();
|
|
199
|
+
if (invocation === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
const { name, args, ancestors } = invocation;
|
|
202
|
+
const key = JSON.stringify([name, args]);
|
|
203
|
+
if (visited.has(key) || ancestors.includes(name))
|
|
44
204
|
continue;
|
|
45
|
-
visited.add(
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
205
|
+
visited.add(key);
|
|
206
|
+
const script = commands[name];
|
|
207
|
+
if (script === undefined)
|
|
48
208
|
continue;
|
|
49
|
-
|
|
209
|
+
const expanded = script.map((command, index) => index === script.length - 1 ? [...command, ...args] : command);
|
|
210
|
+
pending.push(...listInvocations(expanded).map((entry) => ({
|
|
211
|
+
...entry,
|
|
212
|
+
ancestors: [...ancestors, name],
|
|
213
|
+
})));
|
|
50
214
|
const gate = root === name ? name : `${root} > ${name}`;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
215
|
+
for (const command of expanded) {
|
|
216
|
+
const index = findRunner(command);
|
|
217
|
+
if (index >= 0) {
|
|
218
|
+
const verb = command[index + 1];
|
|
219
|
+
if (verb === 'list' || verb === 'bench')
|
|
220
|
+
continue;
|
|
221
|
+
const tokens = command.slice(index + (['run', 'watch', 'dev'].includes(verb ?? '') ? 2 : 1));
|
|
222
|
+
const separator = tokens.indexOf('--');
|
|
223
|
+
const before = separator < 0 ? tokens : tokens.slice(0, separator);
|
|
224
|
+
if (before.some((token, position) => /^(?:--mergeReports|--merge-reports|--listTags|--list-tags|--clearCache|--clear-cache)(?:=|$)/u.test(token) &&
|
|
225
|
+
!token.endsWith('=false') &&
|
|
226
|
+
before[position + 1] !== 'false'))
|
|
227
|
+
continue;
|
|
228
|
+
if (verb === 'related')
|
|
229
|
+
throw new Error(`${gate}: Vitest list exposes no related command or --related option`);
|
|
230
|
+
const flags = readSelection(tokens, options);
|
|
231
|
+
const mode = readArguments(flags, ['--mode'])[0];
|
|
232
|
+
const directory = readArguments(flags, ['--root', '-r'])[0];
|
|
233
|
+
const dir = readArguments(flags, ['--dir'])[0];
|
|
234
|
+
const configured = readArguments(flags, ['--config', '-c'])[0];
|
|
235
|
+
const path = configured ?? (directory === undefined && dir === undefined ? config : undefined);
|
|
236
|
+
units.push({
|
|
237
|
+
config: path === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: relative(process.cwd(), resolve(path)).split('\\').join('/'),
|
|
240
|
+
mode: mode === 'test' ? undefined : mode,
|
|
241
|
+
...(directory === undefined ? {} : { root: directory }),
|
|
242
|
+
...(dir === undefined ? {} : { dir }),
|
|
243
|
+
projects: readArguments(flags, ['--project']),
|
|
244
|
+
chain: gate,
|
|
245
|
+
args: configured === undefined && path !== undefined ? ['--config', path, ...flags] : flags,
|
|
246
|
+
full: before.some((flag) => /^(?:-t|--testNamePattern|--test-name-pattern|--tagsFilter|--tags-filter|--shard|--(?:no-)?allowOnly|--(?:no-)?allow-only|--(?:no-)?strictTags|--(?:no-)?strict-tags)(?:=|$)/u.test(flag) || /:\d+(?:-\d+)?$/u.test(flag)),
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
for (const argument of command) {
|
|
251
|
+
if (/^(?:tests|src|app)\/[A-Za-z0-9_./-]+\.test\.[cm]?ts$/u.test(argument) &&
|
|
252
|
+
!files.has(argument))
|
|
253
|
+
files.set(argument, gate);
|
|
55
254
|
}
|
|
56
|
-
}
|
|
57
|
-
for (const match of text.matchAll(/(?:^|\s)((?:tests|src|app)\/[A-Za-z0-9_./-]+\.test\.[cm]?ts)\b/gu)) {
|
|
58
|
-
if (match[1] !== undefined && !files.has(match[1]))
|
|
59
|
-
files.set(match[1], gate);
|
|
60
255
|
}
|
|
61
256
|
}
|
|
62
257
|
}
|
|
63
|
-
return {
|
|
258
|
+
return { units, files };
|
|
64
259
|
}
|
|
65
|
-
function listCollected(
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
260
|
+
function listCollected(args, full, cache) {
|
|
261
|
+
const parameters = [...(full ? ['--includeTaskLocation'] : ['--filesOnly']), ...args];
|
|
262
|
+
const key = JSON.stringify(parameters);
|
|
263
|
+
const cached = cache.get(key);
|
|
264
|
+
if (cached !== undefined)
|
|
265
|
+
return cached;
|
|
266
|
+
const directory = mkdtempSync(join(tmpdir(), 'discovery-'));
|
|
267
|
+
const listing = join(directory, 'listing.json');
|
|
268
|
+
try {
|
|
269
|
+
const result = spawnSync(process.execPath, [VITEST, 'list', `--json=${listing}`, ...parameters], {
|
|
270
|
+
encoding: 'utf8',
|
|
271
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
272
|
+
timeout: LISTING_TIMEOUT,
|
|
273
|
+
windowsHide: true,
|
|
274
|
+
});
|
|
275
|
+
if (args.includes('--project') &&
|
|
276
|
+
result.status === 1 &&
|
|
277
|
+
result.stderr.includes('No projects matched the filter "')) {
|
|
278
|
+
cache.set(key, []);
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
if (result.status !== 0 || !existsSync(listing)) {
|
|
282
|
+
const diagnostic = result.stderr || result.error?.message || result.stdout;
|
|
283
|
+
console.error(diagnostic
|
|
284
|
+
.split(/\r\n|\n/u)
|
|
285
|
+
.filter((line) => line.trim() !== '')
|
|
286
|
+
.slice(0, 12)
|
|
287
|
+
.join('\n'));
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const parsed = JSON.parse(readFileSync(listing, 'utf8'));
|
|
291
|
+
if (!Array.isArray(parsed))
|
|
292
|
+
return undefined;
|
|
293
|
+
const collected = [];
|
|
294
|
+
for (const entry of parsed) {
|
|
295
|
+
if (typeof entry !== 'object' || entry === null)
|
|
296
|
+
continue;
|
|
297
|
+
const record = Object.fromEntries(Object.entries(entry));
|
|
298
|
+
const location = typeof record.location === 'object' && record.location !== null
|
|
299
|
+
? Object.fromEntries(Object.entries(record.location))
|
|
300
|
+
: {};
|
|
301
|
+
if (typeof record.file === 'string' &&
|
|
302
|
+
(record.projectName === undefined || typeof record.projectName === 'string') &&
|
|
303
|
+
(!full || typeof record.name === 'string')) {
|
|
304
|
+
collected.push({
|
|
305
|
+
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
306
|
+
projectName: record.projectName ?? '',
|
|
307
|
+
name: typeof record.name === 'string' ? record.name : undefined,
|
|
308
|
+
line: typeof location.line === 'number' ? location.line : undefined,
|
|
309
|
+
column: typeof location.column === 'number' ? location.column : undefined,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
cache.set(key, collected);
|
|
314
|
+
return collected;
|
|
80
315
|
}
|
|
81
|
-
|
|
82
|
-
|
|
316
|
+
catch (error) {
|
|
317
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
83
318
|
return undefined;
|
|
84
|
-
const collected = [];
|
|
85
|
-
for (const entry of parsed) {
|
|
86
|
-
if (typeof entry !== 'object' || entry === null)
|
|
87
|
-
continue;
|
|
88
|
-
const record = Object.fromEntries(Object.entries(entry));
|
|
89
|
-
if (typeof record.file === 'string' && typeof record.projectName === 'string') {
|
|
90
|
-
collected.push({
|
|
91
|
-
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
92
|
-
projectName: record.projectName,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
319
|
}
|
|
96
|
-
|
|
320
|
+
finally {
|
|
321
|
+
rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
322
|
+
}
|
|
97
323
|
}
|
|
98
324
|
function listTestFiles(directory) {
|
|
99
325
|
if (!existsSync(directory))
|
|
@@ -112,6 +338,151 @@ function countMarkers(file) {
|
|
|
112
338
|
}
|
|
113
339
|
return counts;
|
|
114
340
|
}
|
|
341
|
+
function collectListings(config, named, gates) {
|
|
342
|
+
const units = [{ config, mode: undefined }];
|
|
343
|
+
for (const gate of gates.units) {
|
|
344
|
+
if (!units.some((unit) => unit.config === gate.config &&
|
|
345
|
+
unit.mode === gate.mode &&
|
|
346
|
+
unit.root === gate.root &&
|
|
347
|
+
unit.dir === gate.dir)) {
|
|
348
|
+
units.push({
|
|
349
|
+
config: gate.config,
|
|
350
|
+
mode: gate.mode,
|
|
351
|
+
...(gate.root === undefined ? {} : { root: gate.root }),
|
|
352
|
+
...(gate.dir === undefined ? {} : { dir: gate.dir }),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const listings = [];
|
|
357
|
+
const populated = new Set();
|
|
358
|
+
const cache = new Map();
|
|
359
|
+
for (const unit of units) {
|
|
360
|
+
const reached = gates.units.filter((gate) => gate.config === unit.config &&
|
|
361
|
+
gate.mode === unit.mode &&
|
|
362
|
+
gate.root === unit.root &&
|
|
363
|
+
gate.dir === unit.dir);
|
|
364
|
+
const parameters = readUnitArguments(unit);
|
|
365
|
+
const collected = listCollected(parameters, true, cache);
|
|
366
|
+
if (collected === undefined)
|
|
367
|
+
return undefined;
|
|
368
|
+
const scope = named.length === 0
|
|
369
|
+
? undefined
|
|
370
|
+
: listCollected([...parameters, ...named.flatMap((name) => ['--project', name])], false, cache);
|
|
371
|
+
if (named.length > 0 && scope === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
const selected = [];
|
|
374
|
+
for (const gate of reached) {
|
|
375
|
+
const selection = JSON.stringify(gate.args) === JSON.stringify(parameters)
|
|
376
|
+
? collected
|
|
377
|
+
: listCollected(gate.args, gate.full, cache);
|
|
378
|
+
if (selection === undefined) {
|
|
379
|
+
console.error(`discovery: gate ${gate.chain}: vitest list refused ${gate.args.join(' ')}`);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
selected.push({
|
|
383
|
+
gate,
|
|
384
|
+
identities: new Set(selection.map((entry) => JSON.stringify([
|
|
385
|
+
entry.file,
|
|
386
|
+
entry.projectName,
|
|
387
|
+
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
388
|
+
]))),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const empty = [];
|
|
392
|
+
for (const project of new Set([...reached.flatMap((gate) => gate.projects), ...named])) {
|
|
393
|
+
if (project.includes('*') || project.startsWith('!'))
|
|
394
|
+
continue;
|
|
395
|
+
if (selected.some(({ gate, identities }) => gate.projects.length === 1 && gate.projects[0] === project && identities.size > 0)) {
|
|
396
|
+
populated.add(project);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const selection = listCollected([...parameters, '--project', project], false, cache);
|
|
400
|
+
if (selection === undefined)
|
|
401
|
+
return undefined;
|
|
402
|
+
if (selection.length > 0)
|
|
403
|
+
populated.add(project);
|
|
404
|
+
else if ((reached.some((gate) => gate.projects.includes(project)) || named.includes(project)) &&
|
|
405
|
+
(named.length === 0 || named.includes(project)))
|
|
406
|
+
empty.push(project);
|
|
407
|
+
}
|
|
408
|
+
listings.push({
|
|
409
|
+
...unit,
|
|
410
|
+
collected: collected
|
|
411
|
+
.filter((entry) => scope === undefined ||
|
|
412
|
+
scope.some((chosen) => chosen.file === entry.file && chosen.projectName === entry.projectName))
|
|
413
|
+
.map((entry) => ({
|
|
414
|
+
...entry,
|
|
415
|
+
gate: selected.find(({ gate, identities }) => identities.has(JSON.stringify([
|
|
416
|
+
entry.file,
|
|
417
|
+
entry.projectName,
|
|
418
|
+
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
419
|
+
])))?.gate.chain,
|
|
420
|
+
})),
|
|
421
|
+
empty,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const candidates = new Set(listings.flatMap((listing) => listing.empty));
|
|
425
|
+
for (const name of candidates) {
|
|
426
|
+
if (populated.has(name))
|
|
427
|
+
continue;
|
|
428
|
+
for (const unit of units) {
|
|
429
|
+
const selection = listCollected([...readUnitArguments(unit), '--project', name], false, cache);
|
|
430
|
+
if (selection === undefined)
|
|
431
|
+
return undefined;
|
|
432
|
+
if (selection.length > 0)
|
|
433
|
+
populated.add(name);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return listings.map((listing) => ({
|
|
437
|
+
...listing,
|
|
438
|
+
empty: listing.empty.filter((name) => !populated.has(name)),
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
function readUnitArguments(unit) {
|
|
442
|
+
return [
|
|
443
|
+
...(unit.config === undefined ? [] : ['--config', unit.config]),
|
|
444
|
+
...(unit.mode === undefined ? [] : ['--mode', unit.mode]),
|
|
445
|
+
...(unit.root === undefined ? [] : ['--root', unit.root]),
|
|
446
|
+
...(unit.dir === undefined ? [] : ['--dir', unit.dir]),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
function mergeCollected(listings) {
|
|
450
|
+
const collected = new Map();
|
|
451
|
+
for (const listing of listings) {
|
|
452
|
+
const occurrences = new Map();
|
|
453
|
+
for (const entry of listing.collected) {
|
|
454
|
+
const identity = JSON.stringify([
|
|
455
|
+
entry.file,
|
|
456
|
+
entry.projectName,
|
|
457
|
+
entry.name,
|
|
458
|
+
entry.line,
|
|
459
|
+
entry.column,
|
|
460
|
+
]);
|
|
461
|
+
const occurrence = (occurrences.get(identity) ?? 0) + 1;
|
|
462
|
+
occurrences.set(identity, occurrence);
|
|
463
|
+
const key = JSON.stringify([identity, occurrence]);
|
|
464
|
+
collected.set(key, { ...entry, gate: entry.gate ?? collected.get(key)?.gate });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [...collected.values()];
|
|
468
|
+
}
|
|
469
|
+
function readSnapshot(gates) {
|
|
470
|
+
const bases = [
|
|
471
|
+
process.cwd(),
|
|
472
|
+
...gates.units.flatMap((gate) => [gate.root, gate.dir].filter((path) => path !== undefined).map((path) => resolve(path))),
|
|
473
|
+
];
|
|
474
|
+
const roots = new Set(bases.flatMap((base) => [base, join(base, 'tmp/probes')]));
|
|
475
|
+
const files = new Map();
|
|
476
|
+
for (const root of roots) {
|
|
477
|
+
if (!existsSync(root))
|
|
478
|
+
continue;
|
|
479
|
+
for (const file of listFiles(root)) {
|
|
480
|
+
if (/\.test\.[cm]?[jt]sx?$/u.test(file.path))
|
|
481
|
+
files.set(file.path, file.modified);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return JSON.stringify([...files].sort(([left], [right]) => left.localeCompare(right)));
|
|
485
|
+
}
|
|
115
486
|
function main(argv) {
|
|
116
487
|
const missing = readMissingFlags(argv, ['--config', '--projects']);
|
|
117
488
|
if (missing.length > 0) {
|
|
@@ -127,39 +498,63 @@ function main(argv) {
|
|
|
127
498
|
console.error(`discovery: ${VITEST} is missing; run npm ci first`);
|
|
128
499
|
return 2;
|
|
129
500
|
}
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
501
|
+
const canonical = relative(process.cwd(), resolve(config)).split('\\').join('/');
|
|
502
|
+
const gates = readGates(readScripts(), canonical, readCLIOptions());
|
|
503
|
+
const named = (readOption(argv, '--projects') ?? '').split(',').filter((name) => name !== '');
|
|
504
|
+
let listings;
|
|
505
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
506
|
+
const before = readSnapshot(gates);
|
|
507
|
+
listings = collectListings(canonical, named, gates);
|
|
508
|
+
if (before === readSnapshot(gates))
|
|
509
|
+
break;
|
|
510
|
+
listings = undefined;
|
|
511
|
+
if (attempt === 1)
|
|
512
|
+
throw new Error('Test files changed during both census attempts');
|
|
513
|
+
}
|
|
514
|
+
if (listings === undefined) {
|
|
133
515
|
console.error('discovery: vitest list failed');
|
|
134
516
|
return 2;
|
|
135
517
|
}
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
...collected.map((entry) => entry.projectName),
|
|
140
|
-
...named,
|
|
141
|
-
]);
|
|
518
|
+
const collected = mergeCollected(listings);
|
|
519
|
+
const known = listings.flatMap((listing) => listing.empty);
|
|
520
|
+
const names = new Set([...known, ...collected.map((entry) => entry.projectName)]);
|
|
142
521
|
const projects = [...names].sort().map((name) => {
|
|
143
522
|
const mine = collected.filter((entry) => entry.projectName === name);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
523
|
+
const units = listings.filter((listing) => listing.collected.some((entry) => entry.projectName === name));
|
|
524
|
+
const chains = mine.map((entry) => entry.gate ?? gates.files.get(entry.file));
|
|
525
|
+
const gate = mine.length === 0
|
|
526
|
+
? (gates.units.find((unit) => unit.projects.includes(name) && unit.chain.includes(' > ')) ?? gates.units.find((unit) => unit.projects.includes(name)))?.chain
|
|
527
|
+
: chains.every((chain) => chain !== undefined)
|
|
528
|
+
? chains[0]
|
|
529
|
+
: undefined;
|
|
147
530
|
return {
|
|
148
531
|
name,
|
|
149
|
-
gate
|
|
532
|
+
gate,
|
|
150
533
|
files: new Set(mine.map((entry) => entry.file)).size,
|
|
151
534
|
tests: mine.length,
|
|
535
|
+
...(units.some((unit) => unit.config !== canonical ||
|
|
536
|
+
unit.mode !== undefined ||
|
|
537
|
+
unit.root !== undefined ||
|
|
538
|
+
unit.dir !== undefined)
|
|
539
|
+
? {
|
|
540
|
+
units: units.map((unit) => ({
|
|
541
|
+
config: unit.config,
|
|
542
|
+
mode: unit.mode,
|
|
543
|
+
...(unit.root === undefined ? {} : { root: unit.root }),
|
|
544
|
+
...(unit.dir === undefined ? {} : { dir: unit.dir }),
|
|
545
|
+
})),
|
|
546
|
+
}
|
|
547
|
+
: {}),
|
|
152
548
|
};
|
|
153
549
|
});
|
|
154
550
|
const collectedFiles = new Set(collected.map((entry) => entry.file));
|
|
155
|
-
const undiscovered = listTestFiles('tests')
|
|
551
|
+
const undiscovered = (named.length === 0 ? listTestFiles('tests') : [])
|
|
156
552
|
.filter((file) => !collectedFiles.has(file) && !gates.files.has(file))
|
|
157
553
|
.sort();
|
|
158
554
|
const ungated = projects
|
|
159
555
|
.filter((project) => project.gate === undefined && project.tests > 0)
|
|
160
556
|
.map((project) => project.name);
|
|
161
|
-
//
|
|
162
|
-
// empty one is reported, never flagged.
|
|
557
|
+
// An empty project no chain containing ` > ` names is a workbench, reported without a flag.
|
|
163
558
|
const empty = projects
|
|
164
559
|
.filter((project) => project.tests === 0 && project.gate?.includes(' > ') === true)
|
|
165
560
|
.map((project) => project.name);
|
|
@@ -186,7 +581,7 @@ function main(argv) {
|
|
|
186
581
|
for (const name of empty)
|
|
187
582
|
console.log(`discovery: ${name} collects nothing`);
|
|
188
583
|
for (const name of workbenches)
|
|
189
|
-
console.log(`discovery: ${name} is
|
|
584
|
+
console.log(`discovery: ${name} is an empty workbench; no chain containing " > " names it`);
|
|
190
585
|
for (const file of undiscovered)
|
|
191
586
|
console.log(`discovery: ${file} is collected by no project`);
|
|
192
587
|
for (const entry of census) {
|
|
@@ -198,4 +593,10 @@ function main(argv) {
|
|
|
198
593
|
}
|
|
199
594
|
return flagged ? 3 : 0;
|
|
200
595
|
}
|
|
201
|
-
|
|
596
|
+
try {
|
|
597
|
+
process.exitCode = main(process.argv.slice(2));
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.error(`discovery: ${error instanceof Error ? error.message : String(error)}`);
|
|
601
|
+
process.exitCode = 2;
|
|
602
|
+
}
|