@orkestrel/scaffold 0.0.101 → 0.0.102
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-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +30 -12
- package/dist/host/configs/helpers.ts +206 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +35 -29
- package/dist/host/tests/config.test.ts +1220 -633
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1669 -985
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -45,104 +45,103 @@ function readCLIOptions() {
|
|
|
45
45
|
});
|
|
46
46
|
if (help.status !== 0)
|
|
47
47
|
throw new Error(help.stderr || 'Cannot read Vitest CLI options');
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// A later line that names the same flag replaces the earlier option at the earlier position.
|
|
49
|
+
return new Map(help.stdout.split(/\r\n|\n/u).flatMap((line) => {
|
|
50
50
|
const match = /^\s+(?:(-\w), )?--([\w.-]+)(?: ([<[][^\n]*?[>\]]))?\s{2}/u.exec(line);
|
|
51
51
|
const name = match?.[2];
|
|
52
52
|
if (name === undefined)
|
|
53
|
-
|
|
53
|
+
return [];
|
|
54
54
|
const option = { name: name.replace(/^no-/u, ''), argument: match?.[3] };
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
55
|
+
return [
|
|
56
|
+
[`--${name}`, option],
|
|
57
|
+
...(name.startsWith('no-') ? [[`--${name.slice(3)}`, option]] : []),
|
|
58
|
+
[`--${name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)}`, option],
|
|
59
|
+
...(match?.[1] === undefined ? [] : [[match[1], option]]),
|
|
60
|
+
];
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
// An option that takes a value consumes the token after it, so that token is never read as a runner.
|
|
64
|
+
function skipRunnerOptions(command, index) {
|
|
65
|
+
const skipped = [...command.entries()].reduce((state, [position, option]) => {
|
|
66
|
+
if (state.found !== undefined || position < state.next)
|
|
67
|
+
return state;
|
|
68
|
+
if (!option.startsWith('-'))
|
|
69
|
+
return { ...state, found: position };
|
|
70
|
+
if (['-e', '--eval', '-p', '--print', '-c', '--check'].includes(option)) {
|
|
71
|
+
return { ...state, found: -1 };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
next: position +
|
|
76
|
+
([
|
|
77
|
+
'-r',
|
|
78
|
+
'--require',
|
|
79
|
+
'--import',
|
|
80
|
+
'--loader',
|
|
81
|
+
'--experimental-loader',
|
|
82
|
+
'--max-old-space-size',
|
|
83
|
+
'--max_old_space_size',
|
|
84
|
+
'-p',
|
|
85
|
+
'--package',
|
|
86
|
+
].includes(option)
|
|
87
|
+
? 2
|
|
88
|
+
: 1),
|
|
89
|
+
};
|
|
90
|
+
}, { next: index, found: undefined });
|
|
91
|
+
return skipped.found ?? skipped.next;
|
|
63
92
|
}
|
|
64
93
|
function findRunner(command) {
|
|
65
|
-
let index = 0;
|
|
66
94
|
const executable = basename(command[0] ?? '').replace(/\.(?:exe|cmd)$/u, '');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
95
|
+
const start = executable === 'npm'
|
|
96
|
+
? command[1] === 'exec'
|
|
97
|
+
? 2
|
|
98
|
+
: -1
|
|
99
|
+
: executable === 'node' || executable === 'npx'
|
|
100
|
+
? 1
|
|
101
|
+
: 0;
|
|
102
|
+
const index = start > 0 ? skipRunnerOptions(command, start) : start;
|
|
103
|
+
if (index < 0)
|
|
104
|
+
return -1;
|
|
94
105
|
return ['vitest', 'vitest.mjs', 'vitest.js'].includes(basename(command[index] ?? '').replace(/\.cmd$/u, ''))
|
|
95
106
|
? index
|
|
96
107
|
: -1;
|
|
97
108
|
}
|
|
109
|
+
// The first `--` ends the options, so the rest of the arguments pass verbatim. A runner option that
|
|
110
|
+
// takes a value consumes the argument after it, so a step below `skip` reads nothing.
|
|
98
111
|
function readSelection(argv, options) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (token === undefined)
|
|
103
|
-
continue;
|
|
112
|
+
return argv.reduce((state, token, index) => {
|
|
113
|
+
if (state.closed || index < state.skip)
|
|
114
|
+
return state;
|
|
104
115
|
if (token === '--') {
|
|
105
|
-
selected.
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
if (!token.startsWith('-')) {
|
|
109
|
-
selected.push(token);
|
|
110
|
-
continue;
|
|
116
|
+
return { ...state, selected: state.selected.concat(argv.slice(index)), closed: true };
|
|
111
117
|
}
|
|
118
|
+
if (!token.startsWith('-'))
|
|
119
|
+
return { ...state, selected: [...state.selected, token] };
|
|
112
120
|
const equals = token.indexOf('=');
|
|
113
121
|
const flag = equals < 0 ? token : token.slice(0, equals);
|
|
114
122
|
const positive = flag.replace(/^--no-/u, '--');
|
|
115
123
|
const option = options.get(positive) ?? options.get(positive.split('.').slice(0, 1).join('.'));
|
|
116
124
|
if (option === undefined || !OMITTED.has(option.name.split('.')[0] ?? '')) {
|
|
117
|
-
selected.
|
|
118
|
-
continue;
|
|
125
|
+
return { ...state, selected: [...state.selected, token] };
|
|
119
126
|
}
|
|
120
127
|
const next = argv[index + 1];
|
|
121
|
-
|
|
128
|
+
const valued = !flag.startsWith('--no-') &&
|
|
122
129
|
equals < 0 &&
|
|
123
130
|
next !== undefined &&
|
|
124
131
|
!next.startsWith('-') &&
|
|
125
|
-
(option.argument !== undefined || next === 'true' || next === 'false')
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
return selected;
|
|
132
|
+
(option.argument !== undefined || next === 'true' || next === 'false');
|
|
133
|
+
return { ...state, skip: index + (valued ? 2 : 1) };
|
|
134
|
+
}, { selected: [], skip: 0, closed: false }).selected;
|
|
129
135
|
}
|
|
130
136
|
function readArguments(argv, names) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const token = argv[index];
|
|
134
|
-
if (token === '--')
|
|
135
|
-
break;
|
|
136
|
-
if (token === undefined)
|
|
137
|
-
continue;
|
|
137
|
+
const end = argv.indexOf('--');
|
|
138
|
+
return (end < 0 ? argv : argv.slice(0, end)).flatMap((token, index) => {
|
|
138
139
|
const equals = token.indexOf('=');
|
|
139
140
|
if (!names.includes(equals < 0 ? token : token.slice(0, equals)))
|
|
140
|
-
|
|
141
|
+
return [];
|
|
141
142
|
const value = equals < 0 ? argv[index + 1] : token.slice(equals + 1);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
return values;
|
|
143
|
+
return value !== undefined && !value.startsWith('-') ? [value] : [];
|
|
144
|
+
});
|
|
146
145
|
}
|
|
147
146
|
function readScripts() {
|
|
148
147
|
const parsed = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
@@ -151,38 +150,23 @@ function readScripts() {
|
|
|
151
150
|
const scripts = Object.fromEntries(Object.entries(parsed)).scripts;
|
|
152
151
|
if (typeof scripts !== 'object' || scripts === null)
|
|
153
152
|
return {};
|
|
154
|
-
|
|
155
|
-
for (const [key, value] of Object.entries(scripts))
|
|
156
|
-
if (typeof value === 'string')
|
|
157
|
-
record[key] = value;
|
|
158
|
-
return record;
|
|
153
|
+
return Object.fromEntries(Object.entries(scripts).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : []));
|
|
159
154
|
}
|
|
160
155
|
function readCommands(text) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
|
156
|
+
const tokens = [...text.matchAll(/(?:[^\s"';&|]+|"[^"]*"|'[^']*')+|[;&|]+/gu)].map((match) => match[0]);
|
|
157
|
+
// A separator token ends a command, so the commands are the runs of tokens between the cuts.
|
|
158
|
+
const cuts = tokens.flatMap((token, index) => (/^[;&|]+$/u.test(token) ? [index] : []));
|
|
159
|
+
return [-1, ...cuts].map((cut, at) => tokens.slice(cut + 1, cuts[at] ?? tokens.length).map((token) => token.replace(/["']/gu, '')));
|
|
174
160
|
}
|
|
175
161
|
function listInvocations(commands) {
|
|
176
|
-
|
|
177
|
-
for (const command of commands) {
|
|
162
|
+
return commands.flatMap((command) => {
|
|
178
163
|
const index = command.indexOf('npm');
|
|
179
164
|
const name = command[index + 2];
|
|
180
165
|
if (index < 0 || command[index + 1] !== 'run' || name === undefined)
|
|
181
|
-
|
|
166
|
+
return [];
|
|
182
167
|
const separator = command.indexOf('--', index + 3);
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
return invoked;
|
|
168
|
+
return [{ name, args: separator < 0 ? [] : command.slice(separator + 1), ancestors: [] }];
|
|
169
|
+
});
|
|
186
170
|
}
|
|
187
171
|
function readGates(scripts, config, options) {
|
|
188
172
|
const commands = Object.fromEntries(Object.entries(scripts).map(([name, text]) => [name, readCommands(text)]));
|
|
@@ -207,10 +191,9 @@ function readGates(scripts, config, options) {
|
|
|
207
191
|
if (script === undefined)
|
|
208
192
|
continue;
|
|
209
193
|
const expanded = script.map((command, index) => index === script.length - 1 ? [...command, ...args] : command);
|
|
210
|
-
|
|
211
|
-
...entry,
|
|
212
|
-
|
|
213
|
-
})));
|
|
194
|
+
for (const entry of listInvocations(expanded)) {
|
|
195
|
+
pending.push({ ...entry, ancestors: [...ancestors, name] });
|
|
196
|
+
}
|
|
214
197
|
const gate = root === name ? name : `${root} > ${name}`;
|
|
215
198
|
for (const command of expanded) {
|
|
216
199
|
const index = findRunner(command);
|
|
@@ -290,26 +273,28 @@ function listCollected(args, full, cache) {
|
|
|
290
273
|
const parsed = JSON.parse(readFileSync(listing, 'utf8'));
|
|
291
274
|
if (!Array.isArray(parsed))
|
|
292
275
|
return undefined;
|
|
293
|
-
const collected =
|
|
294
|
-
for (const entry of parsed) {
|
|
276
|
+
const collected = parsed.flatMap((entry) => {
|
|
295
277
|
if (typeof entry !== 'object' || entry === null)
|
|
296
|
-
|
|
278
|
+
return [];
|
|
297
279
|
const record = Object.fromEntries(Object.entries(entry));
|
|
298
280
|
const location = typeof record.location === 'object' && record.location !== null
|
|
299
281
|
? Object.fromEntries(Object.entries(record.location))
|
|
300
282
|
: {};
|
|
301
|
-
if (typeof record.file
|
|
302
|
-
(record.projectName
|
|
303
|
-
(
|
|
304
|
-
|
|
283
|
+
if (typeof record.file !== 'string' ||
|
|
284
|
+
(record.projectName !== undefined && typeof record.projectName !== 'string') ||
|
|
285
|
+
(full && typeof record.name !== 'string')) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
305
290
|
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
306
291
|
projectName: record.projectName ?? '',
|
|
307
292
|
name: typeof record.name === 'string' ? record.name : undefined,
|
|
308
293
|
line: typeof location.line === 'number' ? location.line : undefined,
|
|
309
294
|
column: typeof location.column === 'number' ? location.column : undefined,
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
}
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
});
|
|
313
298
|
cache.set(key, collected);
|
|
314
299
|
return collected;
|
|
315
300
|
}
|
|
@@ -330,82 +315,111 @@ function listTestFiles(directory) {
|
|
|
330
315
|
}
|
|
331
316
|
function countMarkers(file) {
|
|
332
317
|
const text = readFileSync(file, 'utf8');
|
|
333
|
-
|
|
334
|
-
for (const marker of MARKERS) {
|
|
335
|
-
const count = text.split(marker).length - 1;
|
|
336
|
-
if (count > 0)
|
|
337
|
-
counts[marker] = count;
|
|
338
|
-
}
|
|
339
|
-
return counts;
|
|
318
|
+
return Object.fromEntries(MARKERS.map((marker) => [marker, text.split(marker).length - 1]).filter(([, count]) => count > 0));
|
|
340
319
|
}
|
|
341
320
|
function collectListings(config, named, gates) {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const listings = [];
|
|
357
|
-
const populated = new Set();
|
|
321
|
+
const base = { config, mode: undefined };
|
|
322
|
+
// A gate joins the units unless the base unit or an earlier gate already names the same unit.
|
|
323
|
+
const units = [
|
|
324
|
+
base,
|
|
325
|
+
...gates.units
|
|
326
|
+
.filter((gate, index) => ![base, ...gates.units.slice(0, index)].some((unit) => isSameUnit(unit, gate)))
|
|
327
|
+
.map((gate) => ({
|
|
328
|
+
config: gate.config,
|
|
329
|
+
mode: gate.mode,
|
|
330
|
+
...(gate.root === undefined ? {} : { root: gate.root }),
|
|
331
|
+
...(gate.dir === undefined ? {} : { dir: gate.dir }),
|
|
332
|
+
})),
|
|
333
|
+
];
|
|
334
|
+
// A listing is a memo keyed by its arguments: each key is written once, on its first miss.
|
|
358
335
|
const cache = new Map();
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
336
|
+
const readings = collectReadings(units, (unit) => readUnit(unit, named, gates, cache));
|
|
337
|
+
if (readings === undefined)
|
|
338
|
+
return undefined;
|
|
339
|
+
const listings = readings.map(({ listing }) => listing);
|
|
340
|
+
const populated = new Set(readings.flatMap((reading) => reading.populated));
|
|
341
|
+
// A project empty under one unit may still list tests under another, so each unit is asked for it.
|
|
342
|
+
const rescans = collectReadings([...new Set(listings.flatMap((listing) => listing.empty))]
|
|
343
|
+
.filter((name) => !populated.has(name))
|
|
344
|
+
.flatMap((name) => units.map((unit) => ({ name, unit }))), ({ name, unit }) => {
|
|
345
|
+
const selection = listCollected([...readUnitArguments(unit), '--project', name], false, cache);
|
|
346
|
+
return selection === undefined ? undefined : { name, listed: selection.length > 0 };
|
|
347
|
+
});
|
|
348
|
+
if (rescans === undefined)
|
|
349
|
+
return undefined;
|
|
350
|
+
const resolved = new Set([
|
|
351
|
+
...populated,
|
|
352
|
+
...rescans.filter(({ listed }) => listed).map(({ name }) => name),
|
|
353
|
+
]);
|
|
354
|
+
return listings.map((listing) => ({
|
|
355
|
+
...listing,
|
|
356
|
+
empty: listing.empty.filter((name) => !resolved.has(name)),
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
// Runs one read per item in order and stops at the first refusal, so no later read spawns.
|
|
360
|
+
function collectReadings(items, read) {
|
|
361
|
+
return items.reduce((readings, item) => {
|
|
362
|
+
if (readings === undefined)
|
|
367
363
|
return undefined;
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
364
|
+
const reading = read(item);
|
|
365
|
+
return reading === undefined ? undefined : [...readings, reading];
|
|
366
|
+
}, []);
|
|
367
|
+
}
|
|
368
|
+
function isSameUnit(left, right) {
|
|
369
|
+
return (left.config === right.config &&
|
|
370
|
+
left.mode === right.mode &&
|
|
371
|
+
left.root === right.root &&
|
|
372
|
+
left.dir === right.dir);
|
|
373
|
+
}
|
|
374
|
+
function readUnit(unit, named, gates, cache) {
|
|
375
|
+
const reached = gates.units.filter((gate) => isSameUnit(gate, unit));
|
|
376
|
+
const parameters = readUnitArguments(unit);
|
|
377
|
+
const collected = listCollected(parameters, true, cache);
|
|
378
|
+
if (collected === undefined)
|
|
379
|
+
return undefined;
|
|
380
|
+
const scope = named.length === 0
|
|
381
|
+
? undefined
|
|
382
|
+
: listCollected([...parameters, ...named.flatMap((name) => ['--project', name])], false, cache);
|
|
383
|
+
if (named.length > 0 && scope === undefined)
|
|
384
|
+
return undefined;
|
|
385
|
+
const selected = collectReadings(reached, (gate) => {
|
|
386
|
+
const selection = JSON.stringify(gate.args) === JSON.stringify(parameters)
|
|
387
|
+
? collected
|
|
388
|
+
: listCollected(gate.args, gate.full, cache);
|
|
389
|
+
if (selection === undefined) {
|
|
390
|
+
console.error(`discovery: gate ${gate.chain}: vitest list refused ${gate.args.join(' ')}`);
|
|
372
391
|
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
392
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
(named.length === 0 || named.includes(project)))
|
|
406
|
-
empty.push(project);
|
|
393
|
+
return {
|
|
394
|
+
gate,
|
|
395
|
+
identities: new Set(selection.map((entry) => JSON.stringify([
|
|
396
|
+
entry.file,
|
|
397
|
+
entry.projectName,
|
|
398
|
+
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
399
|
+
]))),
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
if (selected === undefined)
|
|
403
|
+
return undefined;
|
|
404
|
+
const readings = collectReadings([...new Set([...reached.flatMap((gate) => gate.projects), ...named])].filter((project) => !project.includes('*') && !project.startsWith('!')), (project) => {
|
|
405
|
+
if (selected.some(({ gate, identities }) => gate.projects.length === 1 && gate.projects[0] === project && identities.size > 0)) {
|
|
406
|
+
return { project, populated: true, empty: false };
|
|
407
407
|
}
|
|
408
|
-
|
|
408
|
+
const selection = listCollected([...parameters, '--project', project], false, cache);
|
|
409
|
+
if (selection === undefined)
|
|
410
|
+
return undefined;
|
|
411
|
+
return {
|
|
412
|
+
project,
|
|
413
|
+
populated: selection.length > 0,
|
|
414
|
+
empty: selection.length === 0 &&
|
|
415
|
+
(reached.some((gate) => gate.projects.includes(project)) || named.includes(project)) &&
|
|
416
|
+
(named.length === 0 || named.includes(project)),
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
if (readings === undefined)
|
|
420
|
+
return undefined;
|
|
421
|
+
return {
|
|
422
|
+
listing: {
|
|
409
423
|
...unit,
|
|
410
424
|
collected: collected
|
|
411
425
|
.filter((entry) => scope === undefined ||
|
|
@@ -418,25 +432,10 @@ function collectListings(config, named, gates) {
|
|
|
418
432
|
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
419
433
|
])))?.gate.chain,
|
|
420
434
|
})),
|
|
421
|
-
empty,
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
}));
|
|
435
|
+
empty: readings.filter(({ empty }) => empty).map(({ project }) => project),
|
|
436
|
+
},
|
|
437
|
+
populated: readings.filter(({ populated }) => populated).map(({ project }) => project),
|
|
438
|
+
};
|
|
440
439
|
}
|
|
441
440
|
function readUnitArguments(unit) {
|
|
442
441
|
return [
|
|
@@ -446,25 +445,30 @@ function readUnitArguments(unit) {
|
|
|
446
445
|
...(unit.dir === undefined ? [] : ['--dir', unit.dir]),
|
|
447
446
|
];
|
|
448
447
|
}
|
|
448
|
+
function identifyCollected(entry) {
|
|
449
|
+
return JSON.stringify([entry.file, entry.projectName, entry.name, entry.line, entry.column]);
|
|
450
|
+
}
|
|
451
|
+
// The same test listed twice in one listing is two occurrences, and the same occurrence listed by
|
|
452
|
+
// two listings is one test: the last listing's fields win, and the gate is the last one any named.
|
|
449
453
|
function mergeCollected(listings) {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
]
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
454
|
+
const keyed = listings.flatMap((listing) => {
|
|
455
|
+
const positions = new Map([...Map.groupBy(listing.collected, identifyCollected).values()].flatMap((group) => group.map((entry, position) => [entry, position + 1])));
|
|
456
|
+
return listing.collected.map((entry) => ({
|
|
457
|
+
entry,
|
|
458
|
+
key: JSON.stringify([identifyCollected(entry), positions.get(entry)]),
|
|
459
|
+
}));
|
|
460
|
+
});
|
|
461
|
+
return [...Map.groupBy(keyed, ({ key }) => key).values()].flatMap((group) => {
|
|
462
|
+
const last = group.at(-1);
|
|
463
|
+
return last === undefined
|
|
464
|
+
? []
|
|
465
|
+
: [
|
|
466
|
+
{
|
|
467
|
+
...last.entry,
|
|
468
|
+
gate: group.findLast(({ entry }) => entry.gate !== undefined)?.entry.gate,
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
});
|
|
468
472
|
}
|
|
469
473
|
function readSnapshot(gates) {
|
|
470
474
|
const bases = [
|
|
@@ -472,17 +476,23 @@ function readSnapshot(gates) {
|
|
|
472
476
|
...gates.units.flatMap((gate) => [gate.root, gate.dir].filter((path) => path !== undefined).map((path) => resolve(path))),
|
|
473
477
|
];
|
|
474
478
|
const roots = new Set(bases.flatMap((base) => [base, join(base, 'tmp/probes')]));
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
files.set(file.path, file.modified);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
479
|
+
// A later listing of the same path replaces the earlier modification time at the earlier position.
|
|
480
|
+
const files = new Map([...roots].flatMap((root) => existsSync(root)
|
|
481
|
+
? listFiles(root)
|
|
482
|
+
.filter((file) => /\.test\.[cm]?[jt]sx?$/u.test(file.path))
|
|
483
|
+
.map((file) => [file.path, file.modified])
|
|
484
|
+
: []));
|
|
484
485
|
return JSON.stringify([...files].sort(([left], [right]) => left.localeCompare(right)));
|
|
485
486
|
}
|
|
487
|
+
function collectStableListings(config, named, gates, attempt = 0) {
|
|
488
|
+
const before = readSnapshot(gates);
|
|
489
|
+
const listings = collectListings(config, named, gates);
|
|
490
|
+
if (before === readSnapshot(gates))
|
|
491
|
+
return listings;
|
|
492
|
+
if (attempt === 1)
|
|
493
|
+
throw new Error('Test files changed during both census attempts');
|
|
494
|
+
return collectStableListings(config, named, gates, attempt + 1);
|
|
495
|
+
}
|
|
486
496
|
function main(argv) {
|
|
487
497
|
const missing = readMissingFlags(argv, ['--config', '--projects']);
|
|
488
498
|
if (missing.length > 0) {
|
|
@@ -501,16 +511,7 @@ function main(argv) {
|
|
|
501
511
|
const canonical = relative(process.cwd(), resolve(config)).split('\\').join('/');
|
|
502
512
|
const gates = readGates(readScripts(), canonical, readCLIOptions());
|
|
503
513
|
const named = (readOption(argv, '--projects') ?? '').split(',').filter((name) => name !== '');
|
|
504
|
-
|
|
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
|
+
const listings = collectStableListings(canonical, named, gates);
|
|
514
515
|
if (listings === undefined) {
|
|
515
516
|
console.error('discovery: vitest list failed');
|
|
516
517
|
return 2;
|