@orkestrel/scaffold 0.0.87 → 0.0.89
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 +483 -77
- 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 +596 -76
- 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/agents/orkestrel.md +2 -2
- package/dist/host/claude/rules/architecture.md +4 -0
- package/dist/host/claude/rules/tests.md +2 -0
- package/dist/host/claude/rules/workspace.md +1 -1
- package/dist/host/configs/policy.ts +13 -3
- package/dist/host/guides/browser.md +2626 -1195
- package/dist/host/guides/contract.md +1 -1
- package/dist/host/guides/scaffold.md +13 -3
- package/dist/host/manifest.json +16 -16
- package/dist/host/tests/config.test.ts +51 -1
- package/dist/src/core/index.cjs +138 -105
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +14 -14
- package/dist/src/core/index.d.ts +14 -14
- package/dist/src/core/index.js +138 -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,67 +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)
|
|
44
200
|
continue;
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
201
|
+
const { name, args, ancestors } = invocation;
|
|
202
|
+
const key = JSON.stringify([name, args]);
|
|
203
|
+
if (visited.has(key) || ancestors.includes(name))
|
|
48
204
|
continue;
|
|
49
|
-
|
|
205
|
+
visited.add(key);
|
|
206
|
+
const script = commands[name];
|
|
207
|
+
if (script === undefined)
|
|
208
|
+
continue;
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
collected.push({
|
|
86
|
-
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
87
|
-
projectName: record.projectName,
|
|
88
|
-
});
|
|
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 [];
|
|
89
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;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
rmSync(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
90
322
|
}
|
|
91
|
-
return collected;
|
|
92
323
|
}
|
|
93
324
|
function listTestFiles(directory) {
|
|
94
325
|
if (!existsSync(directory))
|
|
@@ -107,6 +338,151 @@ function countMarkers(file) {
|
|
|
107
338
|
}
|
|
108
339
|
return counts;
|
|
109
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
|
+
}
|
|
110
486
|
function main(argv) {
|
|
111
487
|
const missing = readMissingFlags(argv, ['--config', '--projects']);
|
|
112
488
|
if (missing.length > 0) {
|
|
@@ -122,39 +498,63 @@ function main(argv) {
|
|
|
122
498
|
console.error(`discovery: ${VITEST} is missing; run npm ci first`);
|
|
123
499
|
return 2;
|
|
124
500
|
}
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
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) {
|
|
515
|
+
console.error('discovery: vitest list failed');
|
|
129
516
|
return 2;
|
|
130
517
|
}
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
...collected.map((entry) => entry.projectName),
|
|
135
|
-
...named,
|
|
136
|
-
]);
|
|
518
|
+
const collected = mergeCollected(listings);
|
|
519
|
+
const known = listings.flatMap((listing) => listing.empty);
|
|
520
|
+
const names = new Set([...known, ...collected.map((entry) => entry.projectName)]);
|
|
137
521
|
const projects = [...names].sort().map((name) => {
|
|
138
522
|
const mine = collected.filter((entry) => entry.projectName === name);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
530
|
return {
|
|
143
531
|
name,
|
|
144
|
-
gate
|
|
532
|
+
gate,
|
|
145
533
|
files: new Set(mine.map((entry) => entry.file)).size,
|
|
146
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
|
+
: {}),
|
|
147
548
|
};
|
|
148
549
|
});
|
|
149
550
|
const collectedFiles = new Set(collected.map((entry) => entry.file));
|
|
150
|
-
const undiscovered = listTestFiles('tests')
|
|
551
|
+
const undiscovered = (named.length === 0 ? listTestFiles('tests') : [])
|
|
151
552
|
.filter((file) => !collectedFiles.has(file) && !gates.files.has(file))
|
|
152
553
|
.sort();
|
|
153
554
|
const ungated = projects
|
|
154
555
|
.filter((project) => project.gate === undefined && project.tests > 0)
|
|
155
556
|
.map((project) => project.name);
|
|
156
|
-
//
|
|
157
|
-
// empty one is reported, never flagged.
|
|
557
|
+
// An empty project no chain containing ` > ` names is a workbench, reported without a flag.
|
|
158
558
|
const empty = projects
|
|
159
559
|
.filter((project) => project.tests === 0 && project.gate?.includes(' > ') === true)
|
|
160
560
|
.map((project) => project.name);
|
|
@@ -181,7 +581,7 @@ function main(argv) {
|
|
|
181
581
|
for (const name of empty)
|
|
182
582
|
console.log(`discovery: ${name} collects nothing`);
|
|
183
583
|
for (const name of workbenches)
|
|
184
|
-
console.log(`discovery: ${name} is
|
|
584
|
+
console.log(`discovery: ${name} is an empty workbench; no chain containing " > " names it`);
|
|
185
585
|
for (const file of undiscovered)
|
|
186
586
|
console.log(`discovery: ${file} is collected by no project`);
|
|
187
587
|
for (const entry of census) {
|
|
@@ -193,4 +593,10 @@ function main(argv) {
|
|
|
193
593
|
}
|
|
194
594
|
return flagged ? 3 : 0;
|
|
195
595
|
}
|
|
196
|
-
|
|
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
|
+
}
|