@orkestrel/scaffold 0.0.101 → 0.0.103

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 (59) 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 +33 -18
  39. package/dist/host/configs/helpers.ts +209 -148
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/dotfiles/oxlintrc.json +1 -11
  42. package/dist/host/guides/scaffold.md +244 -170
  43. package/dist/host/manifest.json +36 -30
  44. package/dist/host/tests/config.test.ts +1263 -687
  45. package/dist/host/tests/policy.test.ts +19 -0
  46. package/dist/host/tests/setupPolicy.ts +1699 -1026
  47. package/dist/src/core/index.cjs +1595 -1686
  48. package/dist/src/core/index.cjs.map +1 -1
  49. package/dist/src/core/index.d.cts +202 -18
  50. package/dist/src/core/index.d.ts +202 -18
  51. package/dist/src/core/index.js +1590 -1687
  52. package/dist/src/core/index.js.map +1 -1
  53. package/dist/src/server/index.cjs +448 -392
  54. package/dist/src/server/index.cjs.map +1 -1
  55. package/dist/src/server/index.d.cts +1182 -1052
  56. package/dist/src/server/index.d.ts +1182 -1052
  57. package/dist/src/server/index.js +447 -395
  58. package/dist/src/server/index.js.map +1 -1
  59. 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,8 +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 {
60
- ARTIFACT_IMPORT_CASES,
73
+ COMPUTED_IMPORT_PATHS,
61
74
  collectSheets,
75
+ collectStudies,
62
76
  collectFrameworks,
63
77
  readConfigRecord,
64
78
  readConfigScript,
@@ -68,25 +82,29 @@ import {
68
82
  SHEET_POLICY_BARREL_PATTERN,
69
83
  SHEET_POLICY_ORDER_PATTERN,
70
84
  readImportDiagnostics,
85
+ createPolicyLazyCases,
71
86
  createPolicyScratch,
87
+ createPolicySparse,
72
88
  inspectPolicyConfiguration,
73
89
  inspectPolicyWiring,
74
90
  normalizePolicyFilename,
91
+ POLICY_EXTRACTOR,
92
+ POLICY_LAZY_SHAPES,
93
+ POLICY_PUBLISHES,
94
+ POLICY_ROOT,
75
95
  } from './setupPolicy.js'
76
96
  import { describe, expect, it } from 'vitest'
77
97
 
78
- const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
79
-
80
98
  describe('selected faces', () => {
81
99
  it('collects each selected sheet proof from the effective project root and rejects a wrong root', async () => {
82
- for (const face of collectSheets(root)) {
83
- const proofs = resolve(root, 'tests/src', face)
100
+ for (const face of collectSheets(POLICY_ROOT)) {
101
+ const proofs = resolve(POLICY_ROOT, 'tests/src', face)
84
102
  if (!existsSync(proofs)) continue
85
- const wrapper = resolve(root, `configs/src/vite.${face}.config.ts`)
103
+ const wrapper = resolve(POLICY_ROOT, `configs/src/vite.${face}.config.ts`)
86
104
  const loaded = await loadConfigFromFile(
87
105
  { command: 'serve', mode: 'test' },
88
106
  wrapper,
89
- root,
107
+ POLICY_ROOT,
90
108
  'silent',
91
109
  )
92
110
  if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
@@ -95,16 +113,19 @@ describe('selected faces', () => {
95
113
  if (include === undefined) throw new Error(`Missing sheet include ${face}`)
96
114
  // Vitest roots a file project at its wrapper directory unless test.root overrides it.
97
115
  const directory = resolve(
98
- root,
116
+ POLICY_ROOT,
99
117
  project.test?.root ?? dirname(wrapper),
100
118
  project.test?.dir || '.',
101
119
  )
102
120
  const collected = globSync(include, { cwd: directory }).map((path) =>
103
121
  resolve(directory, path),
104
122
  )
105
- const expected = [resolve(proofs, 'index.test.ts')]
106
- if (face === 'styles' && existsSync(resolve(proofs, 'themes/index.test.ts')))
107
- 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
+ ]
108
129
  expect(collected).toEqual(expect.arrayContaining(expected))
109
130
  const control = globSync(include, { cwd: dirname(wrapper) }).map((path) =>
110
131
  resolve(dirname(wrapper), path),
@@ -151,14 +172,59 @@ describe('selected faces', () => {
151
172
  }
152
173
  })
153
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
+
154
220
  it('loads each selected sheet and framework wrapper and checks its packaging and browser project', async () => {
155
221
  const manifest = readConfigRecord(
156
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
222
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
157
223
  )
158
224
  const scripts = readConfigRecord(manifest.scripts)
159
- const sheets = collectSheets(root)
160
- const frameworks = collectFrameworks(root)
161
- const wrappers = collectFaceWrappers(root)
225
+ const sheets = collectSheets(POLICY_ROOT)
226
+ const frameworks = collectFrameworks(POLICY_ROOT)
227
+ const wrappers = collectFaceWrappers(POLICY_ROOT)
162
228
  const readings: Array<{
163
229
  readonly axis: string
164
230
  readonly face: string
@@ -171,8 +237,8 @@ describe('selected faces', () => {
171
237
  for (const wrapper of wrappers) {
172
238
  const loaded = await loadConfigFromFile(
173
239
  { command: 'build', mode: 'test' },
174
- resolve(root, wrapper),
175
- root,
240
+ resolve(POLICY_ROOT, wrapper),
241
+ POLICY_ROOT,
176
242
  'silent',
177
243
  )
178
244
  if (loaded === null) throw new Error(`Unloaded wrapper ${wrapper}`)
@@ -199,7 +265,7 @@ describe('selected faces', () => {
199
265
  for (const { face, wrapper, project } of readings.filter((reading) =>
200
266
  sheets.includes(reading.face),
201
267
  )) {
202
- expect(readFileSync(resolve(root, wrapper), 'utf8')).toContain('sheetProject(')
268
+ expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
203
269
  expect(inspectSheetConfiguration(project)).toEqual([])
204
270
  const exports = readConfigRecord(manifest.exports)
205
271
  expect(exports[`./${face}`]).toBe(`./dist/src/${face}/index.css`)
@@ -222,23 +288,23 @@ describe('selected faces', () => {
222
288
  for (const { wrapper } of readings.filter(
223
289
  (reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
224
290
  )) {
225
- const text = readFileSync(resolve(root, wrapper), 'utf8')
291
+ const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
226
292
  expect(text).toContain('rewriteCoreSpecifier')
227
293
  expect(text).toContain('rewriteBrowserSpecifier')
228
294
  }
229
- if (!existsSync(resolve(root, 'src/styles/themes/index.scss'))) return
295
+ if (!existsSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'))) return
230
296
  const loaded = await loadConfigFromFile(
231
297
  { command: 'build', mode: 'test' },
232
- resolve(root, 'configs/src/vite.themes.config.ts'),
233
- root,
298
+ resolve(POLICY_ROOT, 'configs/src/vite.themes.config.ts'),
299
+ POLICY_ROOT,
234
300
  'silent',
235
301
  )
236
302
  if (loaded === null) throw new Error('Unloaded themes wrapper')
237
303
  expect(loaded.config.build?.outDir).toBe('dist/src/styles/themes')
238
- const barrel = readFileSync(resolve(root, 'src/styles/themes/index.scss'), 'utf8')
304
+ const barrel = readFileSync(resolve(POLICY_ROOT, 'src/styles/themes/index.scss'), 'utf8')
239
305
  expect(readSheetPrelude(barrel)).toMatch(SHEET_POLICY_BARREL_PATTERN)
240
306
  expect(
241
- readSheetPrelude(readFileSync(resolve(root, 'src/styles/_tokens.scss'), 'utf8')),
307
+ readSheetPrelude(readFileSync(resolve(POLICY_ROOT, 'src/styles/_tokens.scss'), 'utf8')),
242
308
  ).toMatch(SHEET_POLICY_ORDER_PATTERN)
243
309
  const target = sheets.includes('styles') ? 'styles' : 'themes'
244
310
  const command = sheets.includes('styles')
@@ -272,25 +338,29 @@ describe('selected faces', () => {
272
338
  it('resolves showcase and journey modes for every occupied application', async () => {
273
339
  const applications = [
274
340
  'browser',
275
- ...collectFrameworks(root)
341
+ ...collectFrameworks(POLICY_ROOT)
276
342
  .filter((face) => face.startsWith('app/'))
277
343
  .map((face) => face.slice(4)),
278
344
  ]
279
345
  const manifest = readConfigRecord(
280
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
346
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
281
347
  )
282
348
  const scripts = readConfigRecord(manifest.scripts)
283
349
  const factories = readConfigRecord(rootConfiguration)
284
350
  const showcases = ['configs/app/vite.showcase.config.ts'].filter(
285
- (path) => existsSync(resolve(root, path)) || existsSync(resolve(root, 'showcase')),
351
+ (path) =>
352
+ existsSync(resolve(POLICY_ROOT, path)) || existsSync(resolve(POLICY_ROOT, 'showcase')),
286
353
  )
287
354
  for (const showcase of showcases) {
288
- 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
+ )
289
359
  expect(wrapper).toContain('defineConfig(({ mode }) => appShowcase(mode))')
290
360
  expect(wrapper).not.toContain('dist/showcase')
291
- expect(readFileSync(resolve(root, '.prettierignore'), 'utf8').split(/\r\n|\n/u)).toContain(
292
- 'showcase/',
293
- )
361
+ expect(
362
+ readFileSync(resolve(POLICY_ROOT, '.prettierignore'), 'utf8').split(/\r\n|\n/u),
363
+ ).toContain('showcase/')
294
364
  for (const application of applications) {
295
365
  const script = 'build:showcase' + (application === 'browser' ? '' : ':' + application)
296
366
  expect(readConfigScript(scripts, script)).toContain('configs/app/vite.showcase.config.ts')
@@ -302,27 +372,27 @@ describe('selected faces', () => {
302
372
  }
303
373
  const loaded = await loadConfigFromFile(
304
374
  { command: 'build', mode: application },
305
- resolve(root, showcase),
306
- root,
375
+ resolve(POLICY_ROOT, showcase),
376
+ POLICY_ROOT,
307
377
  'silent',
308
378
  )
309
379
  if (loaded === null) throw new Error('Unloaded showcase wrapper')
310
380
  const output = loaded.config.build?.outDir
311
381
  if (output === undefined) throw new Error('Missing showcase output')
312
- expect(relative(root, output)).toBe('showcase')
382
+ expect(relative(POLICY_ROOT, output)).toBe('showcase')
313
383
  expect(() =>
314
- expect(relative(root, resolve(root, 'dist/showcase'))).toBe('showcase'),
384
+ expect(relative(POLICY_ROOT, resolve(POLICY_ROOT, 'dist/showcase'))).toBe('showcase'),
315
385
  ).toThrow('expected')
316
386
  expect(loaded.config.build?.emptyOutDir).toBe(false)
317
387
  }
318
388
  expect(scripts.show).toBeUndefined()
319
389
  }
320
- if (!existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) return
390
+ if (!existsSync(resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'))) return
321
391
  for (const application of applications) {
322
392
  const loaded = await loadConfigFromFile(
323
393
  { command: 'serve', mode: application },
324
- resolve(root, 'configs/app/vite.journey.config.ts'),
325
- root,
394
+ resolve(POLICY_ROOT, 'configs/app/vite.journey.config.ts'),
395
+ POLICY_ROOT,
326
396
  'silent',
327
397
  )
328
398
  if (
@@ -331,8 +401,8 @@ describe('selected faces', () => {
331
401
  loaded.config.test.projects.length === 0
332
402
  )
333
403
  throw new Error('Missing journey projects')
334
- const variants = new Set<string>()
335
- 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[] }> = []
336
406
  for (const factory of loaded.config.test.projects) {
337
407
  if (typeof factory !== 'function') throw new Error('Missing journey factory')
338
408
  const project = readConfigRecord(
@@ -344,12 +414,14 @@ describe('selected faces', () => {
344
414
  const provide = readConfigRecord(test.provide)
345
415
  if (typeof provide.variant !== 'string' || !Array.isArray(provide.variants))
346
416
  throw new Error('Missing journey variant values')
347
- variants.add(provide.variant)
348
- for (const value of provide.variants) {
349
- const variant = readConfigRecord(value)
350
- if (typeof variant.name !== 'string') throw new Error('Unnamed journey variant')
351
- declared.add(variant.name)
352
- }
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
+ })
353
425
  const selected = provide.variants
354
426
  .map(readConfigRecord)
355
427
  .find((variant) => variant.name === provide.variant)
@@ -371,7 +443,51 @@ describe('selected faces', () => {
371
443
  expect.arrayContaining(['@orkestrel/test', '@orkestrel/test/browser']),
372
444
  )
373
445
  }
374
- 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
+ }
375
491
  }
376
492
  const factory = factories.appJourney
377
493
  if (typeof factory !== 'function') throw new Error('Missing application journey')
@@ -384,15 +500,56 @@ describe('selected faces', () => {
384
500
  ).toThrow('not declared')
385
501
  })
386
502
 
387
- 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 () => {
388
504
  const factories = readConfigRecord(rootConfiguration)
389
505
  const manifest = readConfigRecord(
390
- JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')),
506
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8')),
391
507
  )
392
508
  const dependencies = {
393
509
  ...readConfigRecord(manifest.dependencies ?? {}),
394
510
  ...readConfigRecord(manifest.devDependencies ?? {}),
395
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
+ }
396
553
  for (const name of [
397
554
  'srcBrowser',
398
555
  'appBrowser',
@@ -423,9 +580,13 @@ describe('selected faces', () => {
423
580
  ...(dependencies['@orkestrel/contract'] === undefined ? [] : ['@orkestrel/contract']),
424
581
  ]),
425
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
+ }
426
587
  }
427
588
  for (const proof of ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts'].filter((path) =>
428
- existsSync(resolve(root, path)),
589
+ existsSync(resolve(POLICY_ROOT, path)),
429
590
  )) {
430
591
  const factory = factories.setupBrowser
431
592
  if (typeof factory !== 'function') throw new Error('Missing browser setup project')
@@ -444,59 +605,32 @@ describe('selected faces', () => {
444
605
  })
445
606
  })
446
607
 
447
- // The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
448
- // a physical directory rather than from a `src` entry of any shape. A regular file, an empty
449
- // directory, a directory holding no recognized environment, a symbolic link, and an entry this host
450
- // refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
451
- // put every one of them under the face proof. The inspection is caught rather than excused by
452
- // option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
453
- // throw on this line is a collection error that takes this whole file down in that target rather
454
- // than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
455
- // false for every inspection error, and this reads them the same way. This is the derivation
456
- // `targetToEnvironments` uses, which filters the environment names by a contained physical
457
- // directory, and it answers the same question `blueprintToScripts`
458
- // answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
459
- // no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
460
- // proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
461
- // axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
462
- // fail the proof. The manifest cases read the same fact from their own `private` flag and name it
463
- // `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
464
- // workspace whose manifest and shape disagree fails one of the two rather than neither.
465
- const publishes = ['core', 'browser', 'server'].some((environment) => {
466
- try {
467
- const entry = lstatSync(resolve(root, 'src', environment))
468
- return entry.isDirectory() && !entry.isSymbolicLink()
469
- } catch {
470
- return false
471
- }
472
- })
473
-
474
- // A declaration roll-up loads the extractor package, which only a workspace publishing source from
475
- // `src` installs. The resolution below is the mechanism its proof is conditioned on.
476
- let extractorPath: string | undefined
477
- try {
478
- extractorPath = createRequire(import.meta.url).resolve('@microsoft/api-extractor')
479
- } catch {
480
- extractorPath = undefined
481
- }
482
-
483
608
  describe('root configuration', () => {
484
609
  it('resolves every declared alias to its real entry', () => {
485
610
  const aliases = configuration.resolve?.alias
486
611
  if (typeof aliases !== 'object' || aliases === null || Array.isArray(aliases)) {
487
612
  throw new Error('The root configuration carries no alias record')
488
613
  }
489
- const required = new Map<string, string>()
490
- for (const axis of ['src', 'app']) {
491
- for (const environment of ['core', 'browser', 'server']) {
492
- const path = `${axis}/${environment}/index.ts`
493
- if (existsSync(resolve(root, path))) required.set(`@${axis}/${environment}`, path)
494
- }
495
- }
496
- for (const face of collectSheets(root)) required.set(`@src/${face}`, `src/${face}/index.ts`)
497
- 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
+ ])
498
630
  const declared = new Map(Object.entries(tsconfig.compilerOptions.paths))
499
- 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)
500
634
  if (required.size === 0) {
501
635
  if (declared.size !== 0 || Object.keys(aliases).length !== 0)
502
636
  throw new Error('The themes-only workspace declares an unexpected alias')
@@ -515,138 +649,146 @@ describe('root configuration', () => {
515
649
  if (path === undefined) throw new Error(`${key} carries no target`)
516
650
  const target = resolveWorkspacePath(path)
517
651
  expect(existsSync(target)).toBe(true)
518
- expect(target).toBe(resolve(root, expected))
652
+ expect(target).toBe(resolve(POLICY_ROOT, expected))
519
653
  expect(Object.getOwnPropertyDescriptor(aliases, key)?.value).toBe(target)
520
654
  }
521
655
  })
522
656
 
523
657
  it('registers every workspace project with its fixed include and setup files', () => {
524
- const expected = new Map<
525
- string,
526
- {
527
- readonly benchmark?: readonly string[]
528
- readonly include: string | readonly string[]
529
- readonly parallel?: boolean
530
- readonly pool?: string
531
- readonly setup: readonly string[]
532
- }
533
- >()
534
- if (existsSync(resolve(root, 'src/core'))) {
535
- expected.set('src:core', {
536
- include: 'tests/src/core/**/*.test.ts',
537
- setup: ['./tests/setup.ts'],
538
- })
539
- }
540
- if (existsSync(resolve(root, 'src/browser'))) {
541
- expected.set('src:browser', {
542
- include: 'tests/src/browser/**/*.test.ts',
543
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
544
- })
545
- }
546
- if (existsSync(resolve(root, 'src/server'))) {
547
- expected.set('src:server', {
548
- include: 'tests/src/server/**/*.test.ts',
549
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
550
- })
551
- }
552
- if (existsSync(resolve(root, 'src/bin'))) {
553
- expected.set('src:bin', {
554
- include: 'tests/src/bin/**/*.test.ts',
555
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
556
- })
557
- }
558
- if (existsSync(resolve(root, 'app/core'))) {
559
- expected.set('app:core', {
560
- include: 'tests/app/core/**/*.test.ts',
561
- setup: ['./tests/setup.ts'],
562
- })
563
- }
564
- if (existsSync(resolve(root, 'app/browser'))) {
565
- expected.set('app:browser', {
566
- include: 'tests/app/browser/**/*.test.ts',
567
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
568
- })
569
- }
570
- if (existsSync(resolve(root, 'app/server'))) {
571
- expected.set('app:server', {
572
- include: 'tests/app/server/**/*.test.ts',
573
- setup: ['./tests/setup.ts', './tests/setupServer.ts'],
574
- })
575
- }
576
- // The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
577
- // project it registers runs the mirrored proofs under `tests/agents/`.
578
- if (existsSync(resolve(root, 'configs/agents/tsconfig.skills.json'))) {
579
- expected.set('skills', {
580
- include: 'tests/agents/**/*.test.ts',
581
- setup: ['./tests/setup.ts'],
582
- })
583
- }
584
- for (const label of [
585
- 'policy',
586
- 'config',
587
- 'guides',
588
- 'conformance',
589
- 'distribution',
590
- 'integration',
591
- ]) {
592
- if (!existsSync(resolve(root, `tests/${label}.test.ts`))) continue
593
- const setup = ['./tests/setup.ts']
594
- if (label === 'conformance') setup.push('./tests/setupServer.ts')
595
- if (
596
- label === 'integration' &&
597
- (collectSheets(root).length > 0 ||
598
- existsSync(resolve(root, 'src/styles/themes/index.scss')))
599
- )
600
- setup.push('./tests/setupBrowser.ts', './tests/setupStyles.ts')
601
- expected.set(label, {
602
- include: `tests/${label}.test.ts`,
603
- setup,
604
- })
605
- }
606
658
  const browserProofs = ['tests/setupBrowser.test.ts', 'tests/setupStyles.test.ts']
607
- const setupProofs = globSync('tests/setup*.test.ts', { cwd: root })
659
+ const setupProofs = globSync('tests/setup*.test.ts', { cwd: POLICY_ROOT })
608
660
  .map((path) => path.replaceAll('\\', '/'))
609
661
  .filter((path) => /^tests\/setup[^/]*\.test\.ts$/u.test(path))
610
- if (setupProofs.some((path) => !browserProofs.includes(path))) {
611
- expected.set('setup', {
612
- include: 'tests/setup*.test.ts',
613
- setup: ['./tests/setup.ts'],
614
- })
615
- }
616
- if (setupProofs.some((path) => browserProofs.includes(path))) {
617
- expected.set('setup:browser', {
618
- include: browserProofs,
619
- setup: ['./tests/setup.ts', './tests/setupBrowser.ts'],
620
- })
621
- }
622
- // The live-service project covers a directory rather than one proof, so its
623
- // readiness module is the fact that selects it. A suite beneath
624
- // `tests/service` with no setup module is a project nothing configures.
625
- if (existsSync(resolve(root, 'tests/setupService.ts'))) {
626
- expected.set('service', {
627
- include: 'tests/service/**/*.test.ts',
628
- setup: ['./tests/setup.ts', './tests/setupService.ts'],
629
- })
630
- }
631
- expected.set('probe', {
632
- benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
633
- include: 'tmp/probes/**/*.test.ts',
634
- parallel: false,
635
- pool: 'threads',
636
- setup: ['./tests/setup.ts'],
637
- })
638
- // A row that is a configuration rather than a factory. Every generated
639
- // workspace registers the factory itself, so this shape is required by the
640
- // expectation rather than observed: the proof exercises that resolution wherever it runs
641
- // instead of only where a hand-written configuration happens to produce it.
642
- 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
+ ])
643
785
 
644
786
  const projects = configuration.test?.projects
645
787
  if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
646
- let extraLoaded = false
788
+ const extraLoads: string[] = []
647
789
  const control = Object.defineProperty(
648
790
  () => {
649
- extraLoaded = true
791
+ extraLoads.push('control')
650
792
  return {
651
793
  test: {
652
794
  name: { label: 'control' },
@@ -666,109 +808,107 @@ describe('root configuration', () => {
666
808
  },
667
809
  }
668
810
  const controlled = projects.concat(control, concrete)
669
- const configured = new Map<
670
- string,
671
- {
672
- readonly benchmark?: readonly string[]
673
- readonly include: string | readonly string[]
674
- readonly parallel?: boolean
675
- readonly pool?: string
676
- readonly setup: readonly string[]
677
- }
678
- >()
679
- for (const [requiredLabel] of expected) {
680
- const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
681
- letter.toUpperCase(),
682
- )
683
- // A row is either the factory named for the project or the configuration
684
- // that project resolves to, and a required project is found as whichever
685
- // it is. Only the required row is read, so an extra factory is still
686
- // selected by name and never called.
687
- const row = controlled.find((candidate) => {
688
- if (typeof candidate === 'function') return candidate.name === factoryName
689
- if (typeof candidate !== 'object' || candidate === null) return false
690
- const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
691
- if (typeof block !== 'object' || block === null) return false
692
- const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
693
- if (typeof named !== 'object' || named === null) return false
694
- return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
695
- })
696
- if (row === undefined) {
697
- throw new Error(`${requiredLabel} has no project factory or configuration`)
698
- }
699
- const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
700
- if (typeof project !== 'object' || project === null) {
701
- throw new Error('A project factory returned no configuration')
702
- }
703
- const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
704
- if (typeof test !== 'object' || test === null) {
705
- throw new Error('A project configuration carries no test block')
706
- }
707
- const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
708
- const include: unknown = Object.getOwnPropertyDescriptor(test, 'include')?.value
709
- const exclude: unknown = Object.getOwnPropertyDescriptor(test, 'exclude')?.value
710
- const setup: unknown = Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value
711
- const label =
712
- typeof name === 'object' && name !== null
713
- ? Object.getOwnPropertyDescriptor(name, 'label')?.value
714
- : undefined
715
- if (
716
- typeof label !== 'string' ||
717
- !Array.isArray(include) ||
718
- typeof include[0] !== 'string' ||
719
- !Array.isArray(setup) ||
720
- !setup.every((path) => typeof path === 'string')
721
- ) {
722
- throw new Error('A project does not expose one include and its setup files')
723
- }
724
- const effective = include.filter(
725
- (path) => typeof path === 'string' && (!Array.isArray(exclude) || !exclude.includes(path)),
726
- )
727
- if (
728
- effective.length !== (label === 'setup:browser' ? 2 : 1) ||
729
- typeof effective[0] !== 'string'
730
- ) {
731
- throw new Error(`${label} does not resolve to its required includes`)
732
- }
733
- for (const required of label === 'setup' ? browserProofs : [])
734
- expect(exclude).toContain(required)
735
- if (label === 'probe') {
736
- const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
737
- const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
738
- const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
739
- if (typeof benchmark !== 'object' || benchmark === null) {
740
- 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`)
741
831
  }
742
- const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
743
- benchmark,
744
- 'include',
745
- )?.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
746
848
  if (
747
- !Array.isArray(benchmarkInclude) ||
748
- !benchmarkInclude.every((path) => typeof path === 'string') ||
749
- typeof parallel !== 'boolean' ||
750
- 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')
751
854
  ) {
752
- 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')
753
856
  }
754
- configured.set(label, {
755
- benchmark: benchmarkInclude,
756
- include: effective[0],
757
- parallel,
758
- pool,
759
- setup: [...new Set(setup)],
760
- })
761
- continue
762
- }
763
- configured.set(label, {
764
- include: label === 'setup:browser' ? effective : effective[0],
765
- setup: [...new Set(setup)],
766
- })
767
- }
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
+ )
768
908
 
769
909
  // Required projects come from present source and test paths. Each factory is selected by name
770
910
  // before its result is read. Extra factories are ignored without validating their result shape.
771
- expect(extraLoaded).toBe(false)
911
+ expect(extraLoads).toStrictEqual([])
772
912
  for (const [label, project] of expected) expect(configured.get(label)).toStrictEqual(project)
773
913
  for (const label of ['setup', 'setup:browser']) {
774
914
  const name = label === 'setup' ? 'setup' : 'setupBrowser'
@@ -824,16 +964,20 @@ describe('root configuration', () => {
824
964
  value: 'spreading',
825
965
  })
826
966
  const wrappers = projects.filter((entry) => typeof entry === 'string')
827
- const sheets = collectSheets(root)
828
- const expectedWrappers = sheets.map((face) => `./configs/src/vite.${face}.config.ts`)
829
- if (!sheets.includes('styles') && existsSync(resolve(root, 'src/styles/themes/index.scss')))
830
- 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
+ ]
831
975
  expect([...wrappers].sort()).toEqual(expectedWrappers.sort())
832
976
  for (const wrapper of wrappers) {
833
977
  const loaded = await loadConfigFromFile(
834
978
  { command: 'serve', mode: invocation.mode },
835
- resolve(root, wrapper),
836
- root,
979
+ resolve(POLICY_ROOT, wrapper),
980
+ POLICY_ROOT,
837
981
  'silent',
838
982
  )
839
983
  if (loaded === null) throw new Error(`Unloaded file project ${wrapper}`)
@@ -898,28 +1042,32 @@ describe('root configuration', () => {
898
1042
  })
899
1043
 
900
1044
  it('requires and validates every selected target wrapper', async () => {
901
- const required: string[] = []
902
- for (const axis of ['src', 'app']) {
903
- for (const environment of ['core', 'browser', 'server']) {
904
- if (!existsSync(resolve(root, axis, environment))) continue
905
- required.push(`configs/${axis}/tsconfig.${environment}.json`)
906
- if (axis === 'src' || environment !== 'core') {
907
- required.push(`configs/${axis}/vite.${environment}.config.ts`)
908
- }
909
- }
910
- }
911
- for (const face of collectFrameworks(root)) {
912
- required.push(`configs/${face.replace('/', '/tsconfig.')}.json`)
913
- }
914
- if (existsSync(resolve(root, 'src/bin'))) {
915
- required.push('configs/src/tsconfig.bin.json', 'configs/src/vite.bin.config.ts')
916
- }
917
- if (existsSync(resolve(root, 'configs/app/vite.showcase.config.ts'))) {
918
- required.push('configs/app/vite.showcase.config.ts')
919
- }
920
- if (existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) {
921
- required.push('configs/app/vite.journey.config.ts')
922
- }
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
+ ]
923
1071
  if (required[0] === undefined) {
924
1072
  throw new Error('The workspace selects no configuration target')
925
1073
  }
@@ -930,10 +1078,9 @@ describe('root configuration', () => {
930
1078
  'configs/src/tsconfig.*.json',
931
1079
  'configs/app/tsconfig.*.json',
932
1080
  ],
933
- { cwd: root },
1081
+ { cwd: POLICY_ROOT },
934
1082
  ).map((path) => path.replaceAll('\\', '/'))
935
1083
  const extra = 'configs/app/vite.core.config.ts'
936
- const controlled = found.concat(extra)
937
1084
  const planted = createPolicyScratch({ prefix: 'config-journey-' })
938
1085
  const journey = 'configs/app/vite.journey.config.ts'
939
1086
  planted.write(
@@ -952,9 +1099,11 @@ describe('root configuration', () => {
952
1099
  }\n`,
953
1100
  )
954
1101
  const journeys: object[][] = []
955
- const targets = required.map((wrapper) => ({ wrapper, directory: root }))
956
- targets.push({ wrapper: journey, directory: planted.path })
957
- 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]
958
1107
 
959
1108
  // Required wrappers come from selected src/app targets. Only that set is loaded and validated.
960
1109
  // Extra wrappers remain in the found population but are ignored before their content is read.
@@ -1009,7 +1158,7 @@ describe('root configuration', () => {
1009
1158
  : environment === 'showcase'
1010
1159
  ? 'showcase'
1011
1160
  : `dist/${axis}/${environment}`
1012
- if (resolve(root, output) !== resolve(root, expected)) {
1161
+ if (resolve(POLICY_ROOT, output) !== resolve(POLICY_ROOT, expected)) {
1013
1162
  throw new Error(`${wrapper} resolves to the wrong output`)
1014
1163
  }
1015
1164
  continue
@@ -1024,7 +1173,7 @@ describe('root configuration', () => {
1024
1173
  if (axis === undefined || environment === undefined) {
1025
1174
  throw new Error(`${wrapper} carries no TypeScript scope`)
1026
1175
  }
1027
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
1176
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8'))
1028
1177
  if (typeof parsed !== 'object' || parsed === null) {
1029
1178
  throw new Error(`${wrapper} is not a TypeScript configuration record`)
1030
1179
  }
@@ -1058,8 +1207,7 @@ describe('root configuration', () => {
1058
1207
  }
1059
1208
  }
1060
1209
  for (const tests of journeys) {
1061
- const names: string[] = []
1062
- for (const test of tests) {
1210
+ const names = tests.map((test) => {
1063
1211
  expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
1064
1212
  'tests/app/browser/integration.test.ts',
1065
1213
  ])
@@ -1097,9 +1245,9 @@ describe('root configuration', () => {
1097
1245
  )
1098
1246
  )
1099
1247
  throw new Error('A journey variant is absent from its declared set')
1100
- expect(names).not.toContain(variant)
1101
- names.push(variant)
1102
- }
1248
+ return variant
1249
+ })
1250
+ expect(new Set(names).size).toBe(names.length)
1103
1251
  }
1104
1252
  } finally {
1105
1253
  planted.destroy()
@@ -1113,13 +1261,13 @@ describe('root configuration', () => {
1113
1261
  expect(() => {
1114
1262
  for (const wrapper of controlRequired) expect(controlFound).toContain(wrapper)
1115
1263
  }).toThrow(/vite\.browser/u)
1116
- expect(() => expect(resolve(root, 'dist/actual')).toBe(resolve(root, 'dist/control'))).toThrow(
1117
- /expected/u,
1118
- )
1264
+ expect(() =>
1265
+ expect(resolve(POLICY_ROOT, 'dist/actual')).toBe(resolve(POLICY_ROOT, 'dist/control')),
1266
+ ).toThrow(/expected/u)
1119
1267
  })
1120
1268
 
1121
1269
  it('registers proof scripts in the correct gate', () => {
1122
- const manifest: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
1270
+ const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
1123
1271
  if (typeof manifest !== 'object' || manifest === null) {
1124
1272
  throw new Error('The package manifest is not a record')
1125
1273
  }
@@ -1134,34 +1282,34 @@ describe('root configuration', () => {
1134
1282
  const integration = Object.getOwnPropertyDescriptor(scripts, 'test:integration')?.value
1135
1283
  const conformance = Object.getOwnPropertyDescriptor(scripts, 'test:conformance')?.value
1136
1284
  const service = Object.getOwnPropertyDescriptor(scripts, 'test:service')?.value
1285
+ const studies = Object.getOwnPropertyDescriptor(scripts, 'test:studies')?.value
1137
1286
  const publish = Object.getOwnPropertyDescriptor(scripts, 'prepublishOnly')?.value
1138
- const hasIntegration = existsSync(resolve(root, 'tests/integration.test.ts'))
1287
+ const hasIntegration = existsSync(resolve(POLICY_ROOT, 'tests/integration.test.ts'))
1139
1288
  // The optional proofs are read off the registered project set rather than off
1140
1289
  // their files, because the defect this measures is a registered project no
1141
1290
  // gate runs. A project selected by a path that is not yet there is still
1142
1291
  // registered, and it is exactly the one whose script goes missing.
1143
1292
  const rows = configuration.test?.projects
1144
1293
  if (!Array.isArray(rows)) throw new Error('The root configuration carries no projects')
1145
- const registered = new Set<string>()
1146
- for (const row of rows) {
1147
- if (typeof row === 'function') {
1148
- registered.add(row.name)
1149
- continue
1150
- }
1151
- if (typeof row !== 'object' || row === null) continue
1152
- const block: unknown = Object.getOwnPropertyDescriptor(row, 'test')?.value
1153
- if (typeof block !== 'object' || block === null) continue
1154
- const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
1155
- if (typeof named !== 'object' || named === null) continue
1156
- const label: unknown = Object.getOwnPropertyDescriptor(named, 'label')?.value
1157
- if (typeof label === 'string') registered.add(label)
1158
- }
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
+ )
1159
1306
  // The population must be able to answer both ways before either answer counts.
1160
1307
  expect(registered.has('config')).toBe(true)
1161
1308
  expect(registered.has('control')).toBe(false)
1162
1309
  const hasConformance = registered.has('conformance')
1163
1310
  const hasDistribution = registered.has('distribution')
1164
1311
  const hasService = registered.has('service')
1312
+ const hasStudies = registered.has('studies')
1165
1313
  expect(config).toBe(
1166
1314
  'vitest run --config vite.config.ts --no-cache --reporter=dot --project config',
1167
1315
  )
@@ -1212,10 +1360,28 @@ describe('root configuration', () => {
1212
1360
  expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
1213
1361
  hasService && manifestPublishes,
1214
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)
1215
1381
  })
1216
1382
 
1217
1383
  it('rebuilds publishing workspaces before packing', () => {
1218
- const manifest: unknown = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
1384
+ const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
1219
1385
  if (typeof manifest !== 'object' || manifest === null) {
1220
1386
  throw new Error('The package manifest is not a record')
1221
1387
  }
@@ -1237,7 +1403,9 @@ describe('root configuration', () => {
1237
1403
  it('keeps the committed host inventory aligned with the vendored checkout bytes', async () => {
1238
1404
  // Run this gate against a quiescent checkout. It compares two reads and cannot
1239
1405
  // distinguish stale committed data from a source edit made while it runs.
1240
- 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
+ )
1241
1409
  if (typeof packageValue !== 'object' || packageValue === null) {
1242
1410
  throw new Error('The package manifest is not a record')
1243
1411
  }
@@ -1248,7 +1416,7 @@ describe('root configuration', () => {
1248
1416
  const generator: unknown = Object.getOwnPropertyDescriptor(scripts, 'build:inventory')?.value
1249
1417
  expect(generator === undefined || typeof generator === 'string').toBe(true)
1250
1418
  if (generator === undefined) {
1251
- if (existsSync(resolve(root, 'host.json'))) {
1419
+ if (existsSync(resolve(POLICY_ROOT, 'host.json'))) {
1252
1420
  throw new Error('A committed host inventory exists without a generator')
1253
1421
  }
1254
1422
  return
@@ -1256,17 +1424,17 @@ describe('root configuration', () => {
1256
1424
  if (typeof generator !== 'string') {
1257
1425
  throw new Error('The host inventory generator is not a script')
1258
1426
  }
1259
- const committed = resolve(root, 'host.json')
1427
+ const committed = resolve(POLICY_ROOT, 'host.json')
1260
1428
  if (!existsSync(committed)) {
1261
1429
  throw new Error('The committed host inventory is absent at host.json')
1262
1430
  }
1263
- const helper = resolve(root, 'src/server/helpers.ts')
1431
+ const helper = resolve(POLICY_ROOT, 'src/server/helpers.ts')
1264
1432
  if (!existsSync(helper)) {
1265
1433
  throw new Error('The committed host inventory has no server stager')
1266
1434
  }
1267
1435
  const server = await createServer({
1268
1436
  configFile: false,
1269
- root,
1437
+ root: POLICY_ROOT,
1270
1438
  ...(configuration.resolve === undefined ? {} : { resolve: configuration.resolve }),
1271
1439
  server: { middlewareMode: true },
1272
1440
  })
@@ -1283,49 +1451,47 @@ describe('root configuration', () => {
1283
1451
  const workspace = scratch.path
1284
1452
  try {
1285
1453
  const generated = join(workspace, 'host.json')
1286
- Reflect.apply(stage, undefined, [root, generated])
1454
+ Reflect.apply(stage, undefined, [POLICY_ROOT, generated])
1287
1455
  const generatedText = readFileSync(generated, 'utf8')
1288
1456
  const committedText = readFileSync(committed, 'utf8')
1289
1457
  const values: readonly unknown[] = [JSON.parse(generatedText), JSON.parse(committedText)]
1290
- const indexes: Array<Map<string, string>> = []
1291
- for (const value of values) {
1458
+ const indexes = values.map((value) => {
1292
1459
  if (typeof value !== 'object' || value === null) {
1293
1460
  throw new Error('A host inventory is not a record')
1294
1461
  }
1295
1462
  const entries: unknown = Object.getOwnPropertyDescriptor(value, 'entries')?.value
1296
1463
  if (!Array.isArray(entries)) throw new Error('A host inventory carries no entry list')
1297
- const index = new Map<string, string>()
1298
- for (const entry of entries) {
1299
- if (typeof entry !== 'object' || entry === null) {
1300
- throw new Error('A host inventory carries a malformed entry')
1301
- }
1302
- const destination: unknown = Object.getOwnPropertyDescriptor(
1303
- entry,
1304
- 'destination',
1305
- )?.value
1306
- const digest: unknown = Object.getOwnPropertyDescriptor(entry, 'digest')?.value
1307
- if (typeof destination !== 'string' || typeof digest !== 'string') {
1308
- throw new Error('A host inventory entry carries no destination digest')
1309
- }
1310
- index.set(destination, digest)
1311
- }
1312
- indexes.push(index)
1313
- }
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
+ })
1314
1481
  const generatedIndex = indexes[0]
1315
1482
  const committedIndex = indexes[1]
1316
1483
  if (generatedIndex === undefined || committedIndex === undefined) {
1317
1484
  throw new Error('The host inventories were not indexed')
1318
1485
  }
1319
1486
  if (generatedText !== committedText) {
1320
- const stale: string[] = []
1321
- for (const [destination, digest] of generatedIndex) {
1322
- if (committedIndex.get(destination) !== digest) stale.push(destination)
1323
- }
1324
- for (const destination of committedIndex.keys()) {
1325
- if (!generatedIndex.has(destination)) stale.push(destination)
1326
- }
1487
+ const stale = [
1488
+ ...[...generatedIndex].flatMap(([destination, digest]) =>
1489
+ committedIndex.get(destination) === digest ? [] : [destination],
1490
+ ),
1491
+ ...[...committedIndex.keys()].filter((destination) => !generatedIndex.has(destination)),
1492
+ ]
1327
1493
  throw new Error(
1328
- `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'}`,
1329
1495
  )
1330
1496
  }
1331
1497
  console.info(`host-inventory: entries=${generatedIndex.size}`)
@@ -1338,7 +1504,7 @@ describe('root configuration', () => {
1338
1504
  })
1339
1505
 
1340
1506
  it('keeps policy rules active across every linted workspace path', () => {
1341
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
1507
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
1342
1508
  expect(inspectPolicyConfiguration(parsed)).toEqual([])
1343
1509
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1344
1510
  throw new Error('The Oxlint configuration is not a record')
@@ -1361,7 +1527,7 @@ describe('root configuration', () => {
1361
1527
  })
1362
1528
 
1363
1529
  it('omits the audit-confirmed dead policy type exports', () => {
1364
- const source = readFileSync(resolve(root, 'configs/policy.ts'), 'utf8')
1530
+ const source = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
1365
1531
  expect(source).not.toMatch(/\bPolicy(?:Call|ClassMember)\b/u)
1366
1532
  })
1367
1533
  })
@@ -2533,6 +2699,85 @@ createSomething({
2533
2699
  expect(commentToPolicyParagraph({ type: 'Block', value: '* ', range: [0, 0] })).toBe('')
2534
2700
  })
2535
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
+
2536
2781
  it('reads the opening word of a paragraph as its letters alone', () => {
2537
2782
  expect(paragraphToPolicyOpener('Creates a control.')).toBe('Creates')
2538
2783
  expect(paragraphToPolicyOpener('"Creates" a control.')).toBe('Creates')
@@ -2576,72 +2821,77 @@ createSomething({
2576
2821
  expect(DATA_SOURCE_FILES).not.toContain('plugins.ts')
2577
2822
  })
2578
2823
 
2579
- it('admits computed ESM imports only in artifact loaders while refusing computed require', () => {
2580
- const fixture = createPolicyScratch({ prefix: 'artifact-imports-' })
2824
+ it('pins the global dynamic-require setting to esmodule false with no override touching it', () => {
2825
+ const configured = readConfigRecord(
2826
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2827
+ )
2828
+ expect(readConfigRecord(configured.rules)['import/no-dynamic-require']).toEqual([
2829
+ 'error',
2830
+ { esmodule: false },
2831
+ ])
2832
+ if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
2833
+ const touching = configured.overrides
2834
+ .map(readConfigRecord)
2835
+ .filter(
2836
+ (override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
2837
+ )
2838
+ expect(touching).toEqual([])
2839
+ })
2840
+
2841
+ it('permits a computed import() and refuses a computed require at every module path', () => {
2842
+ const fixture = createPolicyScratch({ prefix: 'computed-imports-' })
2581
2843
  try {
2582
- fixture.write('configs/policy.ts', readFileSync(resolve(root, 'configs/policy.ts'), 'utf8'))
2844
+ fixture.write(
2845
+ 'configs/policy.ts',
2846
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
2847
+ )
2583
2848
  const configured = readConfigRecord(
2584
- JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
2849
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2585
2850
  )
2586
- // Include campaign fixtures even when the workspace's default lint walk ignores them.
2587
- fixture.write('.oxlintrc.json', JSON.stringify({ ...configured, ignorePatterns: [] }))
2588
- const paths: string[] = []
2589
- const requires: string[] = []
2590
- for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
2851
+ const required = COMPUTED_IMPORT_PATHS.map((path) => path.replace(/\.(\w+)$/u, '.require.$1'))
2852
+ for (const path of COMPUTED_IMPORT_PATHS) {
2591
2853
  fixture.write(path, 'const selected = process.argv[2]\nvoid import(selected)\ndebugger\n')
2592
- paths.push(path)
2593
- if (admitted) {
2594
- const required = path.replace(/\.(\w+)$/u, '.require.$1')
2595
- fixture.write(
2596
- required,
2597
- 'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n',
2598
- )
2599
- requires.push(required)
2600
- }
2601
2854
  }
2602
- const codes = readImportDiagnostics(fixture.path, [...paths, ...requires])
2603
- for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
2855
+ for (const path of required) {
2856
+ fixture.write(path, 'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n')
2857
+ }
2858
+ const paths = [...COMPUTED_IMPORT_PATHS, ...required]
2859
+ // Include campaign fixtures even when the workspace's default lint walk ignores them.
2860
+ fixture.write('.oxlintrc.json', JSON.stringify({ ...configured, ignorePatterns: [] }))
2861
+ const codes = readImportDiagnostics(fixture.path, paths)
2862
+ for (const path of COMPUTED_IMPORT_PATHS) {
2604
2863
  expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
2605
2864
  path,
2606
- refused: !admitted,
2865
+ refused: false,
2607
2866
  })
2608
- expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
2609
2867
  }
2610
- for (const path of requires) {
2611
- expect(codes.has('import(no-dynamic-require) ' + path)).toBe(true)
2612
- expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
2868
+ for (const path of required) {
2869
+ expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
2870
+ path,
2871
+ refused: true,
2872
+ })
2613
2873
  }
2874
+ // The sentinel proves each fixture was linted, so an unlinted path cannot read as permitted.
2875
+ for (const path of paths) expect(codes.has('eslint(no-debugger) ' + path)).toBe(true)
2614
2876
 
2615
- expect(readConfigRecord(configured.rules)['import/no-dynamic-require']).toEqual([
2616
- 'error',
2617
- { esmodule: true },
2618
- ])
2619
- if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
2620
- const overrides = configured.overrides.map(readConfigRecord)
2621
- const artifact = overrides.filter(
2622
- (override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
2623
- )
2624
- expect(artifact).toEqual([
2625
- {
2626
- files: [
2627
- 'src/server/artifacts/**/*.{js,mjs,ts,mts}',
2628
- 'tools/artifacts/**/*.{js,mjs,ts,mts}',
2629
- '.orkestrel/*/instruments/**/tools/artifacts/**/*.{js,mjs,ts,mts}',
2630
- ],
2631
- rules: { 'import/no-dynamic-require': ['error', { esmodule: false }] },
2632
- },
2633
- ])
2877
+ // The control restores the ESM setting and must refuse the same computed imports.
2634
2878
  fixture.write(
2635
2879
  '.oxlintrc.json',
2636
2880
  JSON.stringify({
2637
2881
  ...configured,
2638
2882
  ignorePatterns: [],
2639
- overrides: overrides.filter((override) => !artifact.includes(override)),
2883
+ rules: {
2884
+ ...readConfigRecord(configured.rules),
2885
+ 'import/no-dynamic-require': ['error', { esmodule: true }],
2886
+ },
2640
2887
  }),
2641
2888
  )
2642
- const original = readImportDiagnostics(fixture.path, [...paths, ...requires])
2643
- for (const path of [...paths, ...requires]) {
2644
- expect(original.has('import(no-dynamic-require) ' + path)).toBe(true)
2889
+ const strict = readImportDiagnostics(fixture.path, paths)
2890
+ for (const path of paths) {
2891
+ expect({ path, refused: strict.has('import(no-dynamic-require) ' + path) }).toEqual({
2892
+ path,
2893
+ refused: true,
2894
+ })
2645
2895
  }
2646
2896
  } finally {
2647
2897
  fixture.destroy()
@@ -2653,22 +2903,20 @@ createSomething({
2653
2903
  const scratch = fixture.path
2654
2904
  try {
2655
2905
  const configured = readConfigRecord(
2656
- JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
2906
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2657
2907
  )
2658
2908
  if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
2659
- const overrides: object[] = []
2660
- const expected = new Map<string, boolean>()
2661
- for (const [blockIndex, value] of configured.overrides.entries()) {
2909
+ const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
2662
2910
  const block = readConfigRecord(value)
2663
2911
  const rule = readConfigRecord(block.rules)['no-restricted-imports']
2664
- if (!Array.isArray(rule)) continue
2912
+ if (!Array.isArray(rule)) return []
2665
2913
  const patterns = readConfigRecord(rule[1]).patterns
2666
2914
  if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
2667
- for (const [patternIndex, patternValue] of patterns.entries()) {
2915
+ return patterns.map((patternValue: unknown, patternIndex: number) => {
2668
2916
  const pattern = readConfigRecord(patternValue)
2669
2917
  const message = pattern.message
2670
2918
  if (typeof message !== 'string') throw new Error('Missing pattern message')
2671
- let sources: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2919
+ const listed: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2672
2920
  ? [
2673
2921
  ['https://host/x', true],
2674
2922
  ['data:text/plain,x', true],
@@ -2734,49 +2982,59 @@ createSomething({
2734
2982
  true,
2735
2983
  ],
2736
2984
  ]
2737
- if (message.includes('another styles face')) {
2738
- const face =
2739
- typeof pattern.regex === 'string'
2740
- ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
2741
- : undefined
2742
- if (face === undefined) throw new Error('Missing styles face in restriction pattern')
2743
- sources = [
2744
- [`@src/${face}`, true],
2745
- [`@src/${face}?raw`, true],
2746
- [`@orkestrel/x/${face}`, true],
2747
- [`../${face}/index.js`, true],
2748
- [`../src/${face}/index.js`, true],
2749
- [`../../src/${face}/index.js`, true],
2750
- ['@src/browser', false],
2751
- [`@src/${face}x`, false],
2752
- [`../${face}.ext/index.js`, false],
2753
- ]
2754
- }
2985
+ const face =
2986
+ message.includes('another styles face') && typeof pattern.regex === 'string'
2987
+ ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
2988
+ : undefined
2989
+ if (message.includes('another styles face') && face === undefined)
2990
+ throw new Error('Missing styles face in restriction pattern')
2991
+ const sources: ReadonlyArray<readonly [string, boolean]> =
2992
+ face === undefined
2993
+ ? listed
2994
+ : [
2995
+ [`@src/${face}`, true],
2996
+ [`@src/${face}?raw`, true],
2997
+ [`@orkestrel/x/${face}`, true],
2998
+ [`../${face}/index.js`, true],
2999
+ [`../src/${face}/index.js`, true],
3000
+ [`../../src/${face}/index.js`, true],
3001
+ ['@src/browser', false],
3002
+ [`@src/${face}x`, false],
3003
+ [`../${face}.ext/index.js`, false],
3004
+ ]
2755
3005
  const owner =
2756
3006
  Array.isArray(block.files) && typeof block.files[0] === 'string'
2757
3007
  ? block.files[0].split('/**')[0]
2758
3008
  : undefined
2759
3009
  if (owner === undefined) throw new Error('Missing pattern owner')
2760
- const paths: string[] = []
2761
- for (const [index, [source, refusal]] of [
2762
- ...sources,
2763
- ['./index.js', false] as const,
2764
- ].entries()) {
2765
- const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
2766
- mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
2767
- writeFileSync(
2768
- resolve(scratch, path),
2769
- 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
2770
- )
2771
- expected.set(path, refusal)
2772
- paths.push(path)
3010
+ const cases = [...sources, ['./index.js', false] as const].map(
3011
+ ([source, refusal], index) => {
3012
+ const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3013
+ mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3014
+ writeFileSync(
3015
+ resolve(scratch, path),
3016
+ 'import * as boundary from ' +
3017
+ JSON.stringify(source) +
3018
+ '\nvoid boundary\ndebugger\n',
3019
+ )
3020
+ return { path, refusal }
3021
+ },
3022
+ )
3023
+ return {
3024
+ cases,
3025
+ override: {
3026
+ files: cases.map(({ path }) => path),
3027
+ rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
3028
+ },
2773
3029
  }
2774
- overrides.push({
2775
- files: paths,
2776
- rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
2777
- })
2778
- }
2779
- }
3030
+ })
3031
+ })
3032
+ const overrides = built.map(({ override }) => override)
3033
+ const expected = new Map(
3034
+ built.flatMap(({ cases }) =>
3035
+ cases.map(({ path, refusal }): [string, boolean] => [path, refusal]),
3036
+ ),
3037
+ )
2780
3038
  writeFileSync(
2781
3039
  resolve(scratch, '.oxlintrc.json'),
2782
3040
  JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
@@ -2812,132 +3070,154 @@ createSomething({
2812
3070
  mkdirSync(resolve(scratch, 'configs'))
2813
3071
  writeFileSync(
2814
3072
  resolve(scratch, 'configs/policy.ts'),
2815
- readFileSync(resolve(root, 'configs/policy.ts')),
3073
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts')),
2816
3074
  )
2817
- const text = readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')
3075
+ const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
2818
3076
  writeFileSync(resolve(scratch, '.oxlintrc.json'), text)
2819
- const expected = new Map<string, boolean>()
2820
- for (const owner of [
2821
- 'src/vue',
2822
- 'app/vue',
2823
- 'src/browser',
2824
- 'src/bootstrap',
2825
- 'src/tailwindcss',
2826
- 'src/styles',
2827
- 'app/browser',
2828
- 'src/core',
2829
- 'app/core',
2830
- 'src/server',
2831
- 'app/server',
2832
- 'src/bin',
2833
- ]) {
2834
- const refused: Array<string | readonly [string, string]> = owner.endsWith('/vue')
2835
- ? [
2836
- 'node:fs',
2837
- '@src/server',
2838
- '../server/index.js',
2839
- '@src/print',
2840
- '../print/index.js',
2841
- ...(owner.startsWith('src/')
2842
- ? ['@app/core', '../../app/core/index.js']
2843
- : ['@app/server', '@app/print']),
2844
- ]
2845
- : [
2846
- 'vue',
2847
- 'vue/runtime-dom',
2848
- '@vue/runtime-core',
2849
- '@vitejs/plugin-vue',
2850
- '@src/vue',
2851
- '@orkestrel/sample/vue',
2852
- '@app/vue',
2853
- '../vue/index.js',
2854
- '../../src/vue/index.js',
2855
- '../../app/vue/index.js',
2856
- ]
2857
- const admitted = owner.endsWith('/vue')
2858
- ? [
2859
- '@src/core',
2860
- '@src/browser',
2861
- '../core/index.js',
2862
- '../browser/index.js',
2863
- 'vue',
2864
- '@vue/runtime-core',
2865
- ...(owner.startsWith('app/')
2866
- ? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
2867
- : []),
2868
- ]
2869
- : ['@src/core']
2870
- if (owner === 'src/core') refused.push('@src/browser', './index.css')
2871
- if (['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)) {
2872
- refused.push('node:fs', '@src/server', '../server/index.js', '@app/core')
2873
- for (const face of ['bootstrap', 'tailwindcss', 'styles']) {
2874
- if (owner === `src/${face}`) continue
2875
- refused.push(
2876
- `@src/${face}`,
2877
- `@orkestrel/x/${face}`,
2878
- `../${face}/index.js`,
2879
- `../src/${face}/index.js`,
2880
- `../../src/${face}/index.js`,
2881
- ['export * from', `@src/${face}`],
2882
- ['import type * as boundary from', `@src/${face}`],
2883
- )
2884
- admitted.push(`@src/${face}x`, `../${face}.ext/index.js`)
2885
- }
2886
- admitted.push('@src/browser', './sheet.js', '@orkestrel/contract')
2887
- }
2888
- for (const [sources, refusal] of [
2889
- [
3077
+ const faces = ['bootstrap', 'tailwindcss', 'styles']
3078
+ const expected = new Map([
3079
+ ...[
3080
+ 'src/vue',
3081
+ 'app/vue',
3082
+ 'src/browser',
3083
+ 'src/bootstrap',
3084
+ 'src/tailwindcss',
3085
+ 'src/styles',
3086
+ 'app/browser',
3087
+ 'src/core',
3088
+ 'app/core',
3089
+ 'src/server',
3090
+ 'app/server',
3091
+ 'src/bin',
3092
+ ].flatMap((owner) => {
3093
+ const sheet = ['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)
3094
+ const refused: ReadonlyArray<string | readonly [string, string]> = [
3095
+ ...(owner.endsWith('/vue')
3096
+ ? [
3097
+ 'node:fs',
3098
+ '@src/server',
3099
+ '../server/index.js',
3100
+ '@src/print',
3101
+ '../print/index.js',
3102
+ ...(owner.startsWith('src/')
3103
+ ? ['@app/core', '../../app/core/index.js']
3104
+ : ['@app/server', '@app/print']),
3105
+ ]
3106
+ : [
3107
+ 'vue',
3108
+ 'vue/runtime-dom',
3109
+ '@vue/runtime-core',
3110
+ '@vitejs/plugin-vue',
3111
+ '@src/vue',
3112
+ '@orkestrel/sample/vue',
3113
+ '@app/vue',
3114
+ '../vue/index.js',
3115
+ '../../src/vue/index.js',
3116
+ '../../app/vue/index.js',
3117
+ ]),
3118
+ ...(owner === 'src/core' ? ['@src/browser', './index.css'] : []),
3119
+ ...(sheet
3120
+ ? [
3121
+ 'node:fs',
3122
+ '@src/server',
3123
+ '../server/index.js',
3124
+ '@app/core',
3125
+ ...faces
3126
+ .filter((face) => owner !== `src/${face}`)
3127
+ .flatMap((face): ReadonlyArray<string | readonly [string, string]> => [
3128
+ `@src/${face}`,
3129
+ `@orkestrel/x/${face}`,
3130
+ `../${face}/index.js`,
3131
+ `../src/${face}/index.js`,
3132
+ `../../src/${face}/index.js`,
3133
+ ['export * from', `@src/${face}`],
3134
+ ['import type * as boundary from', `@src/${face}`],
3135
+ ]),
3136
+ ]
3137
+ : []),
3138
+ ]
3139
+ const admitted = [
3140
+ ...(owner.endsWith('/vue')
3141
+ ? [
3142
+ '@src/core',
3143
+ '@src/browser',
3144
+ '../core/index.js',
3145
+ '../browser/index.js',
3146
+ 'vue',
3147
+ '@vue/runtime-core',
3148
+ ...(owner.startsWith('app/')
3149
+ ? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
3150
+ : []),
3151
+ ]
3152
+ : ['@src/core']),
3153
+ ...(sheet
3154
+ ? [
3155
+ ...faces
3156
+ .filter((face) => owner !== `src/${face}`)
3157
+ .flatMap((face) => [`@src/${face}x`, `../${face}.ext/index.js`]),
3158
+ '@src/browser',
3159
+ './sheet.js',
3160
+ '@orkestrel/contract',
3161
+ ]
3162
+ : []),
3163
+ ]
3164
+ return (
2890
3165
  [
2891
- ...refused,
2892
- 'https://host/x',
2893
- 'data:text/plain,x',
2894
- '../core/../server/index.js',
2895
- '../core/./index.js',
2896
- ],
2897
- true,
2898
- ],
3166
+ [
3167
+ [
3168
+ ...refused,
3169
+ 'https://host/x',
3170
+ 'data:text/plain,x',
3171
+ '../core/../server/index.js',
3172
+ '../core/./index.js',
3173
+ ],
3174
+ true,
3175
+ ],
3176
+ [
3177
+ [
3178
+ ...admitted,
3179
+ './index.js',
3180
+ '../core/index.js',
3181
+ ...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
3182
+ ],
3183
+ false,
3184
+ ],
3185
+ ] as const
3186
+ ).flatMap(([sources, refusal]) =>
3187
+ sources.map((source, index): [string, boolean] => {
3188
+ const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
3189
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3190
+ writeFileSync(
3191
+ resolve(scratch, file),
3192
+ typeof source === 'string'
3193
+ ? 'import * as boundary from ' +
3194
+ JSON.stringify(source) +
3195
+ '\nvoid boundary\ndebugger\n'
3196
+ : source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
3197
+ )
3198
+ return [file, refusal]
3199
+ }),
3200
+ )
3201
+ }),
3202
+ ...(
2899
3203
  [
2900
- [
2901
- ...admitted,
2902
- './index.js',
2903
- '../core/index.js',
2904
- ...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
2905
- ],
2906
- false,
2907
- ],
2908
- ] as const) {
2909
- for (const [index, source] of sources.entries()) {
2910
- const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
2911
- mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
2912
- writeFileSync(
2913
- resolve(scratch, file),
2914
- typeof source === 'string'
2915
- ? 'import * as boundary from ' +
2916
- JSON.stringify(source) +
2917
- '\nvoid boundary\ndebugger\n'
2918
- : source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
2919
- )
2920
- expected.set(file, refusal)
2921
- }
2922
- }
2923
- }
2924
- for (const [file, source, refusal] of [
2925
- ['probe.config.ts', 'typescript', true],
2926
- ['allowed.config.ts', 'node:fs', false],
2927
- ['src/core/compiler.ts', 'typescript', true],
2928
- ['tests/compiler.ts', 'typescript', true],
2929
- ['configs/compiler.ts', 'typescript', true],
2930
- ['scripts/compiler.ts', 'typescript', true],
2931
- ] as const) {
2932
- mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
2933
- writeFileSync(
2934
- resolve(scratch, file),
2935
- 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
2936
- )
2937
- expected.set(file, refusal)
2938
- }
2939
- const reports: Array<ReadonlySet<string>> = []
2940
- for (const disabled of [false, true]) {
3204
+ ['probe.config.ts', 'typescript', true],
3205
+ ['allowed.config.ts', 'node:fs', false],
3206
+ ['src/core/compiler.ts', 'typescript', true],
3207
+ ['tests/compiler.ts', 'typescript', true],
3208
+ ['configs/compiler.ts', 'typescript', true],
3209
+ ['scripts/compiler.ts', 'typescript', true],
3210
+ ] as const
3211
+ ).map(([file, source, refusal]): [string, boolean] => {
3212
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3213
+ writeFileSync(
3214
+ resolve(scratch, file),
3215
+ 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
3216
+ )
3217
+ return [file, refusal]
3218
+ }),
3219
+ ])
3220
+ const reports = [false, true].map((disabled) => {
2941
3221
  if (disabled) {
2942
3222
  const parsed: unknown = JSON.parse(text)
2943
3223
  if (typeof parsed !== 'object' || parsed === null)
@@ -2960,9 +3240,8 @@ createSomething({
2960
3240
  }
2961
3241
  writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
2962
3242
  }
2963
- const codes = readImportDiagnostics(scratch, [...expected.keys()])
2964
- reports.push(codes)
2965
- }
3243
+ return readImportDiagnostics(scratch, [...expected.keys()])
3244
+ })
2966
3245
  const [enabled, disabled] = reports
2967
3246
  if (enabled === undefined || disabled === undefined)
2968
3247
  throw new Error('Missing boundary readings')
@@ -2988,7 +3267,7 @@ createSomething({
2988
3267
  })
2989
3268
 
2990
3269
  it('enables every plugin rule over the population its law names', () => {
2991
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
3270
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
2992
3271
  const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
2993
3272
  if (typeof rules !== 'object' || rules === null) {
2994
3273
  throw new Error('The policy plugin declares no rules')
@@ -3010,8 +3289,11 @@ createSomething({
3010
3289
  try {
3011
3290
  // Compare diagnostics from the real directory while the child keeps the scratch cwd.
3012
3291
  const comparisonRoot = realpathSync.native(scratch.path)
3013
- scratch.write('.oxlintrc.json', readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
3014
- scratch.write('configs/policy.ts', readFileSync(resolve(root, 'configs/policy.ts'), 'utf8'))
3292
+ scratch.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3293
+ scratch.write(
3294
+ 'configs/policy.ts',
3295
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3296
+ )
3015
3297
  scratch.write(
3016
3298
  'src/violations/fixture.ts',
3017
3299
  [
@@ -3075,7 +3357,9 @@ createSomething({
3075
3357
  // extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
3076
3358
  // containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
3077
3359
  // the entry survives hoisting, a nested `node_modules` layout, and a future rename.
3078
- const manifestPath = createRequire(join(root, 'package.json')).resolve('oxlint/package.json')
3360
+ const manifestPath = createRequire(join(POLICY_ROOT, 'package.json')).resolve(
3361
+ 'oxlint/package.json',
3362
+ )
3079
3363
  const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
3080
3364
  if (typeof manifest !== 'object' || manifest === null) {
3081
3365
  throw new Error('The oxlint package manifest is not an object')
@@ -3102,8 +3386,7 @@ createSomething({
3102
3386
  [binary, '--config', config, '--format', 'json', 'src/clean'],
3103
3387
  { cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
3104
3388
  )
3105
- const reports: string[][] = []
3106
- for (const result of [violations, clean]) {
3389
+ const reports = [violations, clean].map((result): string[] => {
3107
3390
  if (result.error !== undefined) throw result.error
3108
3391
  const report: unknown = JSON.parse(result.stdout)
3109
3392
  if (typeof report !== 'object' || report === null) {
@@ -3111,8 +3394,7 @@ createSomething({
3111
3394
  }
3112
3395
  const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
3113
3396
  if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
3114
- const codes: string[] = []
3115
- for (const diagnostic of diagnostics) {
3397
+ return diagnostics.map((diagnostic: unknown) => {
3116
3398
  if (typeof diagnostic !== 'object' || diagnostic === null) {
3117
3399
  throw new Error('Oxlint returned a malformed diagnostic')
3118
3400
  }
@@ -3121,10 +3403,9 @@ createSomething({
3121
3403
  if (typeof code !== 'string' || typeof filename !== 'string') {
3122
3404
  throw new Error('Oxlint returned a diagnostic without a rule id and a file')
3123
3405
  }
3124
- codes.push(`${code} ${normalizePolicyFilename(comparisonRoot, filename)}`)
3125
- }
3126
- reports.push(codes)
3127
- }
3406
+ return `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`
3407
+ })
3408
+ })
3128
3409
 
3129
3410
  const violationCodes = reports[0]
3130
3411
  const cleanCodes = reports[1]
@@ -3303,9 +3584,11 @@ describe('configuration helpers', () => {
3303
3584
 
3304
3585
  it('exposes every helper this proof requires', () => {
3305
3586
  const required = [
3587
+ 'BuildState',
3306
3588
  'ENVIRONMENT_MODULE_BYTES',
3307
3589
  'PACKAGE_MANIFEST_BYTES',
3308
3590
  'WORKSPACE_ROOT',
3591
+ 'attemptValue',
3309
3592
  'buildExtractorOverride',
3310
3593
  'containedPath',
3311
3594
  'decodeAssetSource',
@@ -3317,6 +3600,7 @@ describe('configuration helpers', () => {
3317
3600
  'environmentPathError',
3318
3601
  'environmentSourceError',
3319
3602
  'fileSystemPath',
3603
+ 'findPackageDirectory',
3320
3604
  'hasAsciiUrlControl',
3321
3605
  'isBoundaryExemptModule',
3322
3606
  'isExtractorModule',
@@ -3333,10 +3617,10 @@ describe('configuration helpers', () => {
3333
3617
  'parseProjectScope',
3334
3618
  'physicalPath',
3335
3619
  'readBoundedFile',
3620
+ 'readHandleBytes',
3336
3621
  'readCompilerOutput',
3337
3622
  'rewriteCoreSpecifier',
3338
3623
  'sourceFallback',
3339
- 'trustedPackageRootFor',
3340
3624
  'workspacePath',
3341
3625
  ]
3342
3626
  const found = Object.keys(configHelpers)
@@ -3375,17 +3659,19 @@ describe('configuration helpers', () => {
3375
3659
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
3376
3660
  try {
3377
3661
  const outside = scratch.path
3378
- const importer = resolve(root, 'tests/config.test.ts')
3379
- expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(root))
3380
- expect(configHelpers.fileSystemPath(`/@fs/${root}`)).toBe(root)
3381
- expect(configHelpers.physicalPath(`${root}?control=true`)).toBe(realpathSync.native(root))
3662
+ const importer = resolve(POLICY_ROOT, 'tests/config.test.ts')
3663
+ expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(POLICY_ROOT))
3664
+ expect(configHelpers.fileSystemPath(`/@fs/${POLICY_ROOT}`)).toBe(POLICY_ROOT)
3665
+ expect(configHelpers.physicalPath(`${POLICY_ROOT}?control=true`)).toBe(
3666
+ realpathSync.native(POLICY_ROOT),
3667
+ )
3382
3668
  expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
3383
- resolve(root, 'tests/setup.ts'),
3669
+ resolve(POLICY_ROOT, 'tests/setup.ts'),
3384
3670
  )
3385
3671
  expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
3386
3672
  expect(configHelpers.workspacePath(importer)).toBe('tests/config.test.ts')
3387
- expect(configHelpers.containedPath(root, importer)).toBe(true)
3388
- expect(configHelpers.containedPath(root, outside)).toBe(false)
3673
+ expect(configHelpers.containedPath(POLICY_ROOT, importer)).toBe(true)
3674
+ expect(configHelpers.containedPath(POLICY_ROOT, outside)).toBe(false)
3389
3675
  expect(configHelpers.workspacePath(outside)).toBeUndefined()
3390
3676
  expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
3391
3677
  expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
@@ -3424,14 +3710,175 @@ describe('configuration helpers', () => {
3424
3710
  realpathSync.native(packageRoot),
3425
3711
  )
3426
3712
  expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
3713
+ } finally {
3714
+ scratch.destroy()
3715
+ }
3716
+ })
3717
+
3718
+ it('returns a producer value, or undefined when the producer throws', () => {
3719
+ expect(configHelpers.attemptValue(() => decodeURI('%20'))).toBe(' ')
3720
+ expect(configHelpers.attemptValue(() => decodeURI('%E0%A4%A'))).toBeUndefined()
3721
+ expect(configHelpers.attemptValue(() => undefined)).toBeUndefined()
3722
+ expect(configHelpers.attemptValue(() => 0)).toBe(0)
3723
+ })
3724
+
3725
+ it('fills a buffer from an open handle until it ends, and refuses a file over the limit', () => {
3726
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
3727
+ try {
3728
+ // Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
3729
+ const readings = [
3730
+ { text: 'abc', offset: 0, limit: 4 },
3731
+ { text: 'abcd', offset: 0, limit: 4 },
3732
+ { text: 'abcde', offset: 0, limit: 4 },
3733
+ { text: '', offset: 0, limit: 4 },
3734
+ { text: 'abc', offset: 2, limit: 10 },
3735
+ { text: 'abc', offset: 2, limit: 4 },
3736
+ ].map(({ text, offset, limit }) => {
3737
+ scratch.write('subject.txt', text)
3738
+ const handle = openSync(resolve(scratch.path, 'subject.txt'), 'r')
3739
+ try {
3740
+ const bytes = Buffer.alloc(16)
3741
+ const count = configHelpers.readHandleBytes(handle, bytes, offset, limit)
3742
+ return { count, filled: bytes.toString('utf8', offset, count ?? offset) }
3743
+ } finally {
3744
+ closeSync(handle)
3745
+ }
3746
+ })
3747
+ // A file at or under the limit answers its length; the shifted read adds the offset to the
3748
+ // length and fills the buffer past it; a file over the limit answers nothing.
3749
+ expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
3750
+ expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
3751
+ } finally {
3752
+ scratch.destroy()
3753
+ }
3754
+ })
3755
+
3756
+ it('finds the nearest package directory from a manifest, a package boundary, or nothing', () => {
3757
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-find-' })
3758
+ try {
3759
+ scratch.write('held/package.json', '{"name":"held"}')
3760
+ scratch.write('held/src/deep/file.txt', '')
3761
+ scratch.write('modules/node_modules/plain/lib/file.txt', '')
3762
+ scratch.write('modules/node_modules/@scope/named/lib/file.txt', '')
3763
+ scratch.write('modules/node_modules/@scope/file.txt', '')
3764
+ const held = resolve(scratch.path, 'held')
3765
+ expect(configHelpers.findPackageDirectory(held)).toBe(held)
3766
+ expect(configHelpers.findPackageDirectory(resolve(held, 'src/deep'))).toBe(held)
3767
+ const plain = resolve(scratch.path, 'modules/node_modules/plain')
3768
+ expect(configHelpers.findPackageDirectory(resolve(plain, 'lib'))).toBe(plain)
3769
+ const named = resolve(scratch.path, 'modules/node_modules/@scope/named')
3770
+ expect(configHelpers.findPackageDirectory(resolve(named, 'lib'))).toBe(named)
3771
+ expect(configHelpers.findPackageDirectory(named)).toBe(named)
3772
+ // A scope directory alone is not a package boundary, so the walk passes it by.
3427
3773
  expect(
3428
- configHelpers.trustedPackageRootFor(source, new Set([realpathSync.native(packageRoot)])),
3429
- ).toBe(realpathSync.native(packageRoot))
3774
+ configHelpers.isPackageBoundary(resolve(scratch.path, 'modules/node_modules/@scope')),
3775
+ ).toBe(false)
3776
+ // The walk ends at the filesystem root; the answer there is the root only if it holds a manifest.
3777
+ const root = parse(scratch.path).root
3778
+ expect(configHelpers.findPackageDirectory(root)).toBe(
3779
+ existsSync(resolve(root, 'package.json')) ? root : undefined,
3780
+ )
3781
+ } finally {
3782
+ scratch.destroy()
3783
+ }
3784
+ })
3785
+
3786
+ it('holds the workspace defaults until a resolved build config replaces them', async () => {
3787
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-state-' })
3788
+ try {
3789
+ const fresh = new configHelpers.BuildState()
3790
+ expect(fresh.building).toBe(false)
3791
+ expect(fresh.output).toBe(configHelpers.WORKSPACE_ROOT)
3792
+ expect(fresh.root).toBe(configHelpers.WORKSPACE_ROOT)
3793
+ expect(fresh.source).toBe('')
3794
+ expect(new configHelpers.BuildState('C:/output').output).toBe('C:/output')
3795
+
3796
+ const entry = resolve(scratch.path, 'index.ts')
3797
+ const inline: InlineConfig = {
3798
+ root: scratch.path,
3799
+ configFile: false,
3800
+ logLevel: 'silent',
3801
+ build: { outDir: 'out', lib: { entry, formats: ['es'] } },
3802
+ }
3803
+ const building = new configHelpers.BuildState()
3804
+ building.resolve(await resolveConfig(inline, 'build'))
3805
+ expect(building.building).toBe(true)
3806
+ expect(building.output).toBe(resolve(scratch.path, 'out'))
3807
+ expect(building.root).toBe(realpathSync.native(scratch.path))
3808
+ expect(building.source).toBe(entry)
3809
+
3810
+ // The control: a serve command is not a build, and a config with no library entry has no source.
3811
+ const serving = new configHelpers.BuildState()
3812
+ serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
3813
+ expect(serving.building).toBe(false)
3814
+ expect(serving.source).toBe('')
3815
+ } finally {
3816
+ scratch.destroy()
3817
+ }
3818
+ })
3819
+
3820
+ it('admits each physical package root once and locates the first admitted root containing a path', () => {
3821
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-locate-' })
3822
+ try {
3823
+ scratch.write('modules/outer/inner/index.ts', '')
3824
+ scratch.write('modules/other/index.ts', '')
3825
+ const outer = realpathSync.native(resolve(scratch.path, 'modules/outer'))
3826
+ const inner = realpathSync.native(resolve(scratch.path, 'modules/outer/inner'))
3827
+ const inside = resolve(scratch.path, 'modules/outer/inner/index.ts')
3828
+ const beside = resolve(scratch.path, 'modules/other/index.ts')
3829
+
3830
+ const state = new configHelpers.BuildState()
3831
+ // A fresh state admits nothing, so it locates nothing.
3832
+ expect(state.locate(inside)).toBeUndefined()
3833
+ state.trust(outer)
3834
+ state.trust(outer)
3835
+ state.trust(inner)
3836
+ expect(state.locate(inside)).toBe(outer)
3837
+ expect(state.locate(beside)).toBeUndefined()
3838
+ // Admission order decides which of two containing roots answers.
3839
+ const reversed = new configHelpers.BuildState()
3840
+ reversed.trust(inner)
3841
+ reversed.trust(outer)
3842
+ expect(reversed.locate(inside)).toBe(inner)
3843
+ expect(new configHelpers.BuildState().locate(inside)).toBeUndefined()
3844
+ // The admitted roots are never returned, so the class publishes no member that lists them.
3845
+ expect(Object.getOwnPropertyNames(configHelpers.BuildState.prototype).sort()).toEqual([
3846
+ 'building',
3847
+ 'constructor',
3848
+ 'locate',
3849
+ 'output',
3850
+ 'resolve',
3851
+ 'root',
3852
+ 'source',
3853
+ 'trust',
3854
+ ])
3430
3855
  } finally {
3431
3856
  scratch.destroy()
3432
3857
  }
3433
3858
  })
3434
3859
 
3860
+ it('climbs from a policy node through its enclosing parentheses alone', () => {
3861
+ const call = { type: 'CallExpression', range: [0, 9] } satisfies PolicyExpression
3862
+ const outer = {
3863
+ type: 'ParenthesizedExpression',
3864
+ range: [1, 8],
3865
+ parent: call,
3866
+ } satisfies PolicyExpression
3867
+ const inner = {
3868
+ type: 'ParenthesizedExpression',
3869
+ range: [2, 7],
3870
+ parent: outer,
3871
+ } satisfies PolicyExpression
3872
+ const leaf = { type: 'Identifier', range: [3, 4], parent: inner } satisfies PolicyExpression
3873
+ expect(nodeToPolicyOuter(leaf)).toBe(outer)
3874
+ expect(nodeToPolicyOuter(inner)).toBe(outer)
3875
+ expect(nodeToPolicyOuter(outer)).toBe(outer)
3876
+ // The control: a node under a non-parenthesized parent, or under none, stays where it is.
3877
+ const argument = { type: 'Identifier', range: [1, 2], parent: call } satisfies PolicyExpression
3878
+ expect(nodeToPolicyOuter(argument)).toBe(argument)
3879
+ expect(nodeToPolicyOuter(call)).toBe(call)
3880
+ })
3881
+
3435
3882
  it('classifies module boundaries and extracts static asset sources', async () => {
3436
3883
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
3437
3884
  try {
@@ -3444,8 +3891,12 @@ describe('configuration helpers', () => {
3444
3891
  expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
3445
3892
  expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
3446
3893
  expect(configHelpers.isBoundaryExemptModule('virtual:control')).toBe(true)
3447
- expect(configHelpers.isBoundaryExemptModule(resolve(root, 'src/core/index.ts'))).toBe(false)
3448
- expect(configHelpers.isWorkspaceBoundaryModule(resolve(root, 'src/core/index.ts'))).toBe(true)
3894
+ expect(configHelpers.isBoundaryExemptModule(resolve(POLICY_ROOT, 'src/core/index.ts'))).toBe(
3895
+ false,
3896
+ )
3897
+ expect(
3898
+ configHelpers.isWorkspaceBoundaryModule(resolve(POLICY_ROOT, 'src/core/index.ts')),
3899
+ ).toBe(true)
3449
3900
  expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
3450
3901
  expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
3451
3902
  expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
@@ -3471,57 +3922,52 @@ describe('configuration helpers', () => {
3471
3922
  'Browser modules cannot depend on Node or server-only modules',
3472
3923
  )
3473
3924
  expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
3474
- const expected = resolve(root, 'dist/config-control')
3925
+ const expected = resolve(POLICY_ROOT, 'dist/config-control')
3475
3926
  expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
3476
3927
  expect(() =>
3477
- configHelpers.enforceOutputPath(resolve(root, 'dist/outside-control'), expected),
3928
+ configHelpers.enforceOutputPath(resolve(POLICY_ROOT, 'dist/outside-control'), expected),
3478
3929
  ).toThrow('Build output must use its exact configured workspace directory')
3479
- const outside = resolve(dirname(root), 'outside-config-control')
3930
+ const outside = resolve(dirname(POLICY_ROOT), 'outside-config-control')
3480
3931
  expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
3481
3932
  'Build output must remain inside the workspace',
3482
3933
  )
3483
3934
  })
3484
3935
 
3485
- it('admits computed artifact imports through Vite while retaining literal boundary inspection', async () => {
3486
- for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => admitted)) {
3936
+ it('admits a computed import() through Vite at every module path while collecting literal specifiers', async () => {
3937
+ for (const path of [...COMPUTED_IMPORT_PATHS, '../outside/src/server/load.ts']) {
3487
3938
  await expect(
3488
- configHelpers.environmentAssetSources('void import(process.argv[2])', resolve(root, path)),
3939
+ configHelpers.environmentAssetSources(
3940
+ 'void import(process.argv[2])',
3941
+ resolve(POLICY_ROOT, path),
3942
+ ),
3489
3943
  ).resolves.toEqual([])
3490
- }
3491
- for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => !admitted)) {
3492
3944
  await expect(
3493
- configHelpers.environmentAssetSources('void import(process.argv[2])', resolve(root, path)),
3494
- ).rejects.toThrow('Dynamic imports must use static string values')
3945
+ configHelpers.environmentAssetSources(
3946
+ "void import('@src/browser')",
3947
+ resolve(POLICY_ROOT, path),
3948
+ ),
3949
+ ).resolves.toEqual(['@src/browser'])
3495
3950
  }
3496
- await expect(
3497
- configHelpers.environmentAssetSources(
3498
- "void import('@src/browser')",
3499
- resolve(root, 'src/server/artifacts/Artifacts.ts'),
3500
- ),
3501
- ).resolves.toEqual(['@src/browser'])
3502
3951
  expect(configHelpers.environmentSourceError('src/server', '@src/browser')).toBeDefined()
3503
- await expect(
3504
- configHelpers.environmentAssetSources(
3505
- 'void import(process.argv[2])',
3506
- resolve(root, '../outside/src/server/artifacts/Artifacts.ts'),
3507
- ),
3508
- ).rejects.toThrow('Dynamic imports must use static string values')
3509
3952
  })
3510
3953
 
3511
3954
  it('drives each plugin through its real Vite hooks', async () => {
3512
3955
  const environments: ReadonlyArray<
3513
3956
  'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
3514
3957
  > = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
3515
- const owner = environments.find((environment) => existsSync(resolve(root, environment)))
3958
+ const owner = environments.find((environment) => existsSync(resolve(POLICY_ROOT, environment)))
3516
3959
  if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
3517
- const scratch = createPolicyScratch({ parent: resolve(root, owner), prefix: 'config-build-' })
3960
+ const scratch = createPolicyScratch({
3961
+ parent: resolve(POLICY_ROOT, owner),
3962
+ prefix: 'config-build-',
3963
+ })
3518
3964
  const workspace = scratch.path
3519
3965
  try {
3520
3966
  const source = resolve(workspace, 'index.ts')
3521
3967
  writeFileSync(source, 'export const control = true\n', 'utf8')
3522
3968
  await expect(
3523
3969
  build({
3524
- root,
3970
+ root: POLICY_ROOT,
3525
3971
  configFile: false,
3526
3972
  logLevel: 'silent',
3527
3973
  publicDir: false,
@@ -3568,7 +4014,7 @@ describe('configuration helpers', () => {
3568
4014
  // raise its passed count with nothing measured. Every generated workspace runs its projects
3569
4015
  // under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
3570
4016
  // condition in it are read by re-running the project with a reporter that names skipped cases.
3571
- it.skipIf(!publishes)(
4017
+ it.skipIf(!POLICY_PUBLISHES)(
3572
4018
  'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
3573
4019
  () => {
3574
4020
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
@@ -3576,10 +4022,10 @@ describe('configuration helpers', () => {
3576
4022
  // no core project, so this walks to the first face the workspace actually carries.
3577
4023
  const faces = ['core', 'browser', 'server']
3578
4024
  const face = faces.find((candidate) =>
3579
- existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
4025
+ existsSync(resolve(POLICY_ROOT, `configs/src/tsconfig.${candidate}.json`)),
3580
4026
  )
3581
4027
  if (face === undefined) throw new Error('The workspace declares no face project')
3582
- const project = resolve(root, `configs/src/tsconfig.${face}.json`)
4028
+ const project = resolve(POLICY_ROOT, `configs/src/tsconfig.${face}.json`)
3583
4029
  const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
3584
4030
  if (typeof declared !== 'object' || declared === null) {
3585
4031
  throw new Error(`The ${face} project is not a TypeScript configuration record`)
@@ -3634,7 +4080,7 @@ describe('configuration helpers', () => {
3634
4080
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
3635
4081
  // Any project path resolves these, because each reading is refused by the text before the
3636
4082
  // path is read. The root project is the one file every workspace carries.
3637
- const project = resolve(root, 'tsconfig.json')
4083
+ const project = resolve(POLICY_ROOT, 'tsconfig.json')
3638
4084
  expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
3639
4085
  expect(
3640
4086
  configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
@@ -3647,7 +4093,7 @@ describe('configuration helpers', () => {
3647
4093
  configHelpers.readCompilerOutput(compiler, [
3648
4094
  '--showConfig',
3649
4095
  '-p',
3650
- resolve(root, 'configs/tsconfig.absent.json'),
4096
+ resolve(POLICY_ROOT, 'configs/tsconfig.absent.json'),
3651
4097
  ]),
3652
4098
  ).toThrow('The declaration compiler failed')
3653
4099
 
@@ -3720,12 +4166,12 @@ describe('configuration helpers', () => {
3720
4166
 
3721
4167
  // A hoisted install can place the extractor anywhere `require.resolve` reaches, so the skip
3722
4168
  // control compares against the path resolution actually returned rather than a fixed layout.
3723
- it.skipIf(extractorPath === undefined)('finds the resolved extractor on disk', () => {
3724
- if (extractorPath === undefined) throw new Error('The extractor resolution left no path')
3725
- expect(existsSync(extractorPath)).toBe(true)
4169
+ it.skipIf(POLICY_EXTRACTOR === undefined)('finds the resolved extractor on disk', () => {
4170
+ if (POLICY_EXTRACTOR === undefined) throw new Error('The extractor resolution left no path')
4171
+ expect(existsSync(POLICY_EXTRACTOR)).toBe(true)
3726
4172
  })
3727
4173
 
3728
- it.skipIf(extractorPath !== undefined)('rejects resolving the unavailable extractor', () => {
4174
+ it.skipIf(POLICY_EXTRACTOR !== undefined)('rejects resolving the unavailable extractor', () => {
3729
4175
  expect(() => createRequire(import.meta.url).resolve('@microsoft/api-extractor')).toThrow(
3730
4176
  'Cannot find module',
3731
4177
  )
@@ -3733,7 +4179,7 @@ describe('configuration helpers', () => {
3733
4179
 
3734
4180
  // The roll-up loads the declaration extractor, which only a workspace publishing source from
3735
4181
  // `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
3736
- it.skipIf(extractorPath === undefined)(
4182
+ it.skipIf(POLICY_EXTRACTOR === undefined)(
3737
4183
  'rolls one face into a single declaration and rewrites its core and browser specifiers',
3738
4184
  async () => {
3739
4185
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
@@ -3878,3 +4324,133 @@ describe('configuration helpers', () => {
3878
4324
  },
3879
4325
  )
3880
4326
  })
4327
+
4328
+ // A `for…of` loop reads one entry, works on it, and stops at the first failure, where a copy of the
4329
+ // list reads every entry first. Each case runs a policy reader over a recorded list and pins the
4330
+ // outcome and the entries it read, as the loop the reader replaced gave them.
4331
+ describe('policy readers consume a caller-supplied list one entry at a time', () => {
4332
+ const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
4333
+ const declarator = (): PolicyExpression => ({
4334
+ type: 'VariableDeclarator',
4335
+ range: [0, 0],
4336
+ id: name,
4337
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
4338
+ })
4339
+ const exported = (): PolicyExpression => ({
4340
+ type: 'ExportNamedDeclaration',
4341
+ range: [47, 63],
4342
+ declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
4343
+ })
4344
+ const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
4345
+ // A revoked proxy refuses every read, which fails the reader once it holds the entry.
4346
+ const refused =
4347
+ <T extends object>(entry: () => T) =>
4348
+ (): T => {
4349
+ const { proxy, revoke } = Proxy.revocable(entry(), {})
4350
+ revoke()
4351
+ return proxy
4352
+ }
4353
+ const throwing = [
4354
+ [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
4355
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
4356
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
4357
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
4358
+ [POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
4359
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
4360
+ ] as const
4361
+
4362
+ it.each([
4363
+ ...createPolicyLazyCases(
4364
+ 'statementToPolicyBindings',
4365
+ (declarations: PolicyExpression[]) =>
4366
+ statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
4367
+ (binding) => binding.name,
4368
+ ),
4369
+ { valid: declarator, refusal: refused(declarator) },
4370
+ [
4371
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4372
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4373
+ ...throwing,
4374
+ ],
4375
+ ),
4376
+ ...createPolicyLazyCases(
4377
+ 'reportDomain',
4378
+ (body: PolicyExpression[]) => {
4379
+ const reports: Array<string | undefined> = []
4380
+ reportDomain(
4381
+ {
4382
+ filename: '/ws/src/server/execution/run.ts',
4383
+ cwd: '/ws',
4384
+ sourceCode: { text: '', getAllComments: () => [] },
4385
+ report: (diagnostic) => {
4386
+ reports.push(diagnostic.messageId)
4387
+ },
4388
+ },
4389
+ { type: 'Program', range: [0, 0], body },
4390
+ )
4391
+ return reports
4392
+ },
4393
+ { valid: exported, refusal: refused(exported) },
4394
+ [
4395
+ [POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
4396
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4397
+ ...throwing,
4398
+ ],
4399
+ ),
4400
+ ...createPolicyLazyCases(
4401
+ 'programToPolicyDocs statements',
4402
+ (body: PolicyExpression[]) =>
4403
+ programToPolicyDocs(
4404
+ { type: 'Program', range: [0, 0], body },
4405
+ { text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
4406
+ ).map((doc) => doc.name),
4407
+ { valid: exported, refusal: refused(exported) },
4408
+ [
4409
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4410
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4411
+ ...throwing,
4412
+ ],
4413
+ ),
4414
+ ...createPolicyLazyCases(
4415
+ 'programToPolicyDocs comments',
4416
+ (comments: PolicyComment[]) =>
4417
+ programToPolicyDocs(
4418
+ { type: 'Program', range: [0, 0], body: [exported()] },
4419
+ { text: '/** One. */\n', getAllComments: () => comments },
4420
+ ).map((doc) => doc.name),
4421
+ { valid: comment, refusal: refused(comment) },
4422
+ [
4423
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4424
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4425
+ ...throwing,
4426
+ ],
4427
+ ),
4428
+ ...createPolicyLazyCases(
4429
+ 'programToPolicyDocs comments of a program with no export',
4430
+ (comments: PolicyComment[]) =>
4431
+ programToPolicyDocs(
4432
+ { type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
4433
+ { text: 'import x', getAllComments: () => comments },
4434
+ ).map((doc) => doc.name),
4435
+ { valid: comment, refusal: refused(comment) },
4436
+ [
4437
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4438
+ [POLICY_LAZY_SHAPES.dense, 'return []', []],
4439
+ [POLICY_LAZY_SHAPES.hole, 'return []', []],
4440
+ [POLICY_LAZY_SHAPES.holeFailure, 'return []', []],
4441
+ [POLICY_LAZY_SHAPES.failureFirst, 'return []', []],
4442
+ [POLICY_LAZY_SHAPES.refusalFirst, 'return []', []],
4443
+ [POLICY_LAZY_SHAPES.holeBetween, 'return []', []],
4444
+ [POLICY_LAZY_SHAPES.unlisted, 'return []', []],
4445
+ ],
4446
+ ),
4447
+ ])('$label', ({ observe, outcome, reads }) => {
4448
+ const read: number[] = []
4449
+ expect(
4450
+ observe((index) => {
4451
+ read.push(index)
4452
+ }),
4453
+ ).toBe(outcome)
4454
+ expect(read).toStrictEqual(reads)
4455
+ })
4456
+ })