@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.
Files changed (60) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
  7. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
  8. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
  9. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
  10. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
  12. package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
  13. package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
  14. package/dist/bin/main.js +483 -444
  15. package/dist/bin/main.js.map +1 -1
  16. package/dist/host/AGENTS.md +1 -0
  17. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
  20. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
  21. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
  22. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
  23. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
  24. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
  25. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
  26. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
  27. package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
  28. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
  29. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
  30. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
  31. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
  32. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
  33. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
  34. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
  35. package/dist/host/claude/rules/architecture.md +2 -2
  36. package/dist/host/claude/rules/tests.md +8 -2
  37. package/dist/host/claude/rules/typescript.md +130 -0
  38. package/dist/host/claude/rules/workspace.md +36 -12
  39. package/dist/host/configs/helpers.ts +214 -133
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/dotfiles/gitignore +1 -0
  42. package/dist/host/dotfiles/oxlintrc.json +10 -0
  43. package/dist/host/guides/scaffold.md +244 -170
  44. package/dist/host/manifest.json +37 -31
  45. package/dist/host/tests/config.test.ts +1305 -619
  46. package/dist/host/tests/policy.test.ts +19 -0
  47. package/dist/host/tests/setupPolicy.ts +1710 -995
  48. package/dist/src/core/index.cjs +1595 -1686
  49. package/dist/src/core/index.cjs.map +1 -1
  50. package/dist/src/core/index.d.cts +202 -18
  51. package/dist/src/core/index.d.ts +202 -18
  52. package/dist/src/core/index.js +1590 -1687
  53. package/dist/src/core/index.js.map +1 -1
  54. package/dist/src/server/index.cjs +448 -392
  55. package/dist/src/server/index.cjs.map +1 -1
  56. package/dist/src/server/index.d.cts +1182 -1052
  57. package/dist/src/server/index.d.ts +1182 -1052
  58. package/dist/src/server/index.js +447 -395
  59. package/dist/src/server/index.js.map +1 -1
  60. package/package.json +2 -2
@@ -1,13 +1,22 @@
1
1
  // P1: Every checked population must exist and be non-empty; absence fails instead of passing vacuously.
2
2
  // P2: Required items are checked strictly; extra items are ignored before their shape is read.
3
3
 
4
- import type { UserConfig } from 'vite'
4
+ import type { InlineConfig, UserConfig } from 'vite'
5
+ import type {
6
+ PolicyComment,
7
+ PolicyContext,
8
+ PolicyDiagnostic,
9
+ PolicyExpression,
10
+ PolicySourceCode,
11
+ } from '../configs/policy.js'
12
+ import type { PolicyProject } from './setupPolicy.js'
5
13
  import { spawnSync } from 'node:child_process'
