@orkestrel/scaffold 0.0.100 → 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 +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- 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
|
@@ -58,6 +58,12 @@ interface Collected {
|
|
|
58
58
|
readonly gate?: string | undefined
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
interface Selection {
|
|
62
|
+
readonly selected: readonly string[]
|
|
63
|
+
readonly skip: number
|
|
64
|
+
readonly closed: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
interface Gates {
|
|
62
68
|
readonly units: readonly Gate[]
|
|
63
69
|
readonly files: ReadonlyMap<string, string>
|
|
@@ -82,6 +88,17 @@ interface Listing extends Unit {
|
|
|
82
88
|
readonly empty: readonly string[]
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
interface UnitReading {
|
|
92
|
+
readonly listing: Listing
|
|
93
|
+
readonly populated: readonly string[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ProjectReading {
|
|
97
|
+
readonly project: string
|
|
98
|
+
readonly populated: boolean
|
|
99
|
+
readonly empty: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
interface Invocation {
|
|
86
103
|
readonly name: string
|
|
87
104
|
readonly args: readonly string[]
|
|
@@ -108,46 +125,71 @@ function readCLIOptions(): ReadonlyMap<string, Option> {
|
|
|
108
125
|
windowsHide: true,
|
|
109
126
|
})
|
|
110
127
|
if (help.status !== 0) throw new Error(help.stderr || 'Cannot read Vitest CLI options')
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
// A later line that names the same flag replaces the earlier option at the earlier position.
|
|
129
|
+
return new Map(
|
|
130
|
+
help.stdout.split(/\r\n|\n/u).flatMap((line): Array<readonly [string, Option]> => {
|
|
131
|
+
const match = /^\s+(?:(-\w), )?--([\w.-]+)(?: ([<[][^\n]*?[>\]]))?\s{2}/u.exec(line)
|
|
132
|
+
const name = match?.[2]
|
|
133
|
+
if (name === undefined) return []
|
|
134
|
+
const option = { name: name.replace(/^no-/u, ''), argument: match?.[3] }
|
|
135
|
+
return [
|
|
136
|
+
[`--${name}`, option],
|
|
137
|
+
...(name.startsWith('no-') ? [[`--${name.slice(3)}`, option] as const] : []),
|
|
138
|
+
[`--${name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)}`, option],
|
|
139
|
+
...(match?.[1] === undefined ? [] : [[match[1], option] as const]),
|
|
140
|
+
]
|
|
141
|
+
}),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// An option that takes a value consumes the token after it, so that token is never read as a runner.
|
|
146
|
+
function skipRunnerOptions(command: readonly string[], index: number): number {
|
|
147
|
+
const skipped = [...command.entries()].reduce<{
|
|
148
|
+
readonly next: number
|
|
149
|
+
readonly found: number | undefined
|
|
150
|
+
}>(
|
|
151
|
+
(state, [position, option]) => {
|
|
152
|
+
if (state.found !== undefined || position < state.next) return state
|
|
153
|
+
if (!option.startsWith('-')) return { ...state, found: position }
|
|
154
|
+
if (['-e', '--eval', '-p', '--print', '-c', '--check'].includes(option)) {
|
|
155
|
+
return { ...state, found: -1 }
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...state,
|
|
159
|
+
next:
|
|
160
|
+
position +
|
|
161
|
+
([
|
|
162
|
+
'-r',
|
|
163
|
+
'--require',
|
|
164
|
+
'--import',
|
|
165
|
+
'--loader',
|
|
166
|
+
'--experimental-loader',
|
|
167
|
+
'--max-old-space-size',
|
|
168
|
+
'--max_old_space_size',
|
|
169
|
+
'-p',
|
|
170
|
+
'--package',
|
|
171
|
+
].includes(option)
|
|
172
|
+
? 2
|
|
173
|
+
: 1),
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{ next: index, found: undefined },
|
|
177
|
+
)
|
|
178
|
+
return skipped.found ?? skipped.next
|
|
123
179
|
}
|
|
124
180
|
|
|
125
181
|
function findRunner(command: readonly string[]): number {
|
|
126
|
-
let index = 0
|
|
127
182
|
const executable = basename(command[0] ?? '').replace(/\.(?:exe|cmd)$/u, '')
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} else if (executable === 'node' || executable === 'npx') index = 1
|
|
132
|
-
if (index > 0) {
|
|
133
|
-
while (command[index]?.startsWith('-')) {
|
|
134
|
-
const option = command[index]
|
|
135
|
-
if (['-e', '--eval', '-p', '--print', '-c', '--check'].includes(option ?? '')) return -1
|
|
136
|
-
index += [
|
|
137
|
-
'-r',
|
|
138
|
-
'--require',
|
|
139
|
-
'--import',
|
|
140
|
-
'--loader',
|
|
141
|
-
'--experimental-loader',
|
|
142
|
-
'--max-old-space-size',
|
|
143
|
-
'--max_old_space_size',
|
|
144
|
-
'-p',
|
|
145
|
-
'--package',
|
|
146
|
-
].includes(option ?? '')
|
|
183
|
+
const start =
|
|
184
|
+
executable === 'npm'
|
|
185
|
+
? command[1] === 'exec'
|
|
147
186
|
? 2
|
|
148
|
-
: 1
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
: -1
|
|
188
|
+
: executable === 'node' || executable === 'npx'
|
|
189
|
+
? 1
|
|
190
|
+
: 0
|
|
191
|
+
const index = start > 0 ? skipRunnerOptions(command, start) : start
|
|
192
|
+
if (index < 0) return -1
|
|
151
193
|
return ['vitest', 'vitest.mjs', 'vitest.js'].includes(
|
|
152
194
|
basename(command[index] ?? '').replace(/\.cmd$/u, ''),
|
|
153
195
|
)
|
|
@@ -155,55 +197,47 @@ function findRunner(command: readonly string[]): number {
|
|
|
155
197
|
: -1
|
|
156
198
|
}
|
|
157
199
|
|
|
200
|
+
// The first `--` ends the options, so the rest of the arguments pass verbatim. A runner option that
|
|
201
|
+
// takes a value consumes the argument after it, so a step below `skip` reads nothing.
|
|
158
202
|
function readSelection(
|
|
159
203
|
argv: readonly string[],
|
|
160
204
|
options: ReadonlyMap<string, Option>,
|
|
161
205
|
): readonly string[] {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
!next.startsWith('-') &&
|
|
188
|
-
(option.argument !== undefined || next === 'true' || next === 'false')
|
|
189
|
-
)
|
|
190
|
-
index++
|
|
191
|
-
}
|
|
192
|
-
return selected
|
|
206
|
+
return argv.reduce<Selection>(
|
|
207
|
+
(state, token, index) => {
|
|
208
|
+
if (state.closed || index < state.skip) return state
|
|
209
|
+
if (token === '--') {
|
|
210
|
+
return { ...state, selected: state.selected.concat(argv.slice(index)), closed: true }
|
|
211
|
+
}
|
|
212
|
+
if (!token.startsWith('-')) return { ...state, selected: [...state.selected, token] }
|
|
213
|
+
const equals = token.indexOf('=')
|
|
214
|
+
const flag = equals < 0 ? token : token.slice(0, equals)
|
|
215
|
+
const positive = flag.replace(/^--no-/u, '--')
|
|
216
|
+
const option = options.get(positive) ?? options.get(positive.split('.').slice(0, 1).join('.'))
|
|
217
|
+
if (option === undefined || !OMITTED.has(option.name.split('.')[0] ?? '')) {
|
|
218
|
+
return { ...state, selected: [...state.selected, token] }
|
|
219
|
+
}
|
|
220
|
+
const next = argv[index + 1]
|
|
221
|
+
const valued =
|
|
222
|
+
!flag.startsWith('--no-') &&
|
|
223
|
+
equals < 0 &&
|
|
224
|
+
next !== undefined &&
|
|
225
|
+
!next.startsWith('-') &&
|
|
226
|
+
(option.argument !== undefined || next === 'true' || next === 'false')
|
|
227
|
+
return { ...state, skip: index + (valued ? 2 : 1) }
|
|
228
|
+
},
|
|
229
|
+
{ selected: [], skip: 0, closed: false },
|
|
230
|
+
).selected
|
|
193
231
|
}
|
|
194
232
|
|
|
195
233
|
function readArguments(argv: readonly string[], names: readonly string[]): readonly string[] {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const token = argv[index]
|
|
199
|
-
if (token === '--') break
|
|
200
|
-
if (token === undefined) continue
|
|
234
|
+
const end = argv.indexOf('--')
|
|
235
|
+
return (end < 0 ? argv : argv.slice(0, end)).flatMap((token, index) => {
|
|
201
236
|
const equals = token.indexOf('=')
|
|
202
|
-
if (!names.includes(equals < 0 ? token : token.slice(0, equals)))
|
|
237
|
+
if (!names.includes(equals < 0 ? token : token.slice(0, equals))) return []
|
|
203
238
|
const value = equals < 0 ? argv[index + 1] : token.slice(equals + 1)
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
return values
|
|
239
|
+
return value !== undefined && !value.startsWith('-') ? [value] : []
|
|
240
|
+
})
|
|
207
241
|
}
|
|
208
242
|
|
|
209
243
|
function readScripts(): Readonly<Record<string, string>> {
|
|
@@ -211,37 +245,32 @@ function readScripts(): Readonly<Record<string, string>> {
|
|
|
211
245
|
if (typeof parsed !== 'object' || parsed === null) return {}
|
|
212
246
|
const scripts = Object.fromEntries(Object.entries(parsed)).scripts
|
|
213
247
|
if (typeof scripts !== 'object' || scripts === null) return {}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
248
|
+
return Object.fromEntries(
|
|
249
|
+
Object.entries(scripts).flatMap(([key, value]) =>
|
|
250
|
+
typeof value === 'string' ? [[key, value] as const] : [],
|
|
251
|
+
),
|
|
252
|
+
)
|
|
218
253
|
}
|
|
219
254
|
|
|
220
255
|
function readCommands(text: string): ReadonlyArray<readonly string[]> {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
commands.push(command)
|
|
232
|
-
return commands
|
|
256
|
+
const tokens = [...text.matchAll(/(?:[^\s"';&|]+|"[^"]*"|'[^']*')+|[;&|]+/gu)].map(
|
|
257
|
+
(match) => match[0],
|
|
258
|
+
)
|
|
259
|
+
// A separator token ends a command, so the commands are the runs of tokens between the cuts.
|
|
260
|
+
const cuts = tokens.flatMap((token, index) => (/^[;&|]+$/u.test(token) ? [index] : []))
|
|
261
|
+
return [-1, ...cuts].map((cut, at) =>
|
|
262
|
+
tokens.slice(cut + 1, cuts[at] ?? tokens.length).map((token) => token.replace(/["']/gu, '')),
|
|
263
|
+
)
|
|
233
264
|
}
|
|
234
265
|
|
|
235
266
|
function listInvocations(commands: ReadonlyArray<readonly string[]>): readonly Invocation[] {
|
|
236
|
-
|
|
237
|
-
for (const command of commands) {
|
|
267
|
+
return commands.flatMap((command) => {
|
|
238
268
|
const index = command.indexOf('npm')
|
|
239
269
|
const name = command[index + 2]
|
|
240
|
-
if (index < 0 || command[index + 1] !== 'run' || name === undefined)
|
|
270
|
+
if (index < 0 || command[index + 1] !== 'run' || name === undefined) return []
|
|
241
271
|
const separator = command.indexOf('--', index + 3)
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
return invoked
|
|
272
|
+
return [{ name, args: separator < 0 ? [] : command.slice(separator + 1), ancestors: [] }]
|
|
273
|
+
})
|
|
245
274
|
}
|
|
246
275
|
|
|
247
276
|
function readGates(
|
|
@@ -274,12 +303,9 @@ function readGates(
|
|
|
274
303
|
const expanded = script.map((command, index) =>
|
|
275
304
|
index === script.length - 1 ? [...command, ...args] : command,
|
|
276
305
|
)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
ancestors: [...ancestors, name],
|
|
281
|
-
})),
|
|
282
|
-
)
|
|
306
|
+
for (const entry of listInvocations(expanded)) {
|
|
307
|
+
pending.push({ ...entry, ancestors: [...ancestors, name] })
|
|
308
|
+
}
|
|
283
309
|
const gate = root === name ? name : `${root} > ${name}`
|
|
284
310
|
for (const command of expanded) {
|
|
285
311
|
const index = findRunner(command)
|
|
@@ -388,28 +414,30 @@ function listCollected(
|
|
|
388
414
|
}
|
|
389
415
|
const parsed: unknown = JSON.parse(readFileSync(listing, 'utf8'))
|
|
390
416
|
if (!Array.isArray(parsed)) return undefined
|
|
391
|
-
const collected: Collected[]
|
|
392
|
-
|
|
393
|
-
if (typeof entry !== 'object' || entry === null) continue
|
|
417
|
+
const collected = parsed.flatMap((entry): Collected[] => {
|
|
418
|
+
if (typeof entry !== 'object' || entry === null) return []
|
|
394
419
|
const record = Object.fromEntries(Object.entries(entry))
|
|
395
420
|
const location =
|
|
396
421
|
typeof record.location === 'object' && record.location !== null
|
|
397
422
|
? Object.fromEntries(Object.entries(record.location))
|
|
398
423
|
: {}
|
|
399
424
|
if (
|
|
400
|
-
typeof record.file
|
|
401
|
-
(record.projectName
|
|
402
|
-
(
|
|
425
|
+
typeof record.file !== 'string' ||
|
|
426
|
+
(record.projectName !== undefined && typeof record.projectName !== 'string') ||
|
|
427
|
+
(full && typeof record.name !== 'string')
|
|
403
428
|
) {
|
|
404
|
-
|
|
429
|
+
return []
|
|
430
|
+
}
|
|
431
|
+
return [
|
|
432
|
+
{
|
|
405
433
|
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
406
434
|
projectName: record.projectName ?? '',
|
|
407
435
|
name: typeof record.name === 'string' ? record.name : undefined,
|
|
408
436
|
line: typeof location.line === 'number' ? location.line : undefined,
|
|
409
437
|
column: typeof location.column === 'number' ? location.column : undefined,
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
}
|
|
438
|
+
},
|
|
439
|
+
]
|
|
440
|
+
})
|
|
413
441
|
cache.set(key, collected)
|
|
414
442
|
return collected
|
|
415
443
|
} catch (error) {
|
|
@@ -429,12 +457,11 @@ function listTestFiles(directory: string): readonly string[] {
|
|
|
429
457
|
|
|
430
458
|
function countMarkers(file: string): Readonly<Record<string, number>> {
|
|
431
459
|
const text = readFileSync(file, 'utf8')
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return counts
|
|
460
|
+
return Object.fromEntries(
|
|
461
|
+
MARKERS.map((marker) => [marker, text.split(marker).length - 1] as const).filter(
|
|
462
|
+
([, count]) => count > 0,
|
|
463
|
+
),
|
|
464
|
+
)
|
|
438
465
|
}
|
|
439
466
|
|
|
440
467
|
function collectListings(
|
|
@@ -442,93 +469,140 @@ function collectListings(
|
|
|
442
469
|
named: readonly string[],
|
|
443
470
|
gates: Gates,
|
|
444
471
|
): readonly Listing[] | undefined {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
unit.dir === gate.dir,
|
|
472
|
+
const base: Unit = { config, mode: undefined }
|
|
473
|
+
// A gate joins the units unless the base unit or an earlier gate already names the same unit.
|
|
474
|
+
const units: readonly Unit[] = [
|
|
475
|
+
base,
|
|
476
|
+
...gates.units
|
|
477
|
+
.filter(
|
|
478
|
+
(gate, index) =>
|
|
479
|
+
![base, ...gates.units.slice(0, index)].some((unit) => isSameUnit(unit, gate)),
|
|
454
480
|
)
|
|
455
|
-
|
|
456
|
-
units.push({
|
|
481
|
+
.map((gate) => ({
|
|
457
482
|
config: gate.config,
|
|
458
483
|
mode: gate.mode,
|
|
459
484
|
...(gate.root === undefined ? {} : { root: gate.root }),
|
|
460
485
|
...(gate.dir === undefined ? {} : { dir: gate.dir }),
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const listings: Listing[] = []
|
|
465
|
-
const populated = new Set<string>()
|
|
486
|
+
})),
|
|
487
|
+
]
|
|
488
|
+
// A listing is a memo keyed by its arguments: each key is written once, on its first miss.
|
|
466
489
|
const cache = new Map<string, readonly Collected[]>()
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
490
|
+
const readings = collectReadings(units, (unit) => readUnit(unit, named, gates, cache))
|
|
491
|
+
if (readings === undefined) return undefined
|
|
492
|
+
const listings = readings.map(({ listing }) => listing)
|
|
493
|
+
const populated = new Set(readings.flatMap((reading) => reading.populated))
|
|
494
|
+
// A project empty under one unit may still list tests under another, so each unit is asked for it.
|
|
495
|
+
const rescans = collectReadings(
|
|
496
|
+
[...new Set(listings.flatMap((listing) => listing.empty))]
|
|
497
|
+
.filter((name) => !populated.has(name))
|
|
498
|
+
.flatMap((name) => units.map((unit) => ({ name, unit }))),
|
|
499
|
+
({ name, unit }) => {
|
|
500
|
+
const selection = listCollected([...readUnitArguments(unit), '--project', name], false, cache)
|
|
501
|
+
return selection === undefined ? undefined : { name, listed: selection.length > 0 }
|
|
502
|
+
},
|
|
503
|
+
)
|
|
504
|
+
if (rescans === undefined) return undefined
|
|
505
|
+
const resolved = new Set([
|
|
506
|
+
...populated,
|
|
507
|
+
...rescans.filter(({ listed }) => listed).map(({ name }) => name),
|
|
508
|
+
])
|
|
509
|
+
return listings.map((listing) => ({
|
|
510
|
+
...listing,
|
|
511
|
+
empty: listing.empty.filter((name) => !resolved.has(name)),
|
|
512
|
+
}))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Runs one read per item in order and stops at the first refusal, so no later read spawns.
|
|
516
|
+
function collectReadings<Item, Reading>(
|
|
517
|
+
items: readonly Item[],
|
|
518
|
+
read: (item: Item) => Reading | undefined,
|
|
519
|
+
): readonly Reading[] | undefined {
|
|
520
|
+
return items.reduce<readonly Reading[] | undefined>((readings, item) => {
|
|
521
|
+
if (readings === undefined) return undefined
|
|
522
|
+
const reading = read(item)
|
|
523
|
+
return reading === undefined ? undefined : [...readings, reading]
|
|
524
|
+
}, [])
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isSameUnit(left: Unit, right: Unit): boolean {
|
|
528
|
+
return (
|
|
529
|
+
left.config === right.config &&
|
|
530
|
+
left.mode === right.mode &&
|
|
531
|
+
left.root === right.root &&
|
|
532
|
+
left.dir === right.dir
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function readUnit(
|
|
537
|
+
unit: Unit,
|
|
538
|
+
named: readonly string[],
|
|
539
|
+
gates: Gates,
|
|
540
|
+
cache: Map<string, readonly Collected[]>,
|
|
541
|
+
): UnitReading | undefined {
|
|
542
|
+
const reached = gates.units.filter((gate) => isSameUnit(gate, unit))
|
|
543
|
+
const parameters = readUnitArguments(unit)
|
|
544
|
+
const collected = listCollected(parameters, true, cache)
|
|
545
|
+
if (collected === undefined) return undefined
|
|
546
|
+
const scope =
|
|
547
|
+
named.length === 0
|
|
548
|
+
? undefined
|
|
549
|
+
: listCollected(
|
|
550
|
+
[...parameters, ...named.flatMap((name) => ['--project', name])],
|
|
551
|
+
false,
|
|
552
|
+
cache,
|
|
553
|
+
)
|
|
554
|
+
if (named.length > 0 && scope === undefined) return undefined
|
|
555
|
+
const selected = collectReadings(reached, (gate) => {
|
|
556
|
+
const selection =
|
|
557
|
+
JSON.stringify(gate.args) === JSON.stringify(parameters)
|
|
558
|
+
? collected
|
|
559
|
+
: listCollected(gate.args, gate.full, cache)
|
|
560
|
+
if (selection === undefined) {
|
|
561
|
+
console.error(`discovery: gate ${gate.chain}: vitest list refused ${gate.args.join(' ')}`)
|
|
562
|
+
return undefined
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
gate,
|
|
566
|
+
identities: new Set(
|
|
567
|
+
selection.map((entry) =>
|
|
568
|
+
JSON.stringify([
|
|
569
|
+
entry.file,
|
|
570
|
+
entry.projectName,
|
|
571
|
+
...(gate.full ? [entry.name, entry.line, entry.column] : []),
|
|
572
|
+
]),
|
|
507
573
|
),
|
|
508
|
-
|
|
574
|
+
),
|
|
509
575
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
576
|
+
})
|
|
577
|
+
if (selected === undefined) return undefined
|
|
578
|
+
const readings = collectReadings(
|
|
579
|
+
[...new Set([...reached.flatMap((gate) => gate.projects), ...named])].filter(
|
|
580
|
+
(project) => !project.includes('*') && !project.startsWith('!'),
|
|
581
|
+
),
|
|
582
|
+
(project): ProjectReading | undefined => {
|
|
513
583
|
if (
|
|
514
584
|
selected.some(
|
|
515
585
|
({ gate, identities }) =>
|
|
516
586
|
gate.projects.length === 1 && gate.projects[0] === project && identities.size > 0,
|
|
517
587
|
)
|
|
518
588
|
) {
|
|
519
|
-
populated
|
|
520
|
-
continue
|
|
589
|
+
return { project, populated: true, empty: false }
|
|
521
590
|
}
|
|
522
591
|
const selection = listCollected([...parameters, '--project', project], false, cache)
|
|
523
592
|
if (selection === undefined) return undefined
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
593
|
+
return {
|
|
594
|
+
project,
|
|
595
|
+
populated: selection.length > 0,
|
|
596
|
+
empty:
|
|
597
|
+
selection.length === 0 &&
|
|
598
|
+
(reached.some((gate) => gate.projects.includes(project)) || named.includes(project)) &&
|
|
599
|
+
(named.length === 0 || named.includes(project)),
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
)
|
|
603
|
+
if (readings === undefined) return undefined
|
|
604
|
+
return {
|
|
605
|
+
listing: {
|
|
532
606
|
...unit,
|
|
533
607
|
collected: collected
|
|
534
608
|
.filter(
|
|
@@ -550,22 +624,10 @@ function collectListings(
|
|
|
550
624
|
),
|
|
551
625
|
)?.gate.chain,
|
|
552
626
|
})),
|
|
553
|
-
empty,
|
|
554
|
-
}
|
|
627
|
+
empty: readings.filter(({ empty }) => empty).map(({ project }) => project),
|
|
628
|
+
},
|
|
629
|
+
populated: readings.filter(({ populated }) => populated).map(({ project }) => project),
|
|
555
630
|
}
|
|
556
|
-
const candidates = new Set(listings.flatMap((listing) => listing.empty))
|
|
557
|
-
for (const name of candidates) {
|
|
558
|
-
if (populated.has(name)) continue
|
|
559
|
-
for (const unit of units) {
|
|
560
|
-
const selection = listCollected([...readUnitArguments(unit), '--project', name], false, cache)
|
|
561
|
-
if (selection === undefined) return undefined
|
|
562
|
-
if (selection.length > 0) populated.add(name)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return listings.map((listing) => ({
|
|
566
|
-
...listing,
|
|
567
|
-
empty: listing.empty.filter((name) => !populated.has(name)),
|
|
568
|
-
}))
|
|
569
631
|
}
|
|
570
632
|
|
|
571
633
|
function readUnitArguments(unit: Unit): readonly string[] {
|
|
@@ -577,25 +639,35 @@ function readUnitArguments(unit: Unit): readonly string[] {
|
|
|
577
639
|
]
|
|
578
640
|
}
|
|
579
641
|
|
|
642
|
+
function identifyCollected(entry: Collected): string {
|
|
643
|
+
return JSON.stringify([entry.file, entry.projectName, entry.name, entry.line, entry.column])
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// The same test listed twice in one listing is two occurrences, and the same occurrence listed by
|
|
647
|
+
// two listings is one test: the last listing's fields win, and the gate is the last one any named.
|
|
580
648
|
function mergeCollected(listings: readonly Listing[]): readonly Collected[] {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
649
|
+
const keyed = listings.flatMap((listing) => {
|
|
650
|
+
const positions = new Map(
|
|
651
|
+
[...Map.groupBy(listing.collected, identifyCollected).values()].flatMap((group) =>
|
|
652
|
+
group.map((entry, position) => [entry, position + 1] as const),
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
return listing.collected.map((entry) => ({
|
|
656
|
+
entry,
|
|
657
|
+
key: JSON.stringify([identifyCollected(entry), positions.get(entry)]),
|
|
658
|
+
}))
|
|
659
|
+
})
|
|
660
|
+
return [...Map.groupBy(keyed, ({ key }) => key).values()].flatMap((group) => {
|
|
661
|
+
const last = group.at(-1)
|
|
662
|
+
return last === undefined
|
|
663
|
+
? []
|
|
664
|
+
: [
|
|
665
|
+
{
|
|
666
|
+
...last.entry,
|
|
667
|
+
gate: group.findLast(({ entry }) => entry.gate !== undefined)?.entry.gate,
|
|
668
|
+
},
|
|
669
|
+
]
|
|
670
|
+
})
|
|
599
671
|
}
|
|
600
672
|
|
|
601
673
|
function readSnapshot(gates: Gates): string {
|
|
@@ -606,16 +678,32 @@ function readSnapshot(gates: Gates): string {
|
|
|
606
678
|
),
|
|
607
679
|
]
|
|
608
680
|
const roots = new Set(bases.flatMap((base) => [base, join(base, 'tmp/probes')]))
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
681
|
+
// A later listing of the same path replaces the earlier modification time at the earlier position.
|
|
682
|
+
const files = new Map(
|
|
683
|
+
[...roots].flatMap((root) =>
|
|
684
|
+
existsSync(root)
|
|
685
|
+
? listFiles(root)
|
|
686
|
+
.filter((file) => /\.test\.[cm]?[jt]sx?$/u.test(file.path))
|
|
687
|
+
.map((file) => [file.path, file.modified] as const)
|
|
688
|
+
: [],
|
|
689
|
+
),
|
|
690
|
+
)
|
|
616
691
|
return JSON.stringify([...files].sort(([left], [right]) => left.localeCompare(right)))
|
|
617
692
|
}
|
|
618
693
|
|
|
694
|
+
function collectStableListings(
|
|
695
|
+
config: string,
|
|
696
|
+
named: readonly string[],
|
|
697
|
+
gates: Gates,
|
|
698
|
+
attempt = 0,
|
|
699
|
+
): readonly Listing[] | undefined {
|
|
700
|
+
const before = readSnapshot(gates)
|
|
701
|
+
const listings = collectListings(config, named, gates)
|
|
702
|
+
if (before === readSnapshot(gates)) return listings
|
|
703
|
+
if (attempt === 1) throw new Error('Test files changed during both census attempts')
|
|
704
|
+
return collectStableListings(config, named, gates, attempt + 1)
|
|
705
|
+
}
|
|
706
|
+
|
|
619
707
|
function main(argv: readonly string[]): number {
|
|
620
708
|
const missing = readMissingFlags(argv, ['--config', '--projects'])
|
|
621
709
|
if (missing.length > 0) {
|
|
@@ -634,14 +722,7 @@ function main(argv: readonly string[]): number {
|
|
|
634
722
|
const canonical = relative(process.cwd(), resolve(config)).split('\\').join('/')
|
|
635
723
|
const gates = readGates(readScripts(), canonical, readCLIOptions())
|
|
636
724
|
const named = (readOption(argv, '--projects') ?? '').split(',').filter((name) => name !== '')
|
|
637
|
-
|
|
638
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
639
|
-
const before = readSnapshot(gates)
|
|
640
|
-
listings = collectListings(canonical, named, gates)
|
|
641
|
-
if (before === readSnapshot(gates)) break
|
|
642
|
-
listings = undefined
|
|
643
|
-
if (attempt === 1) throw new Error('Test files changed during both census attempts')
|
|
644
|
-
}
|
|
725
|
+
const listings = collectStableListings(canonical, named, gates)
|
|
645
726
|
if (listings === undefined) {
|
|
646
727
|
console.error('discovery: vitest list failed')
|
|
647
728
|
return 2
|