6
14
  import {
15
+ closeSync,
7
16
  existsSync,
8
17
  globSync,
9
- lstatSync,
10
18
  mkdirSync,
19
+ openSync,
11
20
  readdirSync,
12
21
  readFileSync,
13
22
  realpathSync,
@@ -16,9 +25,9 @@ import {
16
25
  } from 'node:fs'
17
26
  import { createRequire } from 'node:module'
18
27
  import { tmpdir } from 'node:os'
19
- import { dirname, join, relative, resolve } from 'node:path'
20
- import { fileURLToPath, pathToFileURL } from 'node:url'
21
- import { build, createServer, loadConfigFromFile } from 'vite'
28
+ import { dirname, join, parse, relative, resolve } from 'node:path'
29
+ import { pathToFileURL } from 'node:url'
30
+ import { build, createServer, loadConfigFromFile, parseSync, resolveConfig } from 'vite'
22
31
  import { RuleTester } from 'oxlint/plugins-dev'
23
32
  import * as configHelpers from '../configs/helpers.js'
24
33
  import policyPlugin, {
@@ -49,7 +58,11 @@ import policyPlugin, {
49
58
  blankPolicyText,
50
59
  commentToPolicyParagraph,
51
60
  isPolicyVoiced,
61
+ nodeToPolicyOuter,
52
62
  paragraphToPolicyOpener,
63
+ programToPolicyDocs,
64
+ reportDomain,
65
+ statementToPolicyBindings,
53
66
  stripPolicyCode,
54
67
  textToPolicyHits,
55
68
  } from '../configs/policy.js'
@@ -57,7 +70,9 @@ import configuration, { resolveWorkspacePath } from '../vite.config.js'
57
70
  import * as rootConfiguration from '../vite.config.js'
58
71
  import tsconfig from '../tsconfig.json' with { type: 'json' }
59
72
  import {
73
+ ARTIFACT_IMPORT_CASES,
60
74
  collectSheets,
75
+ collectStudies,
61
76
  collectFrameworks,
62
77
  readConfigRecord,
63
78
  readConfigScript,
@@ -67,25 +82,29 @@ import {
67
82
  SHEET_POLICY_BARREL_PATTERN,
68
83
  SHEET_POLICY_ORDER_PATTERN,
69
84
  readImportDiagnostics,
85
+ createPolicyLazyCases,
70
86
  createPolicyScratch,
87
+ createPolicySparse,
71
88
  inspectPolicyConfiguration,
72
89
  inspectPolicyWiring,
73
90
  normalizePolicyFilename,
91
+ POLICY_EXTRACTOR,
92
+ POLICY_LAZY_SHAPES,
93
+ POLICY_PUBLISHES,
94
+ POLICY_ROOT,
74
95
  } from './setupPolicy.js'
75
96
  import { describe, expect, it } from 'vitest'
76
97
 
77
- const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
78
-
79
98
  describe('selected faces', () => {
80
99
  it('collects each selected sheet proof from the effective project root and rejects a wrong root', async () => {
81
- for (const face of collectSheets(root)) {
82
- const proofs = resolve(root, 'tests/src', face)
100
+ for (const face of collectSheets(POLICY_ROOT)) {
101
+ const proofs = resolve(POLICY_ROOT, 'tests/src', face)
83
102
  if (!existsSync(proofs)) continue
84
- const wrapper = resolve(root, `configs/src/vite.${face}.config.ts`)
103
+ const wrapper = resolve(POLICY_ROOT, `configs/src/vite.${face}.config.ts`)
85
104
  const loaded = await loadConfigFromFile(
86
105
  { command: 'serve', mode: 'test' },
87
106
  wrapper,
88
- root,
107
+ POLICY_ROOT,
89
108
  'silent',
90
109
  )
91
110
  if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
@@ -94,16 +113,19 @@ describe('selected faces', () => {
94
113
  if (include === undefined) throw new Error(`Missing sheet include ${face}`)
95
114
  // Vitest roots a file project at its wrapper directory unless test.root overrides it.
96
115
  const directory = resolve(
97
- root,
116
+ POLICY_ROOT,
98
117
  project.test?.root ?? dirname(wrapper),
99
118
  project.test?.dir || '.',
100
119
  )
101
120
  const collected = globSync(include, { cwd: directory }).map((path) =>
102
121
  resolve(directory, path),
103
122
  )
104
- const expected = [resolve(proofs, 'index.test.ts')]
105
- if (face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts')))
106
- expected.push(resolve(proofs, 'themes/index.test.ts'))
123
+ const expected = [
124
+ resolve(proofs, 'index.test.ts'),
125
+ ...(face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts'))
126
+ ? [resolve(proofs, 'themes/index.test.ts')]
127
+ : []),
128
+ ]
107
129
  expect(collected).toEqual(expect.arrayContaining(expected))
108
130
  const control = globSync(include, { cwd: dirname(wrapper) }).map((path) =>
109
131
  resolve(dirname(wrapper), path),
@@ -150,14 +172,59 @@ describe('selected faces', () => {
150
172
  }
151
173
  })
152
174
 
175
+ it('collects study tests by exact case, nested or direct, and nothing a lookalike supplies', () => {
176
+ // Each lookalike gets its own workspace, because a host that folds case writes
177
+ // `tests/Studies/adapter.test.ts` into an existing `tests/studies` directory.
178
+ const lookalikes = [
179
+ ['no tests directory', []],
180
+ ['a types module alone', ['tests/studies/types.ts']],
181
+ ['the harness module alone', ['tests/setupStudy.ts']],
182
+ ['a root studies directory alone', ['studies/adapter.test.ts']],
183
+ ['a case-folded directory', ['tests/Studies/adapter.test.ts']],
184
+ ['a test of another suffix', ['tests/studies/adapter.test.js']],
185
+ ['a studies entry that is a file', ['tests/studies']],
186
+ [
187
+ 'a test inside a nested node_modules',
188
+ ['tests/studies/fixture/node_modules/kit/index.test.ts'],
189
+ ],
190
+ ['a test inside a .git directory', ['tests/studies/fixture/.git/hooks/index.test.ts']],
191
+ ] as const
192
+ for (const [label, paths] of lookalikes) {
193
+ const fixture = createPolicyScratch({ prefix: 'config-studies-lookalike-' })
194
+ try {
195
+ for (const path of paths) fixture.write(path, '')
196
+ expect({ label, studies: collectStudies(fixture.path) }).toEqual({ label, studies: [] })
197
+ } finally {
198
+ fixture.destroy()
199
+ }
200
+ }
201
+ const fixture = createPolicyScratch({ prefix: 'config-studies-' })
202
+ try {
203
+ for (const path of [
204
+ 'tests/studies/types.ts',
205
+ 'tests/studies/memory/deep/adapter.test.ts',
206
+ 'tests/studies/session.test.ts',
207
+ 'tests/studies/memory/adapter.test.js',
208
+ 'src/core/index.ts',
209
+ ])
210
+ fixture.write(path, '')
211
+ expect(collectStudies(fixture.path)).toEqual([
212
+ 'tests/studies/memory/deep/adapter.test.ts',
213
+ 'tests/studies/session.test.ts',
214
+ ])
215
+ } finally {
216
+ fixture.destroy()
217
+ }
218
+ })
219
+
153
220
  it('loads each selected sheet and framework wrapper and checks its packaging and browser project', async () => {
154
221
  const manifest = readConfigRecord(
155
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
222
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
156
223
  )
157
224
  const scripts = readConfigRecord(manifest.scripts)
158
- const sheets = collectSheets(root)
159
- const frameworks = collectFrameworks(root)
160
- const wrappers = collectFaceWrappers(root)
225
+ const sheets = collectSheets(POLICY_ROOT)
226
+ const frameworks = collectFrameworks(POLICY_ROOT)
227
+ const wrappers = collectFaceWrappers(POLICY_ROOT)
161
228
  const readings: Array<{
162
229
  readonly axis: string
163
230
  readonly face: string
@@ -170,8 +237,8 @@ describe('selected faces', () => {
170
237
  for (const wrapper of wrappers) {
171
238
  const loaded = await loadConfigFromFile(
172
239
  { command: 'build', mode: 'test' },
173
- resolve(root, wrapper),
174
- root,
240
+ resolve(POLICY_ROOT, wrapper),
241
+ POLICY_ROOT,
175
242
  'silent',
176
243
  )
177
244
  if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
@@ -198,7 +265,7 @@ describe('selected faces', () => {
198
265
  for (const { face, wrapper, project } of readings.filter((reading) =>
199
266
  sheets.includes(reading.face),
200
267
  )) {
201
- expect(readFileSync(resolve(root, wrapper), 'utf8')).toContain('sheetProject(')
268
+ expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
202
269
  expect(inspectSheetConfiguration(project)).toEqual([])
203
270
  const exports = readConfigRecord(manifest.exports)
204
271
  expect(exports[`./${face}`]).toBe(`./dist/src/${face}/index.css`)
@@ -221,23 +288,23 @@ describe('selected faces', () => {
221
288
  for (const { wrapper } of readings.filter(
222
289
  (reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
223
290
  )) {
224
- const text = readFileSync(resolve(root, wrapper), 'utf8')
291
+ const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
225
292
  expect(text).toContain('rewriteCoreSpecifier')
226
293
  expect(text).toContain('rewriteBrowserSpecifier')
227
294
  }
228
- if (!existsSync(resolve(root, 'src/styles/themes/index.scss'))) return
295
+ if (!existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))) return
229
296
  const loaded = await loadConfigFromFile(
230
297
  { command: 'build', mode: 'test' },
231
- resolve(root, 'configs/src/vite.themes.config.ts'),
232
- root,
298
+ resolve(POLICY_ROOT, 'configs/src/vite.themes.config.ts'),
299
+ POLICY_ROOT,
233
300
  'silent',
234
301
  )
235
302
  if (loaded === null) throw new Error('Unloaded themes wrapper')
236
303
  expect(loaded.config.build?.outDir).toBe('dist/src/styles/themes')
237
- const barrel = readFileSync(resolve(root, 'src/styles/themes/index.scss'), 'utf8')
304
+ const barrel = readFileSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'), 'utf8')
238
305
  expect(readSheetPrelude(barrel)).toMatch(SHEET_POLICY_BARREL_PATTERN)
239
306
  expect(
240
- readSheetPrelude(readFileSync(resolve(root, 'src/styles/_tokens.scss'), 'utf8')),
307
+ readSheetPrelude(readFileSync(resolve(POLICY_ROOT, 'src/styles/_tokens.scss'), 'utf8')),
241
308
  ).toMatch(SHEET_POLICY_ORDER_PATTERN)
242
309
  const target = sheets.includes('styles') ? 'styles' : 'themes'
243
310
  const command = sheets.includes('styles')
@@ -271,25 +338,29 @@ describe('selected faces', () => {
271
338
  it('resolves showcase and journey modes for every occupied application', async () => {
272
339
  const applications = [
273
340
  'browser',
274
- ...collectFrameworks(root)
341
+ ...collectFrameworks(POLICY_ROOT)
275
342
  .filter((face) => face.startsWith('app/'))
276
343
  .map((face) => face.slice(4)),
277
344
  ]
278
345
  const manifest = readConfigRecord(
279
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
346
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
280
347
  )
281
348
  const scripts = readConfigRecord(manifest.scripts)
282
349
  const factories = readConfigRecord(rootConfiguration)
283
350
  const showcases = ['configs/app/vite.showcase.config.ts'].filter(
284
- (path) => existsSync(resolve(root, path)) || existsSync(resolve(root, 'showcase')),
351
+ (path) =>
352
+ existsSync(resolve(POLICY_ROOT, path)) || existsSync(resolve(POLICY_ROOT, 'showcase')),
285
353
  )
286
354
  for (const showcase of showcases) {
287
- const wrapper = readFileSync(resolve(root, 'configs/app/vite.showcase.config.ts'), 'utf8')
355
+ const wrapper = readFileSync(
356
+ resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'),
357
+ 'utf8',
358
+ )
288
359
  expect(wrapper).toContain('defineConfig(({ mode }) => appShowcase(mode))')
289
360
  expect(wrapper).not.toContain('dist/showcase')
290
- expect(readFileSync(resolve(root, '.prettierignore'), 'utf8').split(/\r\n|\n/u)).toContain(
291
- 'showcase/',
292
- )
361
+ expect(
362
+ readFileSync(resolve(POLICY_ROOT, '.prettierignore'), 'utf8').split(/\r\n|\n/u),
363
+ ).toContain('showcase/')
293
364
  for (const application of applications) {
294
365
  const script = 'build:showcase' + (application === 'browser' ? '' : ':' + application)
295
366
  expect(readConfigScript(scripts, script)).toContain('configs/app/vite.showcase.config.ts')
@@ -301,27 +372,27 @@ describe('selected faces', () => {
301
372
  }
302
373
  const loaded = await loadConfigFromFile(
303
374
  { command: 'build', mode: application },
304
- resolve(root, showcase),
305
- root,
375
+ resolve(POLICY_ROOT, showcase),
376
+ POLICY_ROOT,
306
377
  'silent',
307
378
  )
308
379
  if (loaded === null) throw new Error('Unloaded showcase wrapper')
309
380
  const output = loaded.config.build?.outDir
310
381
  if (output === undefined) throw new Error('Missing showcase output')
311
- expect(relative(root, output)).toBe('showcase')
382
+ expect(relative(POLICY_ROOT, output)).toBe('showcase')
312
383
  expect(() =>
313
- expect(relative(root, resolve(root, 'dist/showcase'))).toBe('showcase'),
384
+ expect(relative(POLICY_ROOT, resolve(POLICY_ROOT, 'dist/showcase'))).toBe('showcase'),
314
385
  ).toThrow('expected')
315
386
  expect(loaded.config.build?.emptyOutDir).toBe(false)
316
387
  }
317
388
  expect(scripts.show).toBeUndefined()
318
389
  }
319
- if (!existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) return
390
+ if (!existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))) return
320
391
  for (const application of applications) {
321
392
  const loaded = await loadConfigFromFile(
322
393
  { command: 'serve', mode: application },
323
- resolve(root, 'configs/app/vite.journey.config.ts'),
324
- root,
394
+ resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'),
395
+ POLICY_ROOT,
325
396
  'silent',
326
397
  )
327
398
  if (
@@ -330,8 +401,8 @@ describe('selected faces', () => {
330
401
  loaded.config.test.projects.length === 0
331
402
  )
332
403
  throw new Error('Missing journey projects')
333
- const variants = new Set<string>()
334
- const declared = new Set<string>()
404
+ // Each awaited factory appends the variant it provides and the variants it declares.
405
+ const provided: Array<{ readonly variant: string; readonly names: readonly string[] }> = []
335
406
  for (const factory of loaded.config.test.projects) {
336
407
  if (typeof factory !== 'function') throw new Error('Missing journey factory')
337
408
  const project = readConfigRecord(
@@ -343,12 +414,14 @@ describe('selected faces', () => {
343
414
  const provide = readConfigRecord(test.provide)
344
415
  if (typeof provide.variant !== 'string' || !Array.isArray(provide.variants))
345
416
  throw new Error('Missing journey variant values')
346
- variants.add(provide.variant)
347
- for (const value of provide.variants) {
348
- const variant = readConfigRecord(value)
349
- if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
350
- declared.add(variant.name)
351
- }
417
+ provided.push({
418
+ variant: provide.variant,
419
+ names: provide.variants.map((value: unknown) => {
420
+ const variant = readConfigRecord(value)
421
+ if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
422
+ return variant.name
423
+ }),
424
+ })
352
425
  const selected = provide.variants
353
426
  .map(readConfigRecord)
354
427
  .find((variant) => variant.name === provide.variant)
@@ -370,7 +443,51 @@ describe('selected faces', () => {
370
443
  expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
371
444
  )
372
445
  }
373
- expect(variants).toEqual(declared)
446
+ expect(new Set(provided.map(({ variant }) => variant))).toEqual(
447
+ new Set(provided.flatMap(({ names }) => names)),
448
+ )
449
+ }
450
+ // The runner loads the wrapper beside this file's own root configuration, so every journey
451
+ // project's record and the commands module's export are one object graph. A wrapper can
452
+ // add commands of its own; it keeps every command the module registers.
453
+ const registered = (
454
+ await Promise.all(
455
+ Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
456
+ )
457
+ ).map((module) => readConfigRecord(readConfigRecord(module).commands))
458
+ const wrappers = await Promise.all(
459
+ Object.values(
460
+ import.meta.glob<unknown>('../configs/app/vite.journey.config.ts', { import: 'default' }),
461
+ ).map((load) => load()),
462
+ )
463
+ expect(wrappers).toHaveLength(1)
464
+ for (const wrapper of wrappers) {
465
+ if (typeof wrapper !== 'function') throw new Error('Missing journey wrapper factory')
466
+ for (const application of applications) {
467
+ const composed = readConfigRecord(
468
+ Reflect.apply(wrapper, undefined, [{ command: 'serve', mode: application }]),
469
+ )
470
+ const projects = readConfigRecord(composed.test).projects
471
+ if (!Array.isArray(projects) || projects.length === 0)
472
+ throw new Error('Missing journey projects')
473
+ for (const project of projects) {
474
+ if (typeof project !== 'function') throw new Error('Missing journey factory')
475
+ const held = readConfigRecord(
476
+ readConfigRecord(
477
+ readConfigRecord(
478
+ readConfigRecord(
479
+ await Reflect.apply(project, undefined, [
480
+ { command: 'serve', mode: application },
481
+ ]),
482
+ ).test,
483
+ ).browser,
484
+ ).commands,
485
+ )
486
+ for (const commands of registered) {
487
+ for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
488
+ }
489
+ }
490
+ }
374
491
  }
375
492
  const factory = factories.appJourney
376
493
  if (typeof factory !== 'function') throw new Error('Missing application journey')
@@ -383,15 +500,56 @@ describe('selected faces', () => {
383
500
  ).toThrow('not declared')
384
501
  })
385
502
 
386
- it('collects both browser setup proofs and optimizes every selected browser factory', () => {
503
+ it('collects both browser setup proofs and optimizes every selected browser factory', async () => {
387
504
  const factories = readConfigRecord(rootConfiguration)
388
505
  const manifest = readConfigRecord(
389
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
506
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
390
507
  )
391
508
  const dependencies = {
392
509
  ...readConfigRecord(manifest.dependencies ?? {}),
393
510
  ...readConfigRecord(manifest.devDependencies ?? {}),
394
511
  }
512
+ // The runner loads the module this file's root configuration imports, so each factory's
513
+ // record and the module's export are one object graph.
514
+ const registered = (
515
+ await Promise.all(
516
+ Object.values(import.meta.glob<unknown>('../configs/commands.ts')).map((load) => load()),
517
+ )
518
+ ).map((module) => readConfigRecord(readConfigRecord(module).commands))
519
+ // The configuration bundle carries `configs/commands.ts` into every load, builds included,
520
+ // and leaves an alias it cannot resolve as a bare package import. The module therefore
521
+ // imports only a `node:` module or a declared package, and loads workspace source inside a
522
+ // command. The control carries both refused forms.
523
+ const control =
524
+ "import { GREETING } from '@src/core'\nexport * from '../src/server/index.js'\nimport { join } from 'node:path'\nimport type { BrowserConfigOptions } from 'vitest/node'\n"
525
+ for (const [path, text, refused] of [
526
+ ['control', control, ['@src/core', '../src/server/index.js']],
527
+ ...['configs/commands.ts']
528
+ .filter((module) => existsSync(resolve(POLICY_ROOT, module)))
529
+ .map((module) => [module, readFileSync(resolve(POLICY_ROOT, module), 'utf8'), []] as const),
530
+ ] as const) {
531
+ const specifiers = parseSync('commands.ts', text).program.body.flatMap((statement) =>
532
+ statement.type === 'ImportDeclaration' ||
533
+ statement.type === 'ExportAllDeclaration' ||
534
+ (statement.type === 'ExportNamedDeclaration' && statement.source !== null)
535
+ ? [statement.source?.value ?? '']
536
+ : [],
537
+ )
538
+ expect({
539
+ path,
540
+ refused: specifiers.filter(
541
+ (specifier) =>
542
+ !specifier.startsWith('node:') &&
543
+ !Object.hasOwn(
544
+ dependencies,
545
+ specifier
546
+ .split('/')
547
+ .slice(0, specifier.startsWith('@') ? 2 : 1)
548
+ .join('/'),
549
+ ),
550
+ ),
551
+ }).toStrictEqual({ path, refused })
552
+ }
395
553
  for (const name of [
396
554
  'srcBrowser',
397
555
  'appBrowser',
@@ -422,9 +580,13 @@ describe('selected faces', () => {
422
580
  ...(dependencies['@orkestrel/contract'] === undefined ? [] : ['@orkestrel/contract']),
423
581
  ]),
424
582
  )
583
+ for (const commands of registered) {
584
+ const held = readConfigRecord(readConfigRecord(test.browser).commands)
585
+ for (const [key, command] of Object.entries(commands)) expect(held[key]).toBe(command)
586
+ }
425
587
  }
426
588
  for (const proof of ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts'].filter((path) =>
427
- existsSync(resolve(root, path)),
589
+ existsSync(resolve(POLICY_ROOT, path)),
428
590
  )) {
429
591
  const factory = factories.setupBrowser
430
592
  if (typeof factory !== 'function') throw new Error('Missing browser setup project')
@@ -443,59 +605,32 @@ describe('selected faces', () => {
443
605
  })
444
606
  })
445
607
 
446
- // The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
447
- // a physical directory rather than from a `src` entry of any shape. A regular file, an empty
448
- // directory, a directory holding no recognized environment, a symbolic link, and an entry this host
449
- // refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
450
- // put every one of them under the face proof. The inspection is caught rather than excused by
451
- // option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
452
- // throw on this line is a collection error that takes this whole file down in that target rather
453
- // than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
454
- // false for every inspection error, and this reads them the same way. This is the derivation
455
- // `targetToEnvironments` uses, which filters the environment names by a contained physical
456
- // directory, and it answers the same question `blueprintToScripts`
457
- // answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
458
- // no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
459
- // proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
460
- // axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
461
- // fail the proof. The manifest cases read the same fact from their own `private` flag and name it
462
- // `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
463
- // workspace whose manifest and shape disagree fails one of the two rather than neither.
464
- const publishes = ['core', 'browser', 'server'].some((environment) => {
465
- try {
466
- const entry = lstatSync(resolve(root, 'src', environment))
467
- return entry.isDirectory() && !entry.isSymbolicLink()
468
- } catch {
469
- return false
470
- }
471
- })
472
-
473
- // A declaration roll-up loads the extractor package, which only a workspace publishing source from
474
- // `src` installs. The resolution below is the mechanism its proof is conditioned on.
475
- let extractorPath: string | undefined
476
- try {
477
- extractorPath = createRequire(import.meta.url).resolve('@microsoft/api-extractor')
478
- } catch {
479
- extractorPath = undefined
480
- }
481
-
482
608
  describe('root configuration', () => {
483
609
  it('resolves every declared alias to its real entry', () => {
484
610
  const aliases = configuration.resolve?.alias
485
611
  if (typeof aliases !== 'object' || aliases === null || Array.isArray(aliases)) {
486
612
  throw new Error('The root configuration carries no alias record')
487
613
  }
488
- const required = new Map<string, string>()
489
- for (const axis of ['src', 'app']) {
490
- for (const environment of ['core', 'browser', 'server']) {
491
- const path = `${axis}/${environment}/index.ts`
492
- if (existsSync(resolve(root, path))) required.set(`@${axis}/${environment}`, path)
493
- }
494
- }
495
- for (const face of collectSheets(root)) required.set(`@src/${face}`, `src/${face}/index.ts`)
496
- for (const face of collectFrameworks(root)) required.set(`@${face}`, `${face}/index.ts`)
614
+ const required = new Map<string, string>([
615
+ ...['src', 'app'].flatMap((axis) =>
616
+ ['core', 'browser', 'server'].flatMap((environment): Array<[string, string]> => {
617
+ const path = `${axis}/${environment}/index.ts`
618
+ return existsSync(resolve(POLICY_ROOT, path)) ? [[`@${axis}/${environment}`, path]] : []
619
+ }),
620
+ ),
621
+ ...collectSheets(POLICY_ROOT).map((face): [string, string] => [
622
+ `@src/${face}`,
623
+ `src/${face}/index.ts`,
624
+ ]),
625
+ ...collectFrameworks(POLICY_ROOT).map((face): [string, string] => [
626
+ `@${face}`,
627
+ `${face}/index.ts`,
628
+ ]),
629
+ ])
497
630
  const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
498
- expect(required.size > 0 || existsSync(resolve(root, 'src/styles/themes/sheet.ts'))).toBe(true)
631
+ expect(
632
+ required.size > 0 || existsSync(resolve(POLICY_ROOT, 'src/styles/themes/sheet.ts')),
633
+ ).toBe(true)
499
634
  if (required.size === 0) {
500
635
  if (declared.size !== 0 || Object.keys(aliases).length !== 0)
501
636
  throw new Error('The themes-only workspace declares an unexpected alias')
@@ -514,138 +649,146 @@ describe('root configuration', () => {
514
649
  if (path === undefined) throw new Error(`${key} carries no target`)
515
650
  const target = resolveWorkspacePath(path)
516
651
  expect(existsSync(target)).toBe(true)
517
- expect(target).toBe(resolve(root, expected))
652
+ expect(target).toBe(resolve(POLICY_ROOT, expected))
518
653
  expect(Object.getOwnPropertyDescriptor(aliases, key)?.value).toBe(target)
519
654
  }
520
655
  })
521
656
 
522
657
  it('registers every workspace project with its fixed include and setup files', () => {
523
- const expected = new Map<
524
- string,
525
- {
526
- readonly benchmark?: readonly string[]
527
- readonly include: string | readonly string[]
528
- readonly parallel?: boolean
529
- readonly pool?: string
530
- readonly setup: readonly string[]
531
- }
532
- >()
533
- if (existsSync(resolve(root, 'src/core'))) {
534
- expected.set('src:core', {
535
- include: 'tests/src/core/**/*.test.ts',
536
- setup: ['./tests/setup.ts'],
537
- })
538
- }
539
- if (existsSync(resolve(root, 'src/browser'))) {
540
- expected.set('src:browser', {
541
- include: 'tests/src/browser/**/*.test.ts',
542
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
543
- })
544
- }
545
- if (existsSync(resolve(root, 'src/server'))) {
546
- expected.set('src:server', {
547
- include: 'tests/src/server/**/*.test.ts',
548
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
549
- })
550
- }
551
- if (existsSync(resolve(root, 'src/bin'))) {
552
- expected.set('src:bin', {
553
- include: 'tests/src/bin/**/*.test.ts',
554
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
555
- })
556
- }
557
- if (existsSync(resolve(root, 'app/core'))) {
558
- expected.set('app:core', {
559
- include: 'tests/app/core/**/*.test.ts',
560
- setup: ['./tests/setup.ts'],
561
- })
562
- }
563
- if (existsSync(resolve(root, 'app/browser'))) {
564
- expected.set('app:browser', {
565
- include: 'tests/app/browser/**/*.test.ts',
566
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
567
- })
568
- }
569
- if (existsSync(resolve(root, 'app/server'))) {
570
- expected.set('app:server', {
571
- include: 'tests/app/server/**/*.test.ts',
572
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
573
- })
574
- }
575
- // The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
576
- // project it registers runs the mirrored proofs under `tests/agents/`.
577
- if (existsSync(resolve(root, 'configs/agents/tsconfig.skills.json'))) {
578
- expected.set('skills', {
579
- include: 'tests/agents/**/*.test.ts',
580
- setup: ['./tests/setup.ts'],
581
- })
582
- }
583
- for (const label of [
584
- 'policy',
585
- 'config',
586
- 'guides',
587
- 'conformance',
588
- 'distribution',
589
- 'integration',
590
- ]) {
591
- if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
592
- const setup = ['./tests/setup.ts']
593
- if (label === 'conformance') setup.push('./tests/setupServer.ts')
594
- if (
595
- label === 'integration' &&
596
- (collectSheets(root).length > 0 ||
597
- existsSync(resolve(root, 'src/styles/themes/index.scss')))
598
- )
599
- setup.push('./tests/setupBrowser.ts', './tests/setupStyles.ts')
600
- expected.set(label, {
601
- include: `tests/${label}.test.ts`,
602
- setup,
603
- })
604
- }
605
658
  const browserProofs = ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']
606
- const setupProofs = globSync('tests/setup*.test.ts', { cwd: root })
659
+ const setupProofs = globSync('tests/setup*.test.ts', { cwd: POLICY_ROOT })
607
660
  .map((path) => path.replaceAll('\\', '/'))
608
661
  .filter((path) => /^tests\/setup[^/]*\.test\.ts$/u.test(path))
609
- if (setupProofs.some((path) => !browserProofs.includes(path))) {
610
- expected.set('setup', {
611
- include: 'tests/setup*.test.ts',
612
- setup: ['./tests/setup.ts'],
613
- })
614
- }
615
- if (setupProofs.some((path) => browserProofs.includes(path))) {
616
- expected.set('setup:browser', {
617
- include: browserProofs,
618
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
619
- })
620
- }
621
- // The live-service project covers a directory rather than one proof, so its
622
- // readiness module is the fact that selects it. A suite beneath
623
- // `tests/service` with no setup module is a project nothing configures.
624
- if (existsSync(resolve(root, 'tests/setupService.ts'))) {
625
- expected.set('service', {
626
- include: 'tests/service/**/*.test.ts',
627
- setup: ['./tests/setup.ts', './tests/setupService.ts'],
628
- })
629
- }
630
- expected.set('probe', {
631
- benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
632
- include: 'tmp/probes/**/*.test.ts',
633
- parallel: false,
634
- pool: 'threads',
635
- setup: ['./tests/setup.ts'],
636
- })
637
- // A row that is a configuration rather than a factory. Every generated
638
- // workspace registers the factory itself, so this shape is required by the
639
- // expectation rather than observed: the proof exercises that resolution wherever it runs
640
- // instead of only where a hand-written configuration happens to produce it.
641
- expected.set('concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] })
662
+ const expected = new Map<string, PolicyProject>([
663
+ // Each row is a project label, the path whose presence selects it, its include, and its setup.
664
+ ...(
665
+ [
666
+ ['src:core', 'src/core', 'tests/src/core/**/*.test.ts', ['./tests/setup.ts']],
667
+ [
668
+ 'src:browser',
669
+ 'src/browser',
670
+ 'tests/src/browser/**/*.test.ts',
671
+ ['./tests/setup.ts', './tests/setupBrowser.ts'],
672
+ ],
673
+ [
674
+ 'src:server',
675
+ 'src/server',
676
+ 'tests/src/server/**/*.test.ts',
677
+ ['./tests/setup.ts', './tests/setupServer.ts'],
678
+ ],
679
+ [
680
+ 'src:bin',
681
+ 'src/bin',
682
+ 'tests/src/bin/**/*.test.ts',
683
+ ['./tests/setup.ts', './tests/setupServer.ts'],
684
+ ],
685
+ ['app:core', 'app/core', 'tests/app/core/**/*.test.ts', ['./tests/setup.ts']],
686
+ [
687
+ 'app:browser',
688
+ 'app/browser',
689
+ 'tests/app/browser/**/*.test.ts',
690
+ ['./tests/setup.ts', './tests/setupBrowser.ts'],
691
+ ],
692
+ [
693
+ 'app:server',
694
+ 'app/server',
695
+ 'tests/app/server/**/*.test.ts',
696
+ ['./tests/setup.ts', './tests/setupServer.ts'],
697
+ ],
698
+ // The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
699
+ // project it registers runs the mirrored proofs under `tests/agents/`.
700
+ [
701
+ 'skills',
702
+ 'configs/agents/tsconfig.skills.json',
703
+ 'tests/agents/**/*.test.ts',
704
+ ['./tests/setup.ts'],
705
+ ],
706
+ ] as const
707
+ )
708
+ .filter(([, path]) => existsSync(resolve(POLICY_ROOT, path)))
709
+ .map(([label, , include, setup]): [string, PolicyProject] => [label, { include, setup }]),
710
+ ...['policy', 'config', 'guides', 'conformance', 'distribution', 'integration']
711
+ .filter((label) => existsSync(resolve(POLICY_ROOT, `tests/${label}.test.ts`)))
712
+ .map((label): [string, PolicyProject] => [
713
+ label,
714
+ {
715
+ include: `tests/${label}.test.ts`,
716
+ setup: [
717
+ './tests/setup.ts',
718
+ ...(label === 'conformance' ? ['./tests/setupServer.ts'] : []),
719
+ ...(label === 'integration' &&
720
+ (collectSheets(POLICY_ROOT).length > 0 ||
721
+ existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss')))
722
+ ? ['./tests/setupBrowser.ts', './tests/setupStyles.ts']
723
+ : []),
724
+ ],
725
+ },
726
+ ]),
727
+ ...(setupProofs.some((path) => !browserProofs.includes(path))
728
+ ? [
729
+ ['setup', { include: 'tests/setup*.test.ts', setup: ['./tests/setup.ts'] }] satisfies [
730
+ string,
731
+ PolicyProject,
732
+ ],
733
+ ]
734
+ : []),
735
+ ...(setupProofs.some((path) => browserProofs.includes(path))
736
+ ? [
737
+ [
738
+ 'setup:browser',
739
+ { include: browserProofs, setup: ['./tests/setup.ts', './tests/setupBrowser.ts'] },
740
+ ] satisfies [string, PolicyProject],
741
+ ]
742
+ : []),
743
+ // The live-service project covers a directory rather than one proof, so its
744
+ // readiness module is the fact that selects it. A suite beneath
745
+ // `tests/service` with no setup module is a project nothing configures.
746
+ ...(existsSync(resolve(POLICY_ROOT, 'tests/setupService.ts'))
747
+ ? [
748
+ [
749
+ 'service',
750
+ {
751
+ include: 'tests/service/**/*.test.ts',
752
+ setup: ['./tests/setup.ts', './tests/setupService.ts'],
753
+ },
754
+ ] satisfies [string, PolicyProject],
755
+ ]
756
+ : []),
757
+ // The study project covers a directory too, and the exact-case test files beneath
758
+ // `tests/studies` select it, outside any `node_modules` or `.git` directory, which Vitest's
759
+ // default exclude never collects. `tests/setupStudy.ts` is a harness module those tests import
760
+ // and no project loads it as a setup file, so the setup list names `tests/setup.ts` alone.
761
+ ...(collectStudies(POLICY_ROOT).length > 0
762
+ ? [
763
+ [
764
+ 'studies',
765
+ { include: 'tests/studies/**/*.test.ts', setup: ['./tests/setup.ts'] },
766
+ ] satisfies [string, PolicyProject],
767
+ ]
768
+ : []),
769
+ [
770
+ 'probe',
771
+ {
772
+ benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
773
+ include: 'tmp/probes/**/*.test.ts',
774
+ parallel: false,
775
+ pool: 'threads',
776
+ setup: ['./tests/setup.ts'],
777
+ },
778
+ ],
779
+ // A row that is a configuration rather than a factory. Every generated
780
+ // workspace registers the factory itself, so this shape is required by the
781
+ // expectation rather than observed: the proof exercises that resolution wherever it runs
782
+ // instead of only where a hand-written configuration happens to produce it.
783
+ ['concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] }],
784
+ ])
642
785
 
643
786
  const projects = configuration.test?.projects
644
787
  if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
645
- let extraLoaded = false
788
+ const extraLoads: string[] = []
646
789
  const control = Object.defineProperty(
647
790
  () => {
648
- extraLoaded = true
791
+ extraLoads.push('control')
649
792
  return {
650
793
  test: {
651
794
  name: { label: 'control' },
@@ -665,109 +808,107 @@ describe('root configuration', () => {
665
808
  },
666
809
  }
667
810
  const controlled = projects.concat(control, concrete)
668
- const configured = new Map<
669
- string,
670
- {
671
- readonly benchmark?: readonly string[]
672
- readonly include: string | readonly string[]
673
- readonly parallel?: boolean
674
- readonly pool?: string
675
- readonly setup: readonly string[]
676
- }
677
- >()
678
- for (const [requiredLabel] of expected) {
679
- const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
680
- letter.toUpperCase(),
681
- )
682
- // A row is either the factory named for the project or the configuration
683
- // that project resolves to, and a required project is found as whichever
684
- // it is. Only the required row is read, so an extra factory is still
685
- // selected by name and never called.
686
- const row = controlled.find((candidate) => {
687
- if (typeof candidate === 'function') return candidate.name === factoryName
688
- if (typeof candidate !== 'object' || candidate === null) return false
689
- const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
690
- if (typeof block !== 'object' || block === null) return false
691
- const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
692
- if (typeof named !== 'object' || named === null) return false
693
- return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
694
- })
695
- if (row === undefined) {
696
- throw new Error(`${requiredLabel} has no project factory or configuration`)
697
- }
698
- const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
699
- if (typeof project !== 'object' || project === null) {
700
- throw new Error('A project factory returned no configuration')
701
- }
702
- const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
703
- if (typeof test !== 'object' || test === null) {
704
- throw new Error('A project configuration carries no test block')
705
- }
706
- const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
707
- const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
708
- const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
709
- const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
710
- const label =
711
- typeof name === 'object' && name !== null
712
- ? Object.getOwnPropertyDescriptor(name, 'label')?.value
713
- : undefined
714
- if (
715
- typeof label !== 'string' ||
716
- !Array.isArray(include) ||
717
- typeof include[0] !== 'string' ||
718
- !Array.isArray(setup) ||
719
- !setup.every((path) => typeof path === 'string')
720
- ) {
721
- throw new Error('A project does not expose one include and its setup files')
722
- }
723
- const effective = include.filter(
724
- (path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
725
- )
726
- if (
727
- effective.length !== (label === 'setup:browser' ? 2 : 1) ||
728
- typeof effective[0] !== 'string'
729
- ) {
730
- throw new Error(`${label} does not resolve to its required includes`)
731
- }
732
- for (const required of label === 'setup' ? browserProofs : [])
733
- expect(exclude).toContain(required)
734
- if (label === 'probe') {
735
- const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
736
- const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
737
- const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
738
- if (typeof benchmark !== 'object' || benchmark === null) {
739
- throw new Error('The probe project carries no benchmark block')
811
+ const configured = new Map(
812
+ [...expected].map(([requiredLabel]): [string, PolicyProject] => {
813
+ const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
814
+ letter.toUpperCase(),
815
+ )
816
+ // A row is either the factory named for the project or the configuration
817
+ // that project resolves to, and a required project is found as whichever
818
+ // it is. Only the required row is read, so an extra factory is still
819
+ // selected by name and never called.
820
+ const row = controlled.find((candidate) => {
821
+ if (typeof candidate === 'function') return candidate.name === factoryName
822
+ if (typeof candidate !== 'object' || candidate === null) return false
823
+ const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
824
+ if (typeof block !== 'object' || block === null) return false
825
+ const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
826
+ if (typeof named !== 'object' || named === null) return false
827
+ return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
828
+ })
829
+ if (row === undefined) {
830
+ throw new Error(`${requiredLabel} has no project factory or configuration`)
740
831
  }
741
- const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
742
- benchmark,
743
- 'include',
744
- )?.value
832
+ const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
833
+ if (typeof project !== 'object' || project === null) {
834
+ throw new Error('A project factory returned no configuration')
835
+ }
836
+ const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
837
+ if (typeof test !== 'object' || test === null) {
838
+ throw new Error('A project configuration carries no test block')
839
+ }
840
+ const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
841
+ const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
842
+ const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
843
+ const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
844
+ const label =
845
+ typeof name === 'object' && name !== null
846
+ ? Object.getOwnPropertyDescriptor(name, 'label')?.value
847
+ : undefined
745
848
  if (
746
- !Array.isArray(benchmarkInclude) ||
747
- !benchmarkInclude.every((path) => typeof path === 'string') ||
748
- typeof parallel !== 'boolean' ||
749
- typeof pool !== 'string'
849
+ typeof label !== 'string' ||
850
+ !Array.isArray(include) ||
851
+ typeof include[0] !== 'string' ||
852
+ !Array.isArray(setup) ||
853
+ !setup.every((path) => typeof path === 'string')
750
854
  ) {
751
- throw new Error('The probe project carries an invalid benchmark configuration')
855
+ throw new Error('A project does not expose one include and its setup files')
752
856
  }
753
- configured.set(label, {
754
- benchmark: benchmarkInclude,
755
- include: effective[0],
756
- parallel,
757
- pool,
758
- setup: [...new Set(setup)],
759
- })
760
- continue
761
- }
762
- configured.set(label, {
763
- include: label === 'setup:browser' ? effective : effective[0],
764
- setup: [...new Set(setup)],
765
- })
766
- }
857
+ const effective = include.filter(
858
+ (path) =>
859
+ typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
860
+ )
861
+ if (
862
+ effective.length !== (label === 'setup:browser' ? 2 : 1) ||
863
+ typeof effective[0] !== 'string'
864
+ ) {
865
+ throw new Error(`${label} does not resolve to its required includes`)
866
+ }
867
+ for (const required of label === 'setup' ? browserProofs : [])
868
+ expect(exclude).toContain(required)
869
+ if (label === 'probe') {
870
+ const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
871
+ const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
872
+ const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
873
+ if (typeof benchmark !== 'object' || benchmark === null) {
874
+ throw new Error('The probe project carries no benchmark block')
875
+ }
876
+ const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
877
+ benchmark,
878
+ 'include',
879
+ )?.value
880
+ if (
881
+ !Array.isArray(benchmarkInclude) ||
882
+ !benchmarkInclude.every((path) => typeof path === 'string') ||
883
+ typeof parallel !== 'boolean' ||
884
+ typeof pool !== 'string'
885
+ ) {
886
+ throw new Error('The probe project carries an invalid benchmark configuration')
887
+ }
888
+ return [
889
+ label,
890
+ {
891
+ benchmark: benchmarkInclude,
892
+ include: effective[0],
893
+ parallel,
894
+ pool,
895
+ setup: [...new Set(setup)],
896
+ },
897
+ ]
898
+ }
899
+ return [
900
+ label,
901
+ {
902
+ include: label === 'setup:browser' ? effective : effective[0],
903
+ setup: [...new Set(setup)],
904
+ },
905
+ ]
906
+ }),
907
+ )
767
908
 
768
909
  // Required projects come from present source and test paths. Each factory is selected by name
769
910
  // before its result is read. Extra factories are ignored without validating their result shape.
770
- expect(extraLoaded).toBe(false)
911
+ expect(extraLoads).toStrictEqual([])
771
912
  for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
772
913
  for (const label of ['setup', 'setup:browser']) {
773
914
  const name = label === 'setup' ? 'setup' : 'setupBrowser'
@@ -823,16 +964,20 @@ describe('root configuration', () => {
823
964
  value: 'spreading',
824
965
  })
825
966
  const wrappers = projects.filter((entry) => typeof entry === 'string')
826
- const sheets = collectSheets(root)
827
- const expectedWrappers = sheets.map((face) => `./configs/src/vite.${face}.config.ts`)
828
- if (!sheets.includes('styles') && existsSync(resolve(root, 'src/styles/themes/index.scss')))
829
- expectedWrappers.push('./configs/src/vite.themes.config.ts')
967
+ const sheets = collectSheets(POLICY_ROOT)
968
+ const expectedWrappers = [
969
+ ...sheets.map((face) => `./configs/src/vite.${face}.config.ts`),
970
+ ...(!sheets.includes('styles') &&
971
+ existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))
972
+ ? ['./configs/src/vite.themes.config.ts']
973
+ : []),
974
+ ]
830
975
  expect([...wrappers].sort()).toEqual(expectedWrappers.sort())
831
976
  for (const wrapper of wrappers) {
832
977
  const loaded = await loadConfigFromFile(
833
978
  { command: 'serve', mode: invocation.mode },
834
- resolve(root, wrapper),
835
- root,
979
+ resolve(POLICY_ROOT, wrapper),
980
+ POLICY_ROOT,
836
981
  'silent',
837
982
  )
838
983
  if (loaded === null) throw new Error(`Unloaded file project ${wrapper}`)
@@ -897,28 +1042,32 @@ describe('root configuration', () => {
897
1042
  })
898
1043
 
899
1044
  it('requires and validates every selected target wrapper', async () => {
900
- const required: string[] = []
901
- for (const axis of ['src', 'app']) {
902
- for (const environment of ['core', 'browser', 'server']) {
903
- if (!existsSync(resolve(root, axis, environment))) continue
904
- required.push(`configs/${axis}/tsconfig.${environment}.json`)
905
- if (axis === 'src' || environment !== 'core') {
906
- required.push(`configs/${axis}/vite.${environment}.config.ts`)
907
- }
908
- }
909
- }
910
- for (const face of collectFrameworks(root)) {
911
- required.push(`configs/${face.replace('/', '/tsconfig.')}.json`)
912
- }
913
- if (existsSync(resolve(root, 'src/bin'))) {
914
- required.push('configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts')
915
- }
916
- if (existsSync(resolve(root, 'configs/app/vite.showcase.config.ts'))) {
917
- required.push('configs/app/vite.showcase.config.ts')
918
- }
919
- if (existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) {
920
- required.push('configs/app/vite.journey.config.ts')
921
- }
1045
+ const required = [
1046
+ ...['src', 'app'].flatMap((axis) =>
1047
+ ['core', 'browser', 'server'].flatMap((environment) =>
1048
+ existsSync(resolve(POLICY_ROOT, axis, environment))
1049
+ ? [
1050
+ `configs/${axis}/tsconfig.${environment}.json`,
1051
+ ...(axis === 'src' || environment !== 'core'
1052
+ ? [`configs/${axis}/vite.${environment}.config.ts`]
1053
+ : []),
1054
+ ]
1055
+ : [],
1056
+ ),
1057
+ ),
1058
+ ...collectFrameworks(POLICY_ROOT).map(
1059
+ (face) => `configs/${face.replace('/', '/tsconfig.')}.json`,
1060
+ ),
1061
+ ...(existsSync(resolve(POLICY_ROOT, 'src/bin'))
1062
+ ? ['configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts']
1063
+ : []),
1064
+ ...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.showcase.config.ts'))
1065
+ ? ['configs/app/vite.showcase.config.ts']
1066
+ : []),
1067
+ ...(existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))
1068
+ ? ['configs/app/vite.journey.config.ts']
1069
+ : []),
1070
+ ]
922
1071
  if (required[0] === undefined) {
923
1072
  throw new Error('The workspace selects no configuration target')
924
1073
  }
@@ -929,10 +1078,9 @@ describe('root configuration', () => {
929
1078
  'configs/src/tsconfig.*.json',
930
1079
  'configs/app/tsconfig.*.json',
931
1080
  ],
932
- { cwd: root },
1081
+ { cwd: POLICY_ROOT },
933
1082
  ).map((path) => path.replaceAll('\\', '/'))
934
1083
  const extra = 'configs/app/vite.core.config.ts'
935
- const controlled = found.concat(extra)
936
1084
  const planted = createPolicyScratch({ prefix: 'config-journey-' })
937
1085
  const journey = 'configs/app/vite.journey.config.ts'
938
1086
  planted.write(
@@ -951,9 +1099,11 @@ describe('root configuration', () => {
951
1099
  }\n`,
952
1100
  )
953
1101
  const journeys: object[][] = []
954
- const targets = required.map((wrapper) => ({ wrapper, directory: root }))
955
- targets.push({ wrapper: journey, directory: planted.path })
956
- controlled.push(journey)
1102
+ const targets = [
1103
+ ...required.map((wrapper) => ({ wrapper, directory: POLICY_ROOT })),
1104
+ { wrapper: journey, directory: planted.path },
1105
+ ]
1106
+ const controlled = [...found, extra, journey]
957
1107
 
958
1108
  // Required wrappers come from selected src/app targets. Only that set is loaded and validated.
959
1109
  // Extra wrappers remain in the found population but are ignored before their content is read.
@@ -1008,7 +1158,7 @@ describe('root configuration', () => {
1008
1158
  : environment === 'showcase'
1009
1159
  ? 'showcase'
1010
1160
  : `dist/${axis}/${environment}`
1011
- if (resolve(root, output) !== resolve(root, expected)) {
1161
+ if (resolve(POLICY_ROOT, output) !== resolve(POLICY_ROOT, expected)) {
1012
1162
  throw new Error(`${wrapper} resolves to the wrong output`)
1013
1163
  }
1014
1164
  continue
@@ -1023,7 +1173,7 @@ describe('root configuration', () => {
1023
1173
  if (axis === undefined || environment === undefined) {
1024
1174
  throw new Error(`${wrapper} carries no TypeScript scope`)
1025
1175
  }
1026
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
1176
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8'))
1027
1177
  if (typeof parsed !== 'object' || parsed === null) {
1028
1178
  throw new Error(`${wrapper} is not a TypeScript configuration record`)
1029
1179
  }
@@ -1057,8 +1207,7 @@ describe('root configuration', () => {
1057
1207
  }
1058
1208
  }
1059
1209
  for (const tests of journeys) {
1060
- const names: string[] = []
1061
- for (const test of tests) {
1210
+ const names = tests.map((test) => {
1062
1211
  expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
1063
1212
  'tests/app/browser/integration.test.ts',
1064
1213
  ])
@@ -1096,9 +1245,9 @@ describe('root configuration', () => {
1096
1245
  )
1097
1246
  )
1098
1247
  throw new Error('A journey variant is absent from its declared set')
1099
- expect(names).not.toContain(variant)
1100
- names.push(variant)
1101
- }
1248
+ return variant
1249
+ })
1250
+ expect(new Set(names).size).toBe(names.length)
1102
1251
  }
1103
1252
  } finally {
1104
1253
  planted.destroy()
@@ -1112,13 +1261,13 @@ describe('root configuration', () => {
1112
1261
  expect(() => {
1113
1262
  for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
1114
1263
  }).toThrow(/vite\.browser/u)
1115
- expect(() => expect(resolve(root, 'dist/actual')).toBe(resolve(root, 'dist/control'))).toThrow(
1116
- /expected/u,
1117
- )
1264
+ expect(() =>
1265
+ expect(resolve(POLICY_ROOT, 'dist/actual')).toBe(resolve(POLICY_ROOT, 'dist/control')),
1266
+ ).toThrow(/expected/u)
1118
1267
  })
1119
1268
 
1120
1269
  it('registers proof scripts in the correct gate', () => {
1121
- const manifest: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
1270
+ const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
1122
1271
  if (typeof manifest !== 'object' || manifest === null) {
1123
1272
  throw new Error('The package manifest is not a record')
1124
1273
  }
@@ -1133,34 +1282,34 @@ describe('root configuration', () => {
1133
1282
  const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
1134
1283
  const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
1135
1284
  const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
1285
+ const studies = Object.getOwnPropertyDescriptor(scripts, 'test:studies')?.value
1136
1286
  const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
1137
- const hasIntegration = existsSync(resolve(root, 'tests/integration.test.ts'))
1287
+ const hasIntegration = existsSync(resolve(POLICY_ROOT, 'tests/integration.test.ts'))
1138
1288
  // The optional proofs are read off the registered project set rather than off
1139
1289
  // their files, because the defect this measures is a registered project no
1140
1290
  // gate runs. A project selected by a path that is not yet there is still
1141
1291
  // registered, and it is exactly the one whose script goes missing.
1142
1292
  const rows = configuration.test?.projects
1143
1293
  if (!Array.isArray(rows)) throw new Error('The root configuration carries no projects')
1144
- const registered = new Set<string>()
1145
- for (const row of rows) {
1146
- if (typeof row === 'function') {
1147
- registered.add(row.name)
1148
- continue
1149
- }
1150
- if (typeof row !== 'object' || row === null) continue
1151
- const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
1152
- if (typeof block !== 'object' || block === null) continue
1153
- const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
1154
- if (typeof named !== 'object' || named === null) continue
1155
- const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
1156
- if (typeof label === 'string') registered.add(label)
1157
- }
1294
+ const registered = new Set(
1295
+ rows.flatMap((row) => {
1296
+ if (typeof row === 'function') return [row.name]
1297
+ if (typeof row !== 'object' || row === null) return []
1298
+ const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
1299
+ if (typeof block !== 'object' || block === null) return []
1300
+ const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
1301
+ if (typeof named !== 'object' || named === null) return []
1302
+ const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
1303
+ return typeof label === 'string' ? [label] : []
1304
+ }),
1305
+ )
1158
1306
  // The population must be able to answer both ways before either answer counts.
1159
1307
  expect(registered.has('config')).toBe(true)
1160
1308
  expect(registered.has('control')).toBe(false)
1161
1309
  const hasConformance = registered.has('conformance')
1162
1310
  const hasDistribution = registered.has('distribution')
1163
1311
  const hasService = registered.has('service')
1312
+ const hasStudies = registered.has('studies')
1164
1313
  expect(config).toBe(
1165
1314
  'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
1166
1315
  )
@@ -1211,10 +1360,28 @@ describe('root configuration', () => {
1211
1360
  expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
1212
1361
  hasService && manifestPublishes,
1213
1362
  )
1363
+ // The study proof spawns processes, so it follows the same split as the live-service proof:
1364
+ // the publish gate of a publishing workspace runs it after `npm test` and before the release
1365
+ // proof, and a private workspace reaches it from `test`.
1366
+ expect(studies).toBe(
1367
+ hasStudies
1368
+ ? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project studies'
1369
+ : undefined,
1370
+ )
1371
+ expect(typeof test === 'string' && test.includes('npm run test:studies')).toBe(
1372
+ hasStudies && !manifestPublishes,
1373
+ )
1374
+ expect(typeof publish === 'string' && publish.includes('npm run test:studies')).toBe(
1375
+ hasStudies && manifestPublishes,
1376
+ )
1377
+ const steps = ['npm test &&', 'npm run test:studies', 'npm run test:distribution']
1378
+ const positions = typeof publish === 'string' ? steps.map((step) => publish.indexOf(step)) : []
1379
+ const sequenced = positions.every((position, index) => position > (positions[index - 1] ?? -1))
1380
+ expect(sequenced || !(hasStudies && hasDistribution && manifestPublishes)).toBe(true)
1214
1381
  })
1215
1382
 
1216
1383
  it('rebuilds publishing workspaces before packing', () => {
1217
- const manifest: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
1384
+ const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
1218
1385
  if (typeof manifest !== 'object' || manifest === null) {
1219
1386
  throw new Error('The package manifest is not a record')
1220
1387
  }
@@ -1236,7 +1403,9 @@ describe('root configuration', () => {
1236
1403
  it('keeps the committed host inventory aligned with the vendored checkout bytes', async () => {
1237
1404
  // Run this gate against a quiescent checkout. It compares two reads and cannot
1238
1405
  // distinguish stale committed data from a source edit made while it runs.
1239
- const packageValue: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
1406
+ const packageValue: unknown = JSON.parse(
1407
+ readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'),
1408
+ )
1240
1409
  if (typeof packageValue !== 'object' || packageValue === null) {
1241
1410
  throw new Error('The package manifest is not a record')
1242
1411
  }
@@ -1247,7 +1416,7 @@ describe('root configuration', () => {
1247
1416
  const generator: unknown = Object.getOwnPropertyDescriptor(scripts, 'build:inventory')?.value
1248
1417
  expect(generator === undefined || typeof generator === 'string').toBe(true)
1249
1418
  if (generator === undefined) {
1250
- if (existsSync(resolve(root, 'host.json'))) {
1419
+ if (existsSync(resolve(POLICY_ROOT, 'host.json'))) {
1251
1420
  throw new Error('A committed host inventory exists without a generator')
1252
1421
  }
1253
1422
  return
@@ -1255,17 +1424,17 @@ describe('root configuration', () => {
1255
1424
  if (typeof generator !== 'string') {
1256
1425
  throw new Error('The host inventory generator is not a script')
1257
1426
  }
1258
- const committed = resolve(root, 'host.json')
1427
+ const committed = resolve(POLICY_ROOT, 'host.json')
1259
1428
  if (!existsSync(committed)) {
1260
1429
  throw new Error('The committed host inventory is absent at host.json')
1261
1430
  }
1262
- const helper = resolve(root, 'src/server/helpers.ts')
1431
+ const helper = resolve(POLICY_ROOT, 'src/server/helpers.ts')
1263
1432
  if (!existsSync(helper)) {
1264
1433
  throw new Error('The committed host inventory has no server stager')
1265
1434
  }
1266
1435
  const server = await createServer({
1267
1436
  configFile: false,
1268
- root,
1437
+ root: POLICY_ROOT,
1269
1438
  ...(configuration.resolve === undefined ? {} : { resolve: configuration.resolve }),
1270
1439
  server: { middlewareMode: true },
1271
1440
  })
@@ -1282,49 +1451,47 @@ describe('root configuration', () => {
1282
1451
  const workspace = scratch.path
1283
1452
  try {
1284
1453
  const generated = join(workspace, 'host.json')
1285
- Reflect.apply(stage, undefined, [root, generated])
1454
+ Reflect.apply(stage, undefined, [POLICY_ROOT, generated])
1286
1455
  const generatedText = readFileSync(generated, 'utf8')
1287
1456
  const committedText = readFileSync(committed, 'utf8')
1288
1457
  const values: readonly unknown[] = [JSON.parse(generatedText), JSON.parse(committedText)]
1289
- const indexes: Array<Map<string, string>> = []
1290
- for (const value of values) {
1458
+ const indexes = values.map((value) => {
1291
1459
  if (typeof value !== 'object' || value === null) {
1292
1460
  throw new Error('A host inventory is not a record')
1293
1461
  }
1294
1462
  const entries: unknown = Object.getOwnPropertyDescriptor(value, 'entries')?.value
1295
1463
  if (!Array.isArray(entries)) throw new Error('A host inventory carries no entry list')
1296
- const index = new Map<string, string>()
1297
- for (const entry of entries) {
1298
- if (typeof entry !== 'object' || entry === null) {
1299
- throw new Error('A host inventory carries a malformed entry')
1300
- }
1301
- const destination: unknown = Object.getOwnPropertyDescriptor(
1302
- entry,
1303
- 'destination',
1304
- )?.value
1305
- const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
1306
- if (typeof destination !== 'string' || typeof digest !== 'string') {
1307
- throw new Error('A host inventory entry carries no destination digest')
1308
- }
1309
- index.set(destination, digest)
1310
- }
1311
- indexes.push(index)
1312
- }
1464
+ return new Map(
1465
+ entries.map((entry: unknown): [string, string] => {
1466
+ if (typeof entry !== 'object' || entry === null) {
1467
+ throw new Error('A host inventory carries a malformed entry')
1468
+ }
1469
+ const destination: unknown = Object.getOwnPropertyDescriptor(
1470
+ entry,
1471
+ 'destination',
1472
+ )?.value
1473
+ const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
1474
+ if (typeof destination !== 'string' || typeof digest !== 'string') {
1475
+ throw new Error('A host inventory entry carries no destination digest')
1476
+ }
1477
+ return [destination, digest]
1478
+ }),
1479
+ )
1480
+ })
1313
1481
  const generatedIndex = indexes[0]
1314
1482
  const committedIndex = indexes[1]
1315
1483
  if (generatedIndex === undefined || committedIndex === undefined) {
1316
1484
  throw new Error('The host inventories were not indexed')
1317
1485
  }
1318
1486
  if (generatedText !== committedText) {
1319
- const stale: string[] = []
1320
- for (const [destination, digest] of generatedIndex) {
1321
- if (committedIndex.get(destination) !== digest) stale.push(destination)
1322
- }
1323
- for (const destination of committedIndex.keys()) {
1324
- if (!generatedIndex.has(destination)) stale.push(destination)
1325
- }
1487
+ const stale = [
1488
+ ...[...generatedIndex].flatMap(([destination, digest]) =>
1489
+ committedIndex.get(destination) === digest ? [] : [destination],
1490
+ ),
1491
+ ...[...committedIndex.keys()].filter((destination) => !generatedIndex.has(destination)),
1492
+ ]
1326
1493
  throw new Error(
1327
- `The committed host inventory is stale at ${stale.length > 0 ? stale.sort().join(', ') : 'host.json'}`,
1494
+ `The committed host inventory is stale at ${stale.length > 0 ? stale.toSorted().join(', ') : 'host.json'}`,
1328
1495
  )
1329
1496
  }
1330
1497
  console.info(`host-inventory: entries=${generatedIndex.size}`)
@@ -1337,7 +1504,7 @@ describe('root configuration', () => {
1337
1504
  })
1338
1505
 
1339
1506
  it('keeps policy rules active across every linted workspace path', () => {
1340
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
1507
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
1341
1508
  expect(inspectPolicyConfiguration(parsed)).toEqual([])
1342
1509
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1343
1510
  throw new Error('The Oxlint configuration is not a record')
@@ -1360,7 +1527,7 @@ describe('root configuration', () => {
1360
1527
  })
1361
1528
 
1362
1529
  it('omits the audit-confirmed dead policy type exports', () => {
1363
- const source = readFileSync(resolve(root, 'configs/policy.ts'), 'utf8')
1530
+ const source = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
1364
1531
  expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
1365
1532
  })
1366
1533
  })
@@ -2532,6 +2699,85 @@ createSomething({
2532
2699
  expect(commentToPolicyParagraph({ type: 'Block', value: '* ', range: [0, 0] })).toBe('')
2533
2700
  })
2534
2701
 
2702
+ describe('readers over a caller-supplied list with a hole', () => {
2703
+ // A `for…of` loop and a spread read a hole as `undefined`, where `map`, `filter`, `flatMap`, and
2704
+ // `findLast` skip it or stop before it, so each case puts the hole where the reader would not
2705
+ // reach it and pins the refusal the loop gave.
2706
+ const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
2707
+ const exported: PolicyExpression = {
2708
+ type: 'ExportNamedDeclaration',
2709
+ range: [47, 63],
2710
+ declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
2711
+ }
2712
+ const source: PolicySourceCode = { text: '', getAllComments: () => [] }
2713
+
2714
+ it('refuses a hole in the declarators of a statement as the declarator it reads as', () => {
2715
+ const declarator: PolicyExpression = {
2716
+ type: 'VariableDeclarator',
2717
+ range: [0, 0],
2718
+ id: name,
2719
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
2720
+ }
2721
+
2722
+ expect(
2723
+ statementToPolicyBindings({
2724
+ type: 'VariableDeclaration',
2725
+ range: [0, 0],
2726
+ declarations: [declarator],
2727
+ }),
2728
+ ).toHaveLength(1)
2729
+ expect(() =>
2730
+ statementToPolicyBindings({
2731
+ type: 'VariableDeclaration',
2732
+ range: [0, 0],
2733
+ declarations: createPolicySparse(2, new Map([[0, declarator]])),
2734
+ }),
2735
+ ).toThrow(TypeError)
2736
+ })
2737
+
2738
+ it('refuses a hole in the statements of a program as the statement it reads as', () => {
2739
+ const program: PolicyExpression = {
2740
+ type: 'Program',
2741
+ range: [0, 0],
2742
+ body: createPolicySparse(2, new Map([[0, exported]])),
2743
+ }
2744
+
2745
+ expect(() => programToPolicyDocs(program, source)).toThrow(TypeError)
2746
+ })
2747
+
2748
+ it('refuses a hole in the comments of a source as the comment it reads as', () => {
2749
+ const comment: PolicyComment = { type: 'Block', value: '* One.', range: [0, 11] }
2750
+ const comments = createPolicySparse(2, new Map([[1, comment]]))
2751
+ const program: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
2752
+
2753
+ expect(() =>
2754
+ programToPolicyDocs(program, { text: '/** One. */\n', getAllComments: () => comments }),
2755
+ ).toThrow(TypeError)
2756
+ })
2757
+
2758
+ it('refuses a hole in the body of a function-domain module as the statement it reads as', () => {
2759
+ const reports: PolicyDiagnostic[] = []
2760
+ const context: PolicyContext = {
2761
+ filename: '/ws/src/server/execution/run.ts',
2762
+ cwd: '/ws',
2763
+ sourceCode: source,
2764
+ report: (diagnostic) => {
2765
+ reports.push(diagnostic)
2766
+ },
2767
+ }
2768
+ const dense: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
2769
+ const sparse: PolicyExpression = {
2770
+ type: 'Program',
2771
+ range: [0, 0],
2772
+ body: createPolicySparse(2, new Map([[0, exported]])),
2773
+ }
2774
+
2775
+ reportDomain(context, dense)
2776
+ expect(reports).toStrictEqual([])
2777
+ expect(() => reportDomain(context, sparse)).toThrow(TypeError)
2778
+ })
2779
+ })
2780
+
2535
2781
  it('reads the opening word of a paragraph as its letters alone', () => {
2536
2782
  expect(paragraphToPolicyOpener('Creates a control.')).toBe('Creates')
2537
2783
  expect(paragraphToPolicyOpener('"Creates" a control.')).toBe('Creates')
@@ -2575,27 +2821,99 @@ createSomething({
2575
2821
  expect(DATA_SOURCE_FILES).not.toContain('plugins.ts')
2576
2822
  })
2577
2823
 
2824
+ it('admits computed ESM imports only in artifact loaders while refusing computed require', () => {
2825
+ const fixture = createPolicyScratch({ prefix: 'artifact-imports-' })
2826
+ try {
2827
+ fixture.write(
2828
+ 'configs/policy.ts',
2829
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
2830
+ )
2831
+ const configured = readConfigRecord(
2832
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2833
+ )
2834
+ // Include campaign fixtures even when the workspace's default lint walk ignores them.
2835
+ fixture.write('.oxlintrc.json', JSON.stringify({ ...configured, ignorePatterns: [] }))
2836
+ const written = ARTIFACT_IMPORT_CASES.map(([path, admitted]) => {
2837
+ fixture.write(path, 'const selected = process.argv[2]\nvoid import(selected)\ndebugger\n')
2838
+ const required = admitted ? path.replace(/\.(\w+)$/u, '.require.$1') : undefined
2839
+ if (required !== undefined) {
2840
+ fixture.write(
2841
+ required,
2842
+ 'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n',
2843
+ )
2844
+ }
2845
+ return { path, required }
2846
+ })
2847
+ const paths = written.map(({ path }) => path)
2848
+ const requires = written.flatMap(({ required }) => (required === undefined ? [] : [required]))
2849
+ const codes = readImportDiagnostics(fixture.path, [...paths, ...requires])
2850
+ for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
2851
+ expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
2852
+ path,
2853
+ refused: !admitted,
2854
+ })
2855
+ expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
2856
+ }
2857
+ for (const path of requires) {
2858
+ expect(codes.has('import(no-dynamic-require) ' + path)).toBe(true)
2859
+ expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
2860
+ }
2861
+
2862
+ expect(readConfigRecord(configured.rules)['import/no-dynamic-require']).toEqual([
2863
+ 'error',
2864
+ { esmodule: true },
2865
+ ])
2866
+ if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
2867
+ const overrides = configured.overrides.map(readConfigRecord)
2868
+ const artifact = overrides.filter(
2869
+ (override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
2870
+ )
2871
+ expect(artifact).toEqual([
2872
+ {
2873
+ files: [
2874
+ 'src/server/artifacts/**/*.{js,mjs,ts,mts}',
2875
+ 'tools/artifacts/**/*.{js,mjs,ts,mts}',
2876
+ '.orkestrel/*/instruments/**/tools/artifacts/**/*.{js,mjs,ts,mts}',
2877
+ ],
2878
+ rules: { 'import/no-dynamic-require': ['error', { esmodule: false }] },
2879
+ },
2880
+ ])
2881
+ fixture.write(
2882
+ '.oxlintrc.json',
2883
+ JSON.stringify({
2884
+ ...configured,
2885
+ ignorePatterns: [],
2886
+ overrides: overrides.filter((override) => !artifact.includes(override)),
2887
+ }),
2888
+ )
2889
+ const original = readImportDiagnostics(fixture.path, [...paths, ...requires])
2890
+ for (const path of [...paths, ...requires]) {
2891
+ expect(original.has('import(no-dynamic-require) ' + path)).toBe(true)
2892
+ }
2893
+ } finally {
2894
+ fixture.destroy()
2895
+ }
2896
+ })
2897
+
2578
2898
  it('matches every isolated import pattern with refused and admitted fixtures', () => {
2579
2899
  const fixture = createPolicyScratch({ prefix: 'propagation-patterns-' })
2580
2900
  const scratch = fixture.path
2581
2901
  try {
2582
2902
  const configured = readConfigRecord(
2583
- JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
2903
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2584
2904
  )
2585
2905
  if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
2586
- const overrides: object[] = []
2587
- const expected = new Map<string, boolean>()
2588
- for (const [blockIndex, value] of configured.overrides.entries()) {
2906
+ const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
2589
2907
  const block = readConfigRecord(value)
2590
2908
  const rule = readConfigRecord(block.rules)['no-restricted-imports']
2591
- if (!Array.isArray(rule)) continue
2909
+ if (!Array.isArray(rule)) return []
2592
2910
  const patterns = readConfigRecord(rule[1]).patterns
2593
2911
  if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
2594
- for (const [patternIndex, patternValue] of patterns.entries()) {
2912
+ return patterns.map((patternValue: unknown, patternIndex: number) => {
2595
2913
  const pattern = readConfigRecord(patternValue)
2596
2914
  const message = pattern.message
2597
2915
  if (typeof message !== 'string') throw new Error('Missing pattern message')
2598
- let sources: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2916
+ const listed: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2599
2917
  ? [
2600
2918
  ['https://host/x', true],
2601
2919
  ['data:text/plain,x', true],
@@ -2661,49 +2979,59 @@ createSomething({
2661
2979
  true,
2662
2980
  ],
2663
2981
  ]
2664
- if (message.includes('another styles face')) {
2665
- const face =
2666
- typeof pattern.regex === 'string'
2667
- ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
2668
- : undefined
2669
- if (face === undefined) throw new Error('Missing styles face in restriction pattern')
2670
- sources = [
2671
- [`@src/${face}`, true],
2672
- [`@src/${face}?raw`, true],
2673
- [`@orkestrel/x/${face}`, true],
2674
- [`../${face}/index.js`, true],
2675
- [`../src/${face}/index.js`, true],
2676
- [`../../src/${face}/index.js`, true],
2677
- ['@src/browser', false],
2678
- [`@src/${face}x`, false],
2679
- [`../${face}.ext/index.js`, false],
2680
- ]
2681
- }
2982
+ const face =
2983
+ message.includes('another styles face') && typeof pattern.regex === 'string'
2984
+ ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
2985
+ : undefined
2986
+ if (message.includes('another styles face') && face === undefined)
2987
+ throw new Error('Missing styles face in restriction pattern')
2988
+ const sources: ReadonlyArray<readonly [string, boolean]> =
2989
+ face === undefined
2990
+ ? listed
2991
+ : [
2992
+ [`@src/${face}`, true],
2993
+ [`@src/${face}?raw`, true],
2994
+ [`@orkestrel/x/${face}`, true],
2995
+ [`../${face}/index.js`, true],
2996
+ [`../src/${face}/index.js`, true],
2997
+ [`../../src/${face}/index.js`, true],
2998
+ ['@src/browser', false],
2999
+ [`@src/${face}x`, false],
3000
+ [`../${face}.ext/index.js`, false],
3001
+ ]
2682
3002
  const owner =
2683
3003
  Array.isArray(block.files) && typeof block.files[0] === 'string'
2684
3004
  ? block.files[0].split('/**')[0]
2685
3005
  : undefined
2686
3006
  if (owner === undefined) throw new Error('Missing pattern owner')
2687
- const paths: string[] = []
2688
- for (const [index, [source, refusal]] of [
2689
- ...sources,
2690
- ['./index.js', false] as const,
2691
- ].entries()) {
2692
- const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
2693
- mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
2694
- writeFileSync(
2695
- resolve(scratch, path),
2696
- 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
2697
- )
2698
- expected.set(path, refusal)
2699
- paths.push(path)
3007
+ const cases = [...sources, ['./index.js', false] as const].map(
3008
+ ([source, refusal], index) => {
3009
+ const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3010
+ mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3011
+ writeFileSync(
3012
+ resolve(scratch, path),
3013
+ 'import * as boundary from ' +
3014
+ JSON.stringify(source) +
3015
+ '\nvoid boundary\ndebugger\n',
3016
+ )
3017
+ return { path, refusal }
3018
+ },
3019
+ )
3020
+ return {
3021
+ cases,
3022
+ override: {
3023
+ files: cases.map(({ path }) => path),
3024
+ rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
3025
+ },
2700
3026
  }
2701
- overrides.push({
2702
- files: paths,
2703
- rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
2704
- })
2705
- }
2706
- }
3027
+ })
3028
+ })
3029
+ const overrides = built.map(({ override }) => override)
3030
+ const expected = new Map(
3031
+ built.flatMap(({ cases }) =>
3032
+ cases.map(({ path, refusal }): [string, boolean] => [path, refusal]),
3033
+ ),
3034
+ )
2707
3035
  writeFileSync(
2708
3036
  resolve(scratch, '.oxlintrc.json'),
2709
3037
  JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
@@ -2739,132 +3067,154 @@ createSomething({
2739
3067
  mkdirSync(resolve(scratch, 'configs'))
2740
3068
  writeFileSync(
2741
3069
  resolve(scratch, 'configs/policy.ts'),
2742
- readFileSync(resolve(root, 'configs/policy.ts')),
3070
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts')),
2743
3071
  )
2744
- const text = readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')
3072
+ const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
2745
3073
  writeFileSync(resolve(scratch, '.oxlintrc.json'), text)
2746
- const expected = new Map<string, boolean>()
2747
- for (const owner of [
2748
- 'src/vue',
2749
- 'app/vue',
2750
- 'src/browser',
2751
- 'src/bootstrap',
2752
- 'src/tailwindcss',
2753
- 'src/styles',
2754
- 'app/browser',
2755
- 'src/core',
2756
- 'app/core',
2757
- 'src/server',
2758
- 'app/server',
2759
- 'src/bin',
2760
- ]) {
2761
- const refused: Array<string | readonly [string, string]> = owner.endsWith('/vue')
2762
- ? [
2763
- 'node:fs',
2764
- '@src/server',
2765
- '../server/index.js',
2766
- '@src/print',
2767
- '../print/index.js',
2768
- ...(owner.startsWith('src/')
2769
- ? ['@app/core', '../../app/core/index.js']
2770
- : ['@app/server', '@app/print']),
2771
- ]
2772
- : [
2773
- 'vue',
2774
- 'vue/runtime-dom',
2775
- '@vue/runtime-core',
2776
- '@vitejs/plugin-vue',
2777
- '@src/vue',
2778
- '@orkestrel/sample/vue',
2779
- '@app/vue',
2780
- '../vue/index.js',
2781
- '../../src/vue/index.js',
2782
- '../../app/vue/index.js',
2783
- ]
2784
- const admitted = owner.endsWith('/vue')
2785
- ? [
2786
- '@src/core',
2787
- '@src/browser',
2788
- '../core/index.js',
2789
- '../browser/index.js',
2790
- 'vue',
2791
- '@vue/runtime-core',
2792
- ...(owner.startsWith('app/')
2793
- ? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
2794
- : []),
2795
- ]
2796
- : ['@src/core']
2797
- if (owner === 'src/core') refused.push('@src/browser', './index.css')
2798
- if (['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)) {
2799
- refused.push('node:fs', '@src/server', '../server/index.js', '@app/core')
2800
- for (const face of ['bootstrap', 'tailwindcss', 'styles']) {
2801
- if (owner === `src/${face}`) continue
2802
- refused.push(
2803
- `@src/${face}`,
2804
- `@orkestrel/x/${face}`,
2805
- `../${face}/index.js`,
2806
- `../src/${face}/index.js`,
2807
- `../../src/${face}/index.js`,
2808
- ['export * from', `@src/${face}`],
2809
- ['import type * as boundary from', `@src/${face}`],
2810
- )
2811
- admitted.push(`@src/${face}x`, `../${face}.ext/index.js`)
2812
- }
2813
- admitted.push('@src/browser', './sheet.js', '@orkestrel/contract')
2814
- }
2815
- for (const [sources, refusal] of [
2816
- [
3074
+ const faces = ['bootstrap', 'tailwindcss', 'styles']
3075
+ const expected = new Map([
3076
+ ...[
3077
+ 'src/vue',
3078
+ 'app/vue',
3079
+ 'src/browser',
3080
+ 'src/bootstrap',
3081
+ 'src/tailwindcss',
3082
+ 'src/styles',
3083
+ 'app/browser',
3084
+ 'src/core',
3085
+ 'app/core',
3086
+ 'src/server',
3087
+ 'app/server',
3088
+ 'src/bin',
3089
+ ].flatMap((owner) => {
3090
+ const sheet = ['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)
3091
+ const refused: ReadonlyArray<string | readonly [string, string]> = [
3092
+ ...(owner.endsWith('/vue')
3093
+ ? [
3094
+ 'node:fs',
3095
+ '@src/server',
3096
+ '../server/index.js',
3097
+ '@src/print',
3098
+ '../print/index.js',
3099
+ ...(owner.startsWith('src/')
3100
+ ? ['@app/core', '../../app/core/index.js']
3101
+ : ['@app/server', '@app/print']),
3102
+ ]
3103
+ : [
3104
+ 'vue',
3105
+ 'vue/runtime-dom',
3106
+ '@vue/runtime-core',
3107
+ '@vitejs/plugin-vue',
3108
+ '@src/vue',
3109
+ '@orkestrel/sample/vue',
3110
+ '@app/vue',
3111
+ '../vue/index.js',
3112
+ '../../src/vue/index.js',
3113
+ '../../app/vue/index.js',
3114
+ ]),
3115
+ ...(owner === 'src/core' ? ['@src/browser', './index.css'] : []),
3116
+ ...(sheet
3117
+ ? [
3118
+ 'node:fs',
3119
+ '@src/server',
3120
+ '../server/index.js',
3121
+ '@app/core',
3122
+ ...faces
3123
+ .filter((face) => owner !== `src/${face}`)
3124
+ .flatMap((face): ReadonlyArray<string | readonly [string, string]> => [
3125
+ `@src/${face}`,
3126
+ `@orkestrel/x/${face}`,
3127
+ `../${face}/index.js`,
3128
+ `../src/${face}/index.js`,
3129
+ `../../src/${face}/index.js`,
3130
+ ['export * from', `@src/${face}`],
3131
+ ['import type * as boundary from', `@src/${face}`],
3132
+ ]),
3133
+ ]
3134
+ : []),
3135
+ ]
3136
+ const admitted = [
3137
+ ...(owner.endsWith('/vue')
3138
+ ? [
3139
+ '@src/core',
3140
+ '@src/browser',
3141
+ '../core/index.js',
3142
+ '../browser/index.js',
3143
+ 'vue',
3144
+ '@vue/runtime-core',
3145
+ ...(owner.startsWith('app/')
3146
+ ? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
3147
+ : []),
3148
+ ]
3149
+ : ['@src/core']),
3150
+ ...(sheet
3151
+ ? [
3152
+ ...faces
3153
+ .filter((face) => owner !== `src/${face}`)
3154
+ .flatMap((face) => [`@src/${face}x`, `../${face}.ext/index.js`]),
3155
+ '@src/browser',
3156
+ './sheet.js',
3157
+ '@orkestrel/contract',
3158
+ ]
3159
+ : []),
3160
+ ]
3161
+ return (
2817
3162
  [
2818
- ...refused,
2819
- 'https://host/x',
2820
- 'data:text/plain,x',
2821
- '../core/../server/index.js',
2822
- '../core/./index.js',
2823
- ],
2824
- true,
2825
- ],
3163
+ [
3164
+ [
3165
+ ...refused,
3166
+ 'https://host/x',
3167
+ 'data:text/plain,x',
3168
+ '../core/../server/index.js',
3169
+ '../core/./index.js',
3170
+ ],
3171
+ true,
3172
+ ],
3173
+ [
3174
+ [
3175
+ ...admitted,
3176
+ './index.js',
3177
+ '../core/index.js',
3178
+ ...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
3179
+ ],
3180
+ false,
3181
+ ],
3182
+ ] as const
3183
+ ).flatMap(([sources, refusal]) =>
3184
+ sources.map((source, index): [string, boolean] => {
3185
+ const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
3186
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3187
+ writeFileSync(
3188
+ resolve(scratch, file),
3189
+ typeof source === 'string'
3190
+ ? 'import * as boundary from ' +
3191
+ JSON.stringify(source) +
3192
+ '\nvoid boundary\ndebugger\n'
3193
+ : source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
3194
+ )
3195
+ return [file, refusal]
3196
+ }),
3197
+ )
3198
+ }),
3199
+ ...(
2826
3200
  [
2827
- [
2828
- ...admitted,
2829
- './index.js',
2830
- '../core/index.js',
2831
- ...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
2832
- ],
2833
- false,
2834
- ],
2835
- ] as const) {
2836
- for (const [index, source] of sources.entries()) {
2837
- const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
2838
- mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
2839
- writeFileSync(
2840
- resolve(scratch, file),
2841
- typeof source === 'string'
2842
- ? 'import * as boundary from ' +
2843
- JSON.stringify(source) +
2844
- '\nvoid boundary\ndebugger\n'
2845
- : source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
2846
- )
2847
- expected.set(file, refusal)
2848
- }
2849
- }
2850
- }
2851
- for (const [file, source, refusal] of [
2852
- ['probe.config.ts', 'typescript', true],
2853
- ['allowed.config.ts', 'node:fs', false],
2854
- ['src/core/compiler.ts', 'typescript', true],
2855
- ['tests/compiler.ts', 'typescript', true],
2856
- ['configs/compiler.ts', 'typescript', true],
2857
- ['scripts/compiler.ts', 'typescript', true],
2858
- ] as const) {
2859
- mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
2860
- writeFileSync(
2861
- resolve(scratch, file),
2862
- 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
2863
- )
2864
- expected.set(file, refusal)
2865
- }
2866
- const reports: Array<ReadonlySet<string>> = []
2867
- for (const disabled of [false, true]) {
3201
+ ['probe.config.ts', 'typescript', true],
3202
+ ['allowed.config.ts', 'node:fs', false],
3203
+ ['src/core/compiler.ts', 'typescript', true],
3204
+ ['tests/compiler.ts', 'typescript', true],
3205
+ ['configs/compiler.ts', 'typescript', true],
3206
+ ['scripts/compiler.ts', 'typescript', true],
3207
+ ] as const
3208
+ ).map(([file, source, refusal]): [string, boolean] => {
3209
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3210
+ writeFileSync(
3211
+ resolve(scratch, file),
3212
+ 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
3213
+ )
3214
+ return [file, refusal]
3215
+ }),
3216
+ ])
3217
+ const reports = [false, true].map((disabled) => {
2868
3218
  if (disabled) {
2869
3219
  const parsed: unknown = JSON.parse(text)
2870
3220
  if (typeof parsed !== 'object' || parsed === null)
@@ -2887,9 +3237,8 @@ createSomething({
2887
3237
  }
2888
3238
  writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
2889
3239
  }
2890
- const codes = readImportDiagnostics(scratch, [...expected.keys()])
2891
- reports.push(codes)
2892
- }
3240
+ return readImportDiagnostics(scratch, [...expected.keys()])
3241
+ })
2893
3242
  const [enabled, disabled] = reports
2894
3243
  if (enabled === undefined || disabled === undefined)
2895
3244
  throw new Error('Missing boundary readings')
@@ -2915,7 +3264,7 @@ createSomething({
2915
3264
  })
2916
3265
 
2917
3266
  it('enables every plugin rule over the population its law names', () => {
2918
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
3267
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
2919
3268
  const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
2920
3269
  if (typeof rules !== 'object' || rules === null) {
2921
3270
  throw new Error('The policy plugin declares no rules')
@@ -2937,8 +3286,11 @@ createSomething({
2937
3286
  try {
2938
3287
  // Compare diagnostics from the real directory while the child keeps the scratch cwd.
2939
3288
  const comparisonRoot = realpathSync.native(scratch.path)
2940
- scratch.write('.oxlintrc.json', readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
2941
- scratch.write('configs/policy.ts', readFileSync(resolve(root, 'configs/policy.ts'), 'utf8'))
3289
+ scratch.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3290
+ scratch.write(
3291
+ 'configs/policy.ts',
3292
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3293
+ )
2942
3294
  scratch.write(
2943
3295
  'src/violations/fixture.ts',
2944
3296
  [
@@ -3002,7 +3354,9 @@ createSomething({
3002
3354
  // extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
3003
3355
  // containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
3004
3356
  // the entry survives hoisting, a nested `node_modules` layout, and a future rename.
3005
- const manifestPath = createRequire(join(root, 'package.json')).resolve('oxlint/package.json')
3357
+ const manifestPath = createRequire(join(POLICY_ROOT, 'package.json')).resolve(
3358
+ 'oxlint/package.json',
3359
+ )
3006
3360
  const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
3007
3361
  if (typeof manifest !== 'object' || manifest === null) {
3008
3362
  throw new Error('The oxlint package manifest is not an object')
@@ -3029,8 +3383,7 @@ createSomething({
3029
3383
  [binary, '--config', config, '--format', 'json', 'src/clean'],
3030
3384
  { cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
3031
3385
  )
3032
- const reports: string[][] = []
3033
- for (const result of [violations, clean]) {
3386
+ const reports = [violations, clean].map((result): string[] => {
3034
3387
  if (result.error !== undefined) throw result.error
3035
3388
  const report: unknown = JSON.parse(result.stdout)
3036
3389
  if (typeof report !== 'object' || report === null) {
@@ -3038,8 +3391,7 @@ createSomething({
3038
3391
  }
3039
3392
  const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
3040
3393
  if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
3041
- const codes: string[] = []
3042
- for (const diagnostic of diagnostics) {
3394
+ return diagnostics.map((diagnostic: unknown) => {
3043
3395
  if (typeof diagnostic !== 'object' || diagnostic === null) {
3044
3396
  throw new Error('Oxlint returned a malformed diagnostic')
3045
3397
  }
@@ -3048,10 +3400,9 @@ createSomething({
3048
3400
  if (typeof code !== 'string' || typeof filename !== 'string') {
3049
3401
  throw new Error('Oxlint returned a diagnostic without a rule id and a file')
3050
3402
  }
3051
- codes.push(`${code} ${normalizePolicyFilename(comparisonRoot, filename)}`)
3052
- }
3053
- reports.push(codes)
3054
- }
3403
+ return `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`
3404
+ })
3405
+ })
3055
3406
 
3056
3407
  const violationCodes = reports[0]
3057
3408
  const cleanCodes = reports[1]
@@ -3230,9 +3581,11 @@ describe('configuration helpers', () => {
3230
3581
 
3231
3582
  it('exposes every helper this proof requires', () => {
3232
3583
  const required = [
3584
+ 'BuildState',
3233
3585
  'ENVIRONMENT_MODULE_BYTES',
3234
3586
  'PACKAGE_MANIFEST_BYTES',
3235
3587
  'WORKSPACE_ROOT',
3588
+ 'attemptValue',
3236
3589
  'buildExtractorOverride',
3237
3590
  'containedPath',
3238
3591
  'decodeAssetSource',
@@ -3244,6 +3597,7 @@ describe('configuration helpers', () => {
3244
3597
  'environmentPathError',
3245
3598
  'environmentSourceError',
3246
3599
  'fileSystemPath',
3600
+ 'findPackageDirectory',
3247
3601
  'hasAsciiUrlControl',
3248
3602
  'isBoundaryExemptModule',
3249
3603
  'isExtractorModule',
@@ -3260,10 +3614,10 @@ describe('configuration helpers', () => {
3260
3614
  'parseProjectScope',
3261
3615
  'physicalPath',
3262
3616
  'readBoundedFile',
3617
+ 'readHandleBytes',
3263
3618
  'readCompilerOutput',
3264
3619
  'rewriteCoreSpecifier',
3265
3620
  'sourceFallback',
3266
- 'trustedPackageRootFor',
3267
3621
  'workspacePath',
3268
3622
  ]
3269
3623
  const found = Object.keys(configHelpers)
@@ -3302,17 +3656,19 @@ describe('configuration helpers', () => {
3302
3656
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
3303
3657
  try {
3304
3658
  const outside = scratch.path
3305
- const importer = resolve(root, 'tests/config.test.ts')
3306
- expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(root))
3307
- expect(configHelpers.fileSystemPath(`/@fs/${root}`)).toBe(root)
3308
- expect(configHelpers.physicalPath(`${root}?control=true`)).toBe(realpathSync.native(root))
3659
+ const importer = resolve(POLICY_ROOT, 'tests/config.test.ts')
3660
+ expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(POLICY_ROOT))
3661
+ expect(configHelpers.fileSystemPath(`/@fs/${POLICY_ROOT}`)).toBe(POLICY_ROOT)
3662
+ expect(configHelpers.physicalPath(`${POLICY_ROOT}?control=true`)).toBe(
3663
+ realpathSync.native(POLICY_ROOT),
3664
+ )
3309
3665
  expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
3310
- resolve(root, 'tests/setup.ts'),
3666
+ resolve(POLICY_ROOT, 'tests/setup.ts'),
3311
3667
  )
3312
3668
  expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
3313
3669
  expect(configHelpers.workspacePath(importer)).toBe('tests/config.test.ts')
3314
- expect(configHelpers.containedPath(root, importer)).toBe(true)
3315
- expect(configHelpers.containedPath(root, outside)).toBe(false)
3670
+ expect(configHelpers.containedPath(POLICY_ROOT, importer)).toBe(true)
3671
+ expect(configHelpers.containedPath(POLICY_ROOT, outside)).toBe(false)
3316
3672
  expect(configHelpers.workspacePath(outside)).toBeUndefined()
3317
3673
  expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
3318
3674
  expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
@@ -3351,14 +3707,175 @@ describe('configuration helpers', () => {
3351
3707
  realpathSync.native(packageRoot),
3352
3708
  )
3353
3709
  expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
3710
+ } finally {
3711
+ scratch.destroy()
3712
+ }
3713
+ })
3714
+
3715
+ it('returns a producer value, or undefined when the producer throws', () => {
3716
+ expect(configHelpers.attemptValue(() => decodeURI('%20'))).toBe(' ')
3717
+ expect(configHelpers.attemptValue(() => decodeURI('%E0%A4%A'))).toBeUndefined()
3718
+ expect(configHelpers.attemptValue(() => undefined)).toBeUndefined()
3719
+ expect(configHelpers.attemptValue(() => 0)).toBe(0)
3720
+ })
3721
+
3722
+ it('fills a buffer from an open handle until it ends, and refuses a file over the limit', () => {
3723
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
3724
+ try {
3725
+ // Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
3726
+ const readings = [
3727
+ { text: 'abc', offset: 0, limit: 4 },
3728
+ { text: 'abcd', offset: 0, limit: 4 },
3729
+ { text: 'abcde', offset: 0, limit: 4 },
3730
+ { text: '', offset: 0, limit: 4 },
3731
+ { text: 'abc', offset: 2, limit: 10 },
3732
+ { text: 'abc', offset: 2, limit: 4 },
3733
+ ].map(({ text, offset, limit }) => {
3734
+ scratch.write('subject.txt', text)
3735
+ const handle = openSync(resolve(scratch.path, 'subject.txt'), 'r')
3736
+ try {
3737
+ const bytes = Buffer.alloc(16)
3738
+ const count = configHelpers.readHandleBytes(handle, bytes, offset, limit)
3739
+ return { count, filled: bytes.toString('utf8', offset, count ?? offset) }
3740
+ } finally {
3741
+ closeSync(handle)
3742
+ }
3743
+ })
3744
+ // A file at or under the limit answers its length; the shifted read adds the offset to the
3745
+ // length and fills the buffer past it; a file over the limit answers nothing.
3746
+ expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
3747
+ expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
3748
+ } finally {
3749
+ scratch.destroy()
3750
+ }
3751
+ })
3752
+
3753
+ it('finds the nearest package directory from a manifest, a package boundary, or nothing', () => {
3754
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-find-' })
3755
+ try {
3756
+ scratch.write('held/package.json', '{"name":"held"}')
3757
+ scratch.write('held/src/deep/file.txt', '')
3758
+ scratch.write('modules/node_modules/plain/lib/file.txt', '')
3759
+ scratch.write('modules/node_modules/@scope/named/lib/file.txt', '')
3760
+ scratch.write('modules/node_modules/@scope/file.txt', '')
3761
+ const held = resolve(scratch.path, 'held')
3762
+ expect(configHelpers.findPackageDirectory(held)).toBe(held)
3763
+ expect(configHelpers.findPackageDirectory(resolve(held, 'src/deep'))).toBe(held)
3764
+ const plain = resolve(scratch.path, 'modules/node_modules/plain')
3765
+ expect(configHelpers.findPackageDirectory(resolve(plain, 'lib'))).toBe(plain)
3766
+ const named = resolve(scratch.path, 'modules/node_modules/@scope/named')
3767
+ expect(configHelpers.findPackageDirectory(resolve(named, 'lib'))).toBe(named)
3768
+ expect(configHelpers.findPackageDirectory(named)).toBe(named)
3769
+ // A scope directory alone is not a package boundary, so the walk passes it by.
3354
3770
  expect(
3355
- configHelpers.trustedPackageRootFor(source, new Set([realpathSync.native(packageRoot)])),
3356
- ).toBe(realpathSync.native(packageRoot))
3771
+ configHelpers.isPackageBoundary(resolve(scratch.path, 'modules/node_modules/@scope')),
3772
+ ).toBe(false)
3773
+ // The walk ends at the filesystem root; the answer there is the root only if it holds a manifest.
3774
+ const root = parse(scratch.path).root
3775
+ expect(configHelpers.findPackageDirectory(root)).toBe(
3776
+ existsSync(resolve(root, 'package.json')) ? root : undefined,
3777
+ )
3778
+ } finally {
3779
+ scratch.destroy()
3780
+ }
3781
+ })
3782
+
3783
+ it('holds the workspace defaults until a resolved build config replaces them', async () => {
3784
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-state-' })
3785
+ try {
3786
+ const fresh = new configHelpers.BuildState()
3787
+ expect(fresh.building).toBe(false)
3788
+ expect(fresh.output).toBe(configHelpers.WORKSPACE_ROOT)
3789
+ expect(fresh.root).toBe(configHelpers.WORKSPACE_ROOT)
3790
+ expect(fresh.source).toBe('')
3791
+ expect(new configHelpers.BuildState('C:/output').output).toBe('C:/output')
3792
+
3793
+ const entry = resolve(scratch.path, 'index.ts')
3794
+ const inline: InlineConfig = {
3795
+ root: scratch.path,
3796
+ configFile: false,
3797
+ logLevel: 'silent',
3798
+ build: { outDir: 'out', lib: { entry, formats: ['es'] } },
3799
+ }
3800
+ const building = new configHelpers.BuildState()
3801
+ building.resolve(await resolveConfig(inline, 'build'))
3802
+ expect(building.building).toBe(true)
3803
+ expect(building.output).toBe(resolve(scratch.path, 'out'))
3804
+ expect(building.root).toBe(realpathSync.native(scratch.path))
3805
+ expect(building.source).toBe(entry)
3806
+
3807
+ // The control: a serve command is not a build, and a config with no library entry has no source.
3808
+ const serving = new configHelpers.BuildState()
3809
+ serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
3810
+ expect(serving.building).toBe(false)
3811
+ expect(serving.source).toBe('')
3812
+ } finally {
3813
+ scratch.destroy()
3814
+ }
3815
+ })
3816
+
3817
+ it('admits each physical package root once and locates the first admitted root containing a path', () => {
3818
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-locate-' })
3819
+ try {
3820
+ scratch.write('modules/outer/inner/index.ts', '')
3821
+ scratch.write('modules/other/index.ts', '')
3822
+ const outer = realpathSync.native(resolve(scratch.path, 'modules/outer'))
3823
+ const inner = realpathSync.native(resolve(scratch.path, 'modules/outer/inner'))
3824
+ const inside = resolve(scratch.path, 'modules/outer/inner/index.ts')
3825
+ const beside = resolve(scratch.path, 'modules/other/index.ts')
3826
+
3827
+ const state = new configHelpers.BuildState()
3828
+ // A fresh state admits nothing, so it locates nothing.
3829
+ expect(state.locate(inside)).toBeUndefined()
3830
+ state.trust(outer)
3831
+ state.trust(outer)
3832
+ state.trust(inner)
3833
+ expect(state.locate(inside)).toBe(outer)
3834
+ expect(state.locate(beside)).toBeUndefined()
3835
+ // Admission order decides which of two containing roots answers.
3836
+ const reversed = new configHelpers.BuildState()
3837
+ reversed.trust(inner)
3838
+ reversed.trust(outer)
3839
+ expect(reversed.locate(inside)).toBe(inner)
3840
+ expect(new configHelpers.BuildState().locate(inside)).toBeUndefined()
3841
+ // The admitted roots are never returned, so the class publishes no member that lists them.
3842
+ expect(Object.getOwnPropertyNames(configHelpers.BuildState.prototype).sort()).toEqual([
3843
+ 'building',
3844
+ 'constructor',
3845
+ 'locate',
3846
+ 'output',
3847
+ 'resolve',
3848
+ 'root',
3849
+ 'source',
3850
+ 'trust',
3851
+ ])
3357
3852
  } finally {
3358
3853
  scratch.destroy()
3359
3854
  }
3360
3855
  })
3361
3856
 
3857
+ it('climbs from a policy node through its enclosing parentheses alone', () => {
3858
+ const call = { type: 'CallExpression', range: [0, 9] } satisfies PolicyExpression
3859
+ const outer = {
3860
+ type: 'ParenthesizedExpression',
3861
+ range: [1, 8],
3862
+ parent: call,
3863
+ } satisfies PolicyExpression
3864
+ const inner = {
3865
+ type: 'ParenthesizedExpression',
3866
+ range: [2, 7],
3867
+ parent: outer,
3868
+ } satisfies PolicyExpression
3869
+ const leaf = { type: 'Identifier', range: [3, 4], parent: inner } satisfies PolicyExpression
3870
+ expect(nodeToPolicyOuter(leaf)).toBe(outer)
3871
+ expect(nodeToPolicyOuter(inner)).toBe(outer)
3872
+ expect(nodeToPolicyOuter(outer)).toBe(outer)
3873
+ // The control: a node under a non-parenthesized parent, or under none, stays where it is.
3874
+ const argument = { type: 'Identifier', range: [1, 2], parent: call } satisfies PolicyExpression
3875
+ expect(nodeToPolicyOuter(argument)).toBe(argument)
3876
+ expect(nodeToPolicyOuter(call)).toBe(call)
3877
+ })
3878
+
3362
3879
  it('classifies module boundaries and extracts static asset sources', async () => {
3363
3880
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
3364
3881
  try {
@@ -3371,8 +3888,12 @@ describe('configuration helpers', () => {
3371
3888
  expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
3372
3889
  expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
3373
3890
  expect(configHelpers.isBoundaryExemptModule('virtual:control')).toBe(true)
3374
- expect(configHelpers.isBoundaryExemptModule(resolve(root, 'src/core/index.ts'))).toBe(false)
3375
- expect(configHelpers.isWorkspaceBoundaryModule(resolve(root, 'src/core/index.ts'))).toBe(true)
3891
+ expect(configHelpers.isBoundaryExemptModule(resolve(POLICY_ROOT, 'src/core/index.ts'))).toBe(
3892
+ false,
3893
+ )
3894
+ expect(
3895
+ configHelpers.isWorkspaceBoundaryModule(resolve(POLICY_ROOT, 'src/core/index.ts')),
3896
+ ).toBe(true)
3376
3897
  expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
3377
3898
  expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
3378
3899
  expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
@@ -3398,31 +3919,66 @@ describe('configuration helpers', () => {
3398
3919
  'Browser modules cannot depend on Node or server-only modules',
3399
3920
  )
3400
3921
  expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
3401
- const expected = resolve(root, 'dist/config-control')
3922
+ const expected = resolve(POLICY_ROOT, 'dist/config-control')
3402
3923
  expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
3403
3924
  expect(() =>
3404
- configHelpers.enforceOutputPath(resolve(root, 'dist/outside-control'), expected),
3925
+ configHelpers.enforceOutputPath(resolve(POLICY_ROOT, 'dist/outside-control'), expected),
3405
3926
  ).toThrow('Build output must use its exact configured workspace directory')
3406
- const outside = resolve(dirname(root), 'outside-config-control')
3927
+ const outside = resolve(dirname(POLICY_ROOT), 'outside-config-control')
3407
3928
  expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
3408
3929
  'Build output must remain inside the workspace',
3409
3930
  )
3410
3931
  })
3411
3932
 
3933
+ it('admits computed artifact imports through Vite while retaining literal boundary inspection', async () => {
3934
+ for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => admitted)) {
3935
+ await expect(
3936
+ configHelpers.environmentAssetSources(
3937
+ 'void import(process.argv[2])',
3938
+ resolve(POLICY_ROOT, path),
3939
+ ),
3940
+ ).resolves.toEqual([])
3941
+ }
3942
+ for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => !admitted)) {
3943
+ await expect(
3944
+ configHelpers.environmentAssetSources(
3945
+ 'void import(process.argv[2])',
3946
+ resolve(POLICY_ROOT, path),
3947
+ ),
3948
+ ).rejects.toThrow('Dynamic imports must use static string values')
3949
+ }
3950
+ await expect(
3951
+ configHelpers.environmentAssetSources(
3952
+ "void import('@src/browser')",
3953
+ resolve(POLICY_ROOT, 'src/server/artifacts/Artifacts.ts'),
3954
+ ),
3955
+ ).resolves.toEqual(['@src/browser'])
3956
+ expect(configHelpers.environmentSourceError('src/server', '@src/browser')).toBeDefined()
3957
+ await expect(
3958
+ configHelpers.environmentAssetSources(
3959
+ 'void import(process.argv[2])',
3960
+ resolve(POLICY_ROOT, '../outside/src/server/artifacts/Artifacts.ts'),
3961
+ ),
3962
+ ).rejects.toThrow('Dynamic imports must use static string values')
3963
+ })
3964
+
3412
3965
  it('drives each plugin through its real Vite hooks', async () => {
3413
3966
  const environments: ReadonlyArray<
3414
3967
  'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
3415
3968
  > = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
3416
- const owner = environments.find((environment) => existsSync(resolve(root, environment)))
3969
+ const owner = environments.find((environment) => existsSync(resolve(POLICY_ROOT, environment)))
3417
3970
  if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
3418
- const scratch = createPolicyScratch({ parent: resolve(root, owner), prefix: 'config-build-' })
3971
+ const scratch = createPolicyScratch({
3972
+ parent: resolve(POLICY_ROOT, owner),
3973
+ prefix: 'config-build-',
3974
+ })
3419
3975
  const workspace = scratch.path
3420
3976
  try {
3421
3977
  const source = resolve(workspace, 'index.ts')
3422
3978
  writeFileSync(source, 'export const control = true\n', 'utf8')
3423
3979
  await expect(
3424
3980
  build({
3425
- root,
3981
+ root: POLICY_ROOT,
3426
3982
  configFile: false,
3427
3983
  logLevel: 'silent',
3428
3984
  publicDir: false,
@@ -3469,7 +4025,7 @@ describe('configuration helpers', () => {
3469
4025
  // raise its passed count with nothing measured. Every generated workspace runs its projects
3470
4026
  // under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
3471
4027
  // condition in it are read by re-running the project with a reporter that names skipped cases.
3472
- it.skipIf(!publishes)(
4028
+ it.skipIf(!POLICY_PUBLISHES)(
3473
4029
  'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
3474
4030
  () => {
3475
4031
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
@@ -3477,10 +4033,10 @@ describe('configuration helpers', () => {
3477
4033
  // no core project, so this walks to the first face the workspace actually carries.
3478
4034
  const faces = ['core', 'browser', 'server']
3479
4035
  const face = faces.find((candidate) =>
3480
- existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
4036
+ existsSync(resolve(POLICY_ROOT, `configs/src/tsconfig.${candidate}.json`)),
3481
4037
  )
3482
4038
  if (face === undefined) throw new Error('The workspace declares no face project')
3483
- const project = resolve(root, `configs/src/tsconfig.${face}.json`)
4039
+ const project = resolve(POLICY_ROOT, `configs/src/tsconfig.${face}.json`)
3484
4040
  const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
3485
4041
  if (typeof declared !== 'object' || declared === null) {
3486
4042
  throw new Error(`The ${face} project is not a TypeScript configuration record`)
@@ -3535,7 +4091,7 @@ describe('configuration helpers', () => {
3535
4091
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
3536
4092
  // Any project path resolves these, because each reading is refused by the text before the
3537
4093
  // path is read. The root project is the one file every workspace carries.
3538
- const project = resolve(root, 'tsconfig.json')
4094
+ const project = resolve(POLICY_ROOT, 'tsconfig.json')
3539
4095
  expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
3540
4096
  expect(
3541
4097
  configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
@@ -3548,7 +4104,7 @@ describe('configuration helpers', () => {
3548
4104
  configHelpers.readCompilerOutput(compiler, [
3549
4105
  '--showConfig',
3550
4106
  '-p',
3551
- resolve(root, 'configs/tsconfig.absent.json'),
4107
+ resolve(POLICY_ROOT, 'configs/tsconfig.absent.json'),
3552
4108
  ]),
3553
4109
  ).toThrow('The declaration compiler failed')
3554
4110
 
@@ -3621,12 +4177,12 @@ describe('configuration helpers', () => {
3621
4177
 
3622
4178
  // A hoisted install can place the extractor anywhere `require.resolve` reaches, so the skip
3623
4179
  // control compares against the path resolution actually returned rather than a fixed layout.
3624
- it.skipIf(extractorPath === undefined)('finds the resolved extractor on disk', () => {
3625
- if (extractorPath === undefined) throw new Error('The extractor resolution left no path')
3626
- expect(existsSync(extractorPath)).toBe(true)
4180
+ it.skipIf(POLICY_EXTRACTOR === undefined)('finds the resolved extractor on disk', () => {
4181
+ if (POLICY_EXTRACTOR === undefined) throw new Error('The extractor resolution left no path')
4182
+ expect(existsSync(POLICY_EXTRACTOR)).toBe(true)
3627
4183
  })
3628
4184
 
3629
- it.skipIf(extractorPath !== undefined)('rejects resolving the unavailable extractor', () => {
4185
+ it.skipIf(POLICY_EXTRACTOR !== undefined)('rejects resolving the unavailable extractor', () => {
3630
4186
  expect(() => createRequire(import.meta.url).resolve('@microsoft/api-extractor')).toThrow(
3631
4187
  'Cannot find module',
3632
4188
  )
@@ -3634,7 +4190,7 @@ describe('configuration helpers', () => {
3634
4190
 
3635
4191
  // The roll-up loads the declaration extractor, which only a workspace publishing source from
3636
4192
  // `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
3637
- it.skipIf(extractorPath === undefined)(
4193
+ it.skipIf(POLICY_EXTRACTOR === undefined)(
3638
4194
  'rolls one face into a single declaration and rewrites its core and browser specifiers',
3639
4195
  async () => {
3640
4196
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
@@ -3779,3 +4335,133 @@ describe('configuration helpers', () => {
3779
4335
  },
3780
4336
  )
3781
4337
  })
4338
+
4339
+ // A `for…of` loop reads one entry, works on it, and stops at the first failure, where a copy of the
4340
+ // list reads every entry first. Each case runs a policy reader over a recorded list and pins the
4341
+ // outcome and the entries it read, as the loop the reader replaced gave them.
4342
+ describe('policy readers consume a caller-supplied list one entry at a time', () => {
4343
+ const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
4344
+ const declarator = (): PolicyExpression => ({
4345
+ type: 'VariableDeclarator',
4346
+ range: [0, 0],
4347
+ id: name,
4348
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
4349
+ })
4350
+ const exported = (): PolicyExpression => ({
4351
+ type: 'ExportNamedDeclaration',
4352
+ range: [47, 63],
4353
+ declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
4354
+ })
4355
+ const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
4356
+ // A revoked proxy refuses every read, which fails the reader once it holds the entry.
4357
+ const refused =
4358
+ <T extends object>(entry: () => T) =>
4359
+ (): T => {
4360
+ const { proxy, revoke } = Proxy.revocable(entry(), {})
4361
+ revoke()
4362
+ return proxy
4363
+ }
4364
+ const throwing = [
4365
+ [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
4366
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
4367
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
4368
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
4369
+ [POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
4370
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
4371
+ ] as const
4372
+
4373
+ it.each([
4374
+ ...createPolicyLazyCases(
4375
+ 'statementToPolicyBindings',
4376
+ (declarations: PolicyExpression[]) =>
4377
+ statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
4378
+ (binding) => binding.name,
4379
+ ),
4380
+ { valid: declarator, refusal: refused(declarator) },
4381
+ [
4382
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4383
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4384
+ ...throwing,
4385
+ ],
4386
+ ),
4387
+ ...createPolicyLazyCases(
4388
+ 'reportDomain',
4389
+ (body: PolicyExpression[]) => {
4390
+ const reports: Array<string | undefined> = []
4391
+ reportDomain(
4392
+ {
4393
+ filename: '/ws/src/server/execution/run.ts',
4394
+ cwd: '/ws',
4395
+ sourceCode: { text: '', getAllComments: () => [] },
4396
+ report: (diagnostic) => {
4397
+ reports.push(diagnostic.messageId)
4398
+ },
4399
+ },
4400
+ { type: 'Program', range: [0, 0], body },
4401
+ )
4402
+ return reports
4403
+ },
4404
+ { valid: exported, refusal: refused(exported) },
4405
+ [
4406
+ [POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
4407
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4408
+ ...throwing,
4409
+ ],
4410
+ ),
4411
+ ...createPolicyLazyCases(
4412
+ 'programToPolicyDocs statements',
4413
+ (body: PolicyExpression[]) =>
4414
+ programToPolicyDocs(
4415
+ { type: 'Program', range: [0, 0], body },
4416
+ { text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
4417
+ ).map((doc) => doc.name),
4418
+ { valid: exported, refusal: refused(exported) },
4419
+ [
4420
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4421
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4422
+ ...throwing,
4423
+ ],
4424
+ ),
4425
+ ...createPolicyLazyCases(
4426
+ 'programToPolicyDocs comments',
4427
+ (comments: PolicyComment[]) =>
4428
+ programToPolicyDocs(
4429
+ { type: 'Program', range: [0, 0], body: [exported()] },
4430
+ { text: '/** One. */\n', getAllComments: () => comments },
4431
+ ).map((doc) => doc.name),
4432
+ { valid: comment, refusal: refused(comment) },
4433
+ [
4434
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4435
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4436
+ ...throwing,
4437
+ ],
4438
+ ),
4439
+ ...createPolicyLazyCases(
4440
+ 'programToPolicyDocs comments of a program with no export',
4441
+ (comments: PolicyComment[]) =>
4442
+ programToPolicyDocs(
4443
+ { type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
4444
+ { text: 'import x', getAllComments: () => comments },
4445
+ ).map((doc) => doc.name),
4446
+ { valid: comment, refusal: refused(comment) },
4447
+ [
4448
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4449
+ [POLICY_LAZY_SHAPES.dense, 'return []', []],
4450
+ [POLICY_LAZY_SHAPES.hole, 'return []', []],
4451
+ [POLICY_LAZY_SHAPES.holeFailure, 'return []', []],
4452
+ [POLICY_LAZY_SHAPES.failureFirst, 'return []', []],
4453
+ [POLICY_LAZY_SHAPES.refusalFirst, 'return []', []],
4454
+ [POLICY_LAZY_SHAPES.holeBetween, 'return []', []],
4455
+ [POLICY_LAZY_SHAPES.unlisted, 'return []', []],
4456
+ ],
4457
+ ),
4458
+ ])('$label', ({ observe, outcome, reads }) => {
4459
+ const read: number[] = []
4460
+ expect(
4461
+ observe((index) => {
4462
+ read.push(index)
4463
+ }),
4464
+ ).toBe(outcome)
4465
+ expect(read).toStrictEqual(reads)
4466
+ })
4467
+ })