@orkestrel/scaffold 0.0.101 → 0.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) 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 +30 -12
  39. package/dist/host/configs/helpers.ts +206 -133
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/guides/scaffold.md +244 -170
  42. package/dist/host/manifest.json +35 -29
  43. package/dist/host/tests/config.test.ts +1220 -633
  44. package/dist/host/tests/policy.test.ts +19 -0
  45. package/dist/host/tests/setupPolicy.ts +1669 -985
  46. package/dist/src/core/index.cjs +1595 -1686
  47. package/dist/src/core/index.cjs.map +1 -1
  48. package/dist/src/core/index.d.cts +202 -18
  49. package/dist/src/core/index.d.ts +202 -18
  50. package/dist/src/core/index.js +1590 -1687
  51. package/dist/src/core/index.js.map +1 -1
  52. package/dist/src/server/index.cjs +448 -392
  53. package/dist/src/server/index.cjs.map +1 -1
  54. package/dist/src/server/index.d.cts +1182 -1052
  55. package/dist/src/server/index.d.ts +1182 -1052
  56. package/dist/src/server/index.js +447 -395
  57. package/dist/src/server/index.js.map +1 -1
  58. 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'
@@ -59,6 +72,7 @@ import tsconfig from '../tsconfig.json' with { type: 'json' }
59
72
  import {
60
73
  ARTIFACT_IMPORT_CASES,
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')
@@ -2579,26 +2824,28 @@ createSomething({
2579
2824
  it('admits computed ESM imports only in artifact loaders while refusing computed require', () => {
2580
2825
  const fixture = createPolicyScratch({ prefix: 'artifact-imports-' })
2581
2826
  try {
2582
- fixture.write('configs/policy.ts', readFileSync(resolve(root, 'configs/policy.ts'), 'utf8'))
2827
+ fixture.write(
2828
+ 'configs/policy.ts',
2829
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
2830
+ )
2583
2831
  const configured = readConfigRecord(
2584
- JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
2832
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2585
2833
  )
2586
2834
  // Include campaign fixtures even when the workspace's default lint walk ignores them.
2587
2835
  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) {
2836
+ const written = ARTIFACT_IMPORT_CASES.map(([path, admitted]) => {
2591
2837
  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')
2838
+ const required = admitted ? path.replace(/\.(\w+)$/u, '.require.$1') : undefined
2839
+ if (required !== undefined) {
2595
2840
  fixture.write(
2596
2841
  required,
2597
2842
  'const selected = process.argv[2]\nvoid require(selected)\ndebugger\n',
2598
2843
  )
2599
- requires.push(required)
2600
2844
  }
2601
- }
2845
+ return { path, required }
2846
+ })
2847
+ const paths = written.map(({ path }) => path)
2848
+ const requires = written.flatMap(({ required }) => (required === undefined ? [] : [required]))
2602
2849
  const codes = readImportDiagnostics(fixture.path, [...paths, ...requires])
2603
2850
  for (const [path, admitted] of ARTIFACT_IMPORT_CASES) {
2604
2851
  expect({ path, refused: codes.has('import(no-dynamic-require) ' + path) }).toEqual({
@@ -2653,22 +2900,20 @@ createSomething({
2653
2900
  const scratch = fixture.path
2654
2901
  try {
2655
2902
  const configured = readConfigRecord(
2656
- JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')),
2903
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
2657
2904
  )
2658
2905
  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()) {
2906
+ const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
2662
2907
  const block = readConfigRecord(value)
2663
2908
  const rule = readConfigRecord(block.rules)['no-restricted-imports']
2664
- if (!Array.isArray(rule)) continue
2909
+ if (!Array.isArray(rule)) return []
2665
2910
  const patterns = readConfigRecord(rule[1]).patterns
2666
2911
  if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
2667
- for (const [patternIndex, patternValue] of patterns.entries()) {
2912
+ return patterns.map((patternValue: unknown, patternIndex: number) => {
2668
2913
  const pattern = readConfigRecord(patternValue)
2669
2914
  const message = pattern.message
2670
2915
  if (typeof message !== 'string') throw new Error('Missing pattern message')
2671
- let sources: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2916
+ const listed: ReadonlyArray<readonly [string, boolean]> = message.includes('URL schemes')
2672
2917
  ? [
2673
2918
  ['https://host/x', true],
2674
2919
  ['data:text/plain,x', true],
@@ -2734,49 +2979,59 @@ createSomething({
2734
2979
  true,
2735
2980
  ],
2736
2981
  ]
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
- }
2982
+ const face =
2983
+ message.includes('another styles face') && typeof pattern.regex === 'string'
2984
+ ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
2985
+ : undefined
2986
+ if (message.includes('another styles face') && face === undefined)
2987
+ throw new Error('Missing styles face in restriction pattern')
2988
+ const sources: ReadonlyArray<readonly [string, boolean]> =
2989
+ face === undefined
2990
+ ? listed
2991
+ : [
2992
+ [`@src/${face}`, true],
2993
+ [`@src/${face}?raw`, true],
2994
+ [`@orkestrel/x/${face}`, true],
2995
+ [`../${face}/index.js`, true],
2996
+ [`../src/${face}/index.js`, true],
2997
+ [`../../src/${face}/index.js`, true],
2998
+ ['@src/browser', false],
2999
+ [`@src/${face}x`, false],
3000
+ [`../${face}.ext/index.js`, false],
3001
+ ]
2755
3002
  const owner =
2756
3003
  Array.isArray(block.files) && typeof block.files[0] === 'string'
2757
3004
  ? block.files[0].split('/**')[0]
2758
3005
  : undefined
2759
3006
  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)
3007
+ const cases = [...sources, ['./index.js', false] as const].map(
3008
+ ([source, refusal], index) => {
3009
+ const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3010
+ mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3011
+ writeFileSync(
3012
+ resolve(scratch, path),
3013
+ 'import * as boundary from ' +
3014
+ JSON.stringify(source) +
3015
+ '\nvoid boundary\ndebugger\n',
3016
+ )
3017
+ return { path, refusal }
3018
+ },
3019
+ )
3020
+ return {
3021
+ cases,
3022
+ override: {
3023
+ files: cases.map(({ path }) => path),
3024
+ rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
3025
+ },
2773
3026
  }
2774
- overrides.push({
2775
- files: paths,
2776
- rules: { 'no-restricted-imports': ['error', { patterns: [pattern] }] },
2777
- })
2778
- }
2779
- }
3027
+ })
3028
+ })
3029
+ const overrides = built.map(({ override }) => override)
3030
+ const expected = new Map(
3031
+ built.flatMap(({ cases }) =>
3032
+ cases.map(({ path, refusal }): [string, boolean] => [path, refusal]),
3033
+ ),
3034
+ )
2780
3035
  writeFileSync(
2781
3036
  resolve(scratch, '.oxlintrc.json'),
2782
3037
  JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
@@ -2812,132 +3067,154 @@ createSomething({
2812
3067
  mkdirSync(resolve(scratch, 'configs'))
2813
3068
  writeFileSync(
2814
3069
  resolve(scratch, 'configs/policy.ts'),
2815
- readFileSync(resolve(root, 'configs/policy.ts')),
3070
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts')),
2816
3071
  )
2817
- const text = readFileSync(resolve(root, '.oxlintrc.json'), 'utf8')
3072
+ const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
2818
3073
  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
- [
3074
+ const faces = ['bootstrap', 'tailwindcss', 'styles']
3075
+ const expected = new Map([
3076
+ ...[
3077
+ 'src/vue',
3078
+ 'app/vue',
3079
+ 'src/browser',
3080
+ 'src/bootstrap',
3081
+ 'src/tailwindcss',
3082
+ 'src/styles',
3083
+ 'app/browser',
3084
+ 'src/core',
3085
+ 'app/core',
3086
+ 'src/server',
3087
+ 'app/server',
3088
+ 'src/bin',
3089
+ ].flatMap((owner) => {
3090
+ const sheet = ['src/bootstrap', 'src/tailwindcss', 'src/styles'].includes(owner)
3091
+ const refused: ReadonlyArray<string | readonly [string, string]> = [
3092
+ ...(owner.endsWith('/vue')
3093
+ ? [
3094
+ 'node:fs',
3095
+ '@src/server',
3096
+ '../server/index.js',
3097
+ '@src/print',
3098
+ '../print/index.js',
3099
+ ...(owner.startsWith('src/')
3100
+ ? ['@app/core', '../../app/core/index.js']
3101
+ : ['@app/server', '@app/print']),
3102
+ ]
3103
+ : [
3104
+ 'vue',
3105
+ 'vue/runtime-dom',
3106
+ '@vue/runtime-core',
3107
+ '@vitejs/plugin-vue',
3108
+ '@src/vue',
3109
+ '@orkestrel/sample/vue',
3110
+ '@app/vue',
3111
+ '../vue/index.js',
3112
+ '../../src/vue/index.js',
3113
+ '../../app/vue/index.js',
3114
+ ]),
3115
+ ...(owner === 'src/core' ? ['@src/browser', './index.css'] : []),
3116
+ ...(sheet
3117
+ ? [
3118
+ 'node:fs',
3119
+ '@src/server',
3120
+ '../server/index.js',
3121
+ '@app/core',
3122
+ ...faces
3123
+ .filter((face) => owner !== `src/${face}`)
3124
+ .flatMap((face): ReadonlyArray<string | readonly [string, string]> => [
3125
+ `@src/${face}`,
3126
+ `@orkestrel/x/${face}`,
3127
+ `../${face}/index.js`,
3128
+ `../src/${face}/index.js`,
3129
+ `../../src/${face}/index.js`,
3130
+ ['export * from', `@src/${face}`],
3131
+ ['import type * as boundary from', `@src/${face}`],
3132
+ ]),
3133
+ ]
3134
+ : []),
3135
+ ]
3136
+ const admitted = [
3137
+ ...(owner.endsWith('/vue')
3138
+ ? [
3139
+ '@src/core',
3140
+ '@src/browser',
3141
+ '../core/index.js',
3142
+ '../browser/index.js',
3143
+ 'vue',
3144
+ '@vue/runtime-core',
3145
+ ...(owner.startsWith('app/')
3146
+ ? ['@app/core', '@app/browser', '@src/vue', '../../src/vue/index.js']
3147
+ : []),
3148
+ ]
3149
+ : ['@src/core']),
3150
+ ...(sheet
3151
+ ? [
3152
+ ...faces
3153
+ .filter((face) => owner !== `src/${face}`)
3154
+ .flatMap((face) => [`@src/${face}x`, `../${face}.ext/index.js`]),
3155
+ '@src/browser',
3156
+ './sheet.js',
3157
+ '@orkestrel/contract',
3158
+ ]
3159
+ : []),
3160
+ ]
3161
+ return (
2890
3162
  [
2891
- ...refused,
2892
- 'https://host/x',
2893
- 'data:text/plain,x',
2894
- '../core/../server/index.js',
2895
- '../core/./index.js',
2896
- ],
2897
- true,
2898
- ],
3163
+ [
3164
+ [
3165
+ ...refused,
3166
+ 'https://host/x',
3167
+ 'data:text/plain,x',
3168
+ '../core/../server/index.js',
3169
+ '../core/./index.js',
3170
+ ],
3171
+ true,
3172
+ ],
3173
+ [
3174
+ [
3175
+ ...admitted,
3176
+ './index.js',
3177
+ '../core/index.js',
3178
+ ...(owner.endsWith('/server') || owner === 'src/bin' ? ['node:fs'] : []),
3179
+ ],
3180
+ false,
3181
+ ],
3182
+ ] as const
3183
+ ).flatMap(([sources, refusal]) =>
3184
+ sources.map((source, index): [string, boolean] => {
3185
+ const file = owner + '/' + (refusal ? 'refused' : 'admitted') + index + '.ts'
3186
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3187
+ writeFileSync(
3188
+ resolve(scratch, file),
3189
+ typeof source === 'string'
3190
+ ? 'import * as boundary from ' +
3191
+ JSON.stringify(source) +
3192
+ '\nvoid boundary\ndebugger\n'
3193
+ : source[0] + ' ' + JSON.stringify(source[1]) + '\ndebugger\n',
3194
+ )
3195
+ return [file, refusal]
3196
+ }),
3197
+ )
3198
+ }),
3199
+ ...(
2899
3200
  [
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]) {
3201
+ ['probe.config.ts', 'typescript', true],
3202
+ ['allowed.config.ts', 'node:fs', false],
3203
+ ['src/core/compiler.ts', 'typescript', true],
3204
+ ['tests/compiler.ts', 'typescript', true],
3205
+ ['configs/compiler.ts', 'typescript', true],
3206
+ ['scripts/compiler.ts', 'typescript', true],
3207
+ ] as const
3208
+ ).map(([file, source, refusal]): [string, boolean] => {
3209
+ mkdirSync(dirname(resolve(scratch, file)), { recursive: true })
3210
+ writeFileSync(
3211
+ resolve(scratch, file),
3212
+ 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
3213
+ )
3214
+ return [file, refusal]
3215
+ }),
3216
+ ])
3217
+ const reports = [false, true].map((disabled) => {
2941
3218
  if (disabled) {
2942
3219
  const parsed: unknown = JSON.parse(text)
2943
3220
  if (typeof parsed !== 'object' || parsed === null)
@@ -2960,9 +3237,8 @@ createSomething({
2960
3237
  }
2961
3238
  writeFileSync(resolve(scratch, '.oxlintrc.json'), JSON.stringify(parsed))
2962
3239
  }
2963
- const codes = readImportDiagnostics(scratch, [...expected.keys()])
2964
- reports.push(codes)
2965
- }
3240
+ return readImportDiagnostics(scratch, [...expected.keys()])
3241
+ })
2966
3242
  const [enabled, disabled] = reports
2967
3243
  if (enabled === undefined || disabled === undefined)
2968
3244
  throw new Error('Missing boundary readings')
@@ -2988,7 +3264,7 @@ createSomething({
2988
3264
  })
2989
3265
 
2990
3266
  it('enables every plugin rule over the population its law names', () => {
2991
- const parsed: unknown = JSON.parse(readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
3267
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
2992
3268
  const rules: unknown = Object.getOwnPropertyDescriptor(policyPlugin, 'rules')?.value
2993
3269
  if (typeof rules !== 'object' || rules === null) {
2994
3270
  throw new Error('The policy plugin declares no rules')
@@ -3010,8 +3286,11 @@ createSomething({
3010
3286
  try {
3011
3287
  // Compare diagnostics from the real directory while the child keeps the scratch cwd.
3012
3288
  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'))
3289
+ scratch.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3290
+ scratch.write(
3291
+ 'configs/policy.ts',
3292
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3293
+ )
3015
3294
  scratch.write(
3016
3295
  'src/violations/fixture.ts',
3017
3296
  [
@@ -3075,7 +3354,9 @@ createSomething({
3075
3354
  // extensionless form; spawning the `.cmd` would need `shell: true`, which breaks on paths
3076
3355
  // containing spaces. Resolving through `createRequire` reads oxlint's own `bin` field, so
3077
3356
  // 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')
3357
+ const manifestPath = createRequire(join(POLICY_ROOT, 'package.json')).resolve(
3358
+ 'oxlint/package.json',
3359
+ )
3079
3360
  const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8'))
3080
3361
  if (typeof manifest !== 'object' || manifest === null) {
3081
3362
  throw new Error('The oxlint package manifest is not an object')
@@ -3102,8 +3383,7 @@ createSomething({
3102
3383
  [binary, '--config', config, '--format', 'json', 'src/clean'],
3103
3384
  { cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
3104
3385
  )
3105
- const reports: string[][] = []
3106
- for (const result of [violations, clean]) {
3386
+ const reports = [violations, clean].map((result): string[] => {
3107
3387
  if (result.error !== undefined) throw result.error
3108
3388
  const report: unknown = JSON.parse(result.stdout)
3109
3389
  if (typeof report !== 'object' || report === null) {
@@ -3111,8 +3391,7 @@ createSomething({
3111
3391
  }
3112
3392
  const diagnostics: unknown = Object.getOwnPropertyDescriptor(report, 'diagnostics')?.value
3113
3393
  if (!Array.isArray(diagnostics)) throw new Error('Oxlint returned no diagnostic list')
3114
- const codes: string[] = []
3115
- for (const diagnostic of diagnostics) {
3394
+ return diagnostics.map((diagnostic: unknown) => {
3116
3395
  if (typeof diagnostic !== 'object' || diagnostic === null) {
3117
3396
  throw new Error('Oxlint returned a malformed diagnostic')
3118
3397
  }
@@ -3121,10 +3400,9 @@ createSomething({
3121
3400
  if (typeof code !== 'string' || typeof filename !== 'string') {
3122
3401
  throw new Error('Oxlint returned a diagnostic without a rule id and a file')
3123
3402
  }
3124
- codes.push(`${code} ${normalizePolicyFilename(comparisonRoot, filename)}`)
3125
- }
3126
- reports.push(codes)
3127
- }
3403
+ return `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`
3404
+ })
3405
+ })
3128
3406
 
3129
3407
  const violationCodes = reports[0]
3130
3408
  const cleanCodes = reports[1]
@@ -3303,9 +3581,11 @@ describe('configuration helpers', () => {
3303
3581
 
3304
3582
  it('exposes every helper this proof requires', () => {
3305
3583
  const required = [
3584
+ 'BuildState',
3306
3585
  'ENVIRONMENT_MODULE_BYTES',
3307
3586
  'PACKAGE_MANIFEST_BYTES',
3308
3587
  'WORKSPACE_ROOT',
3588
+ 'attemptValue',
3309
3589
  'buildExtractorOverride',
3310
3590
  'containedPath',
3311
3591
  'decodeAssetSource',
@@ -3317,6 +3597,7 @@ describe('configuration helpers', () => {
3317
3597
  'environmentPathError',
3318
3598
  'environmentSourceError',
3319
3599
  'fileSystemPath',
3600
+ 'findPackageDirectory',
3320
3601
  'hasAsciiUrlControl',
3321
3602
  'isBoundaryExemptModule',
3322
3603
  'isExtractorModule',
@@ -3333,10 +3614,10 @@ describe('configuration helpers', () => {
3333
3614
  'parseProjectScope',
3334
3615
  'physicalPath',
3335
3616
  'readBoundedFile',
3617
+ 'readHandleBytes',
3336
3618
  'readCompilerOutput',
3337
3619
  'rewriteCoreSpecifier',
3338
3620
  'sourceFallback',
3339
- 'trustedPackageRootFor',
3340
3621
  'workspacePath',
3341
3622
  ]
3342
3623
  const found = Object.keys(configHelpers)
@@ -3375,17 +3656,19 @@ describe('configuration helpers', () => {
3375
3656
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-outside-' })
3376
3657
  try {
3377
3658
  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))
3659
+ const importer = resolve(POLICY_ROOT, 'tests/config.test.ts')
3660
+ expect(configHelpers.WORKSPACE_ROOT).toBe(realpathSync.native(POLICY_ROOT))
3661
+ expect(configHelpers.fileSystemPath(`/@fs/${POLICY_ROOT}`)).toBe(POLICY_ROOT)
3662
+ expect(configHelpers.physicalPath(`${POLICY_ROOT}?control=true`)).toBe(
3663
+ realpathSync.native(POLICY_ROOT),
3664
+ )
3382
3665
  expect(configHelpers.sourceFallback(importer, './setup.ts')).toBe(
3383
- resolve(root, 'tests/setup.ts'),
3666
+ resolve(POLICY_ROOT, 'tests/setup.ts'),
3384
3667
  )
3385
3668
  expect(configHelpers.sourceFallback(importer, pathToFileURL(importer).href)).toBe(importer)
3386
3669
  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)
3670
+ expect(configHelpers.containedPath(POLICY_ROOT, importer)).toBe(true)
3671
+ expect(configHelpers.containedPath(POLICY_ROOT, outside)).toBe(false)
3389
3672
  expect(configHelpers.workspacePath(outside)).toBeUndefined()
3390
3673
  expect(configHelpers.isOutsideWorkspacePath(outside)).toBe(true)
3391
3674
  expect(configHelpers.isOutsideWorkspacePath('src/core/index.ts')).toBe(false)
@@ -3424,14 +3707,175 @@ describe('configuration helpers', () => {
3424
3707
  realpathSync.native(packageRoot),
3425
3708
  )
3426
3709
  expect(configHelpers.packageRootForResolved(source)).toBe(realpathSync.native(packageRoot))
3710
+ } finally {
3711
+ scratch.destroy()
3712
+ }
3713
+ })
3714
+
3715
+ it('returns a producer value, or undefined when the producer throws', () => {
3716
+ expect(configHelpers.attemptValue(() => decodeURI('%20'))).toBe(' ')
3717
+ expect(configHelpers.attemptValue(() => decodeURI('%E0%A4%A'))).toBeUndefined()
3718
+ expect(configHelpers.attemptValue(() => undefined)).toBeUndefined()
3719
+ expect(configHelpers.attemptValue(() => 0)).toBe(0)
3720
+ })
3721
+
3722
+ it('fills a buffer from an open handle until it ends, and refuses a file over the limit', () => {
3723
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
3724
+ try {
3725
+ // Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
3726
+ const readings = [
3727
+ { text: 'abc', offset: 0, limit: 4 },
3728
+ { text: 'abcd', offset: 0, limit: 4 },
3729
+ { text: 'abcde', offset: 0, limit: 4 },
3730
+ { text: '', offset: 0, limit: 4 },
3731
+ { text: 'abc', offset: 2, limit: 10 },
3732
+ { text: 'abc', offset: 2, limit: 4 },
3733
+ ].map(({ text, offset, limit }) => {
3734
+ scratch.write('subject.txt', text)
3735
+ const handle = openSync(resolve(scratch.path, 'subject.txt'), 'r')
3736
+ try {
3737
+ const bytes = Buffer.alloc(16)
3738
+ const count = configHelpers.readHandleBytes(handle, bytes, offset, limit)
3739
+ return { count, filled: bytes.toString('utf8', offset, count ?? offset) }
3740
+ } finally {
3741
+ closeSync(handle)
3742
+ }
3743
+ })
3744
+ // A file at or under the limit answers its length; the shifted read adds the offset to the
3745
+ // length and fills the buffer past it; a file over the limit answers nothing.
3746
+ expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
3747
+ expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
3748
+ } finally {
3749
+ scratch.destroy()
3750
+ }
3751
+ })
3752
+
3753
+ it('finds the nearest package directory from a manifest, a package boundary, or nothing', () => {
3754
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-find-' })
3755
+ try {
3756
+ scratch.write('held/package.json', '{"name":"held"}')
3757
+ scratch.write('held/src/deep/file.txt', '')
3758
+ scratch.write('modules/node_modules/plain/lib/file.txt', '')
3759
+ scratch.write('modules/node_modules/@scope/named/lib/file.txt', '')
3760
+ scratch.write('modules/node_modules/@scope/file.txt', '')
3761
+ const held = resolve(scratch.path, 'held')
3762
+ expect(configHelpers.findPackageDirectory(held)).toBe(held)
3763
+ expect(configHelpers.findPackageDirectory(resolve(held, 'src/deep'))).toBe(held)
3764
+ const plain = resolve(scratch.path, 'modules/node_modules/plain')
3765
+ expect(configHelpers.findPackageDirectory(resolve(plain, 'lib'))).toBe(plain)
3766
+ const named = resolve(scratch.path, 'modules/node_modules/@scope/named')
3767
+ expect(configHelpers.findPackageDirectory(resolve(named, 'lib'))).toBe(named)
3768
+ expect(configHelpers.findPackageDirectory(named)).toBe(named)
3769
+ // A scope directory alone is not a package boundary, so the walk passes it by.
3427
3770
  expect(
3428
- configHelpers.trustedPackageRootFor(source, new Set([realpathSync.native(packageRoot)])),
3429
- ).toBe(realpathSync.native(packageRoot))
3771
+ configHelpers.isPackageBoundary(resolve(scratch.path, 'modules/node_modules/@scope')),
3772
+ ).toBe(false)
3773
+ // The walk ends at the filesystem root; the answer there is the root only if it holds a manifest.
3774
+ const root = parse(scratch.path).root
3775
+ expect(configHelpers.findPackageDirectory(root)).toBe(
3776
+ existsSync(resolve(root, 'package.json')) ? root : undefined,
3777
+ )
3430
3778
  } finally {
3431
3779
  scratch.destroy()
3432
3780
  }
3433
3781
  })
3434
3782
 
3783
+ it('holds the workspace defaults until a resolved build config replaces them', async () => {
3784
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-state-' })
3785
+ try {
3786
+ const fresh = new configHelpers.BuildState()
3787
+ expect(fresh.building).toBe(false)
3788
+ expect(fresh.output).toBe(configHelpers.WORKSPACE_ROOT)
3789
+ expect(fresh.root).toBe(configHelpers.WORKSPACE_ROOT)
3790
+ expect(fresh.source).toBe('')
3791
+ expect(new configHelpers.BuildState('C:/output').output).toBe('C:/output')
3792
+
3793
+ const entry = resolve(scratch.path, 'index.ts')
3794
+ const inline: InlineConfig = {
3795
+ root: scratch.path,
3796
+ configFile: false,
3797
+ logLevel: 'silent',
3798
+ build: { outDir: 'out', lib: { entry, formats: ['es'] } },
3799
+ }
3800
+ const building = new configHelpers.BuildState()
3801
+ building.resolve(await resolveConfig(inline, 'build'))
3802
+ expect(building.building).toBe(true)
3803
+ expect(building.output).toBe(resolve(scratch.path, 'out'))
3804
+ expect(building.root).toBe(realpathSync.native(scratch.path))
3805
+ expect(building.source).toBe(entry)
3806
+
3807
+ // The control: a serve command is not a build, and a config with no library entry has no source.
3808
+ const serving = new configHelpers.BuildState()
3809
+ serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
3810
+ expect(serving.building).toBe(false)
3811
+ expect(serving.source).toBe('')
3812
+ } finally {
3813
+ scratch.destroy()
3814
+ }
3815
+ })
3816
+
3817
+ it('admits each physical package root once and locates the first admitted root containing a path', () => {
3818
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-config-locate-' })
3819
+ try {
3820
+ scratch.write('modules/outer/inner/index.ts', '')
3821
+ scratch.write('modules/other/index.ts', '')
3822
+ const outer = realpathSync.native(resolve(scratch.path, 'modules/outer'))
3823
+ const inner = realpathSync.native(resolve(scratch.path, 'modules/outer/inner'))
3824
+ const inside = resolve(scratch.path, 'modules/outer/inner/index.ts')
3825
+ const beside = resolve(scratch.path, 'modules/other/index.ts')
3826
+
3827
+ const state = new configHelpers.BuildState()
3828
+ // A fresh state admits nothing, so it locates nothing.
3829
+ expect(state.locate(inside)).toBeUndefined()
3830
+ state.trust(outer)
3831
+ state.trust(outer)
3832
+ state.trust(inner)
3833
+ expect(state.locate(inside)).toBe(outer)
3834
+ expect(state.locate(beside)).toBeUndefined()
3835
+ // Admission order decides which of two containing roots answers.
3836
+ const reversed = new configHelpers.BuildState()
3837
+ reversed.trust(inner)
3838
+ reversed.trust(outer)
3839
+ expect(reversed.locate(inside)).toBe(inner)
3840
+ expect(new configHelpers.BuildState().locate(inside)).toBeUndefined()
3841
+ // The admitted roots are never returned, so the class publishes no member that lists them.
3842
+ expect(Object.getOwnPropertyNames(configHelpers.BuildState.prototype).sort()).toEqual([
3843
+ 'building',
3844
+ 'constructor',
3845
+ 'locate',
3846
+ 'output',
3847
+ 'resolve',
3848
+ 'root',
3849
+ 'source',
3850
+ 'trust',
3851
+ ])
3852
+ } finally {
3853
+ scratch.destroy()
3854
+ }
3855
+ })
3856
+
3857
+ it('climbs from a policy node through its enclosing parentheses alone', () => {
3858
+ const call = { type: 'CallExpression', range: [0, 9] } satisfies PolicyExpression
3859
+ const outer = {
3860
+ type: 'ParenthesizedExpression',
3861
+ range: [1, 8],
3862
+ parent: call,
3863
+ } satisfies PolicyExpression
3864
+ const inner = {
3865
+ type: 'ParenthesizedExpression',
3866
+ range: [2, 7],
3867
+ parent: outer,
3868
+ } satisfies PolicyExpression
3869
+ const leaf = { type: 'Identifier', range: [3, 4], parent: inner } satisfies PolicyExpression
3870
+ expect(nodeToPolicyOuter(leaf)).toBe(outer)
3871
+ expect(nodeToPolicyOuter(inner)).toBe(outer)
3872
+ expect(nodeToPolicyOuter(outer)).toBe(outer)
3873
+ // The control: a node under a non-parenthesized parent, or under none, stays where it is.
3874
+ const argument = { type: 'Identifier', range: [1, 2], parent: call } satisfies PolicyExpression
3875
+ expect(nodeToPolicyOuter(argument)).toBe(argument)
3876
+ expect(nodeToPolicyOuter(call)).toBe(call)
3877
+ })
3878
+
3435
3879
  it('classifies module boundaries and extracts static asset sources', async () => {
3436
3880
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-assets-' })
3437
3881
  try {
@@ -3444,8 +3888,12 @@ describe('configuration helpers', () => {
3444
3888
  expect(configHelpers.hasAsciiUrlControl('clean')).toBe(false)
3445
3889
  expect(configHelpers.hasAsciiUrlControl('line\u0000break')).toBe(true)
3446
3890
  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)
3891
+ expect(configHelpers.isBoundaryExemptModule(resolve(POLICY_ROOT, 'src/core/index.ts'))).toBe(
3892
+ false,
3893
+ )
3894
+ expect(
3895
+ configHelpers.isWorkspaceBoundaryModule(resolve(POLICY_ROOT, 'src/core/index.ts')),
3896
+ ).toBe(true)
3449
3897
  expect(configHelpers.isWorkspaceBoundaryModule(source)).toBe(false)
3450
3898
  expect(configHelpers.isStylesheetPath('src/styles/index.scss?direct')).toBe(true)
3451
3899
  expect(configHelpers.isStylesheetPath('src/core/index.ts')).toBe(false)
@@ -3471,12 +3919,12 @@ describe('configuration helpers', () => {
3471
3919
  'Browser modules cannot depend on Node or server-only modules',
3472
3920
  )
3473
3921
  expect(configHelpers.environmentSourceError('src/browser', '@src/core')).toBeUndefined()
3474
- const expected = resolve(root, 'dist/config-control')
3922
+ const expected = resolve(POLICY_ROOT, 'dist/config-control')
3475
3923
  expect(() => configHelpers.enforceOutputPath(expected, expected)).not.toThrow()
3476
3924
  expect(() =>
3477
- configHelpers.enforceOutputPath(resolve(root, 'dist/outside-control'), expected),
3925
+ configHelpers.enforceOutputPath(resolve(POLICY_ROOT, 'dist/outside-control'), expected),
3478
3926
  ).toThrow('Build output must use its exact configured workspace directory')
3479
- const outside = resolve(dirname(root), 'outside-config-control')
3927
+ const outside = resolve(dirname(POLICY_ROOT), 'outside-config-control')
3480
3928
  expect(() => configHelpers.enforceOutputPath(outside, outside)).toThrow(
3481
3929
  'Build output must remain inside the workspace',
3482
3930
  )
@@ -3485,25 +3933,31 @@ describe('configuration helpers', () => {
3485
3933
  it('admits computed artifact imports through Vite while retaining literal boundary inspection', async () => {
3486
3934
  for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => admitted)) {
3487
3935
  await expect(
3488
- configHelpers.environmentAssetSources('void import(process.argv[2])', resolve(root, path)),
3936
+ configHelpers.environmentAssetSources(
3937
+ 'void import(process.argv[2])',
3938
+ resolve(POLICY_ROOT, path),
3939
+ ),
3489
3940
  ).resolves.toEqual([])
3490
3941
  }
3491
3942
  for (const [path] of ARTIFACT_IMPORT_CASES.filter(([, admitted]) => !admitted)) {
3492
3943
  await expect(
3493
- configHelpers.environmentAssetSources('void import(process.argv[2])', resolve(root, path)),
3944
+ configHelpers.environmentAssetSources(
3945
+ 'void import(process.argv[2])',
3946
+ resolve(POLICY_ROOT, path),
3947
+ ),
3494
3948
  ).rejects.toThrow('Dynamic imports must use static string values')
3495
3949
  }
3496
3950
  await expect(
3497
3951
  configHelpers.environmentAssetSources(
3498
3952
  "void import('@src/browser')",
3499
- resolve(root, 'src/server/artifacts/Artifacts.ts'),
3953
+ resolve(POLICY_ROOT, 'src/server/artifacts/Artifacts.ts'),
3500
3954
  ),
3501
3955
  ).resolves.toEqual(['@src/browser'])
3502
3956
  expect(configHelpers.environmentSourceError('src/server', '@src/browser')).toBeDefined()
3503
3957
  await expect(
3504
3958
  configHelpers.environmentAssetSources(
3505
3959
  'void import(process.argv[2])',
3506
- resolve(root, '../outside/src/server/artifacts/Artifacts.ts'),
3960
+ resolve(POLICY_ROOT, '../outside/src/server/artifacts/Artifacts.ts'),
3507
3961
  ),
3508
3962
  ).rejects.toThrow('Dynamic imports must use static string values')
3509
3963
  })
@@ -3512,16 +3966,19 @@ describe('configuration helpers', () => {
3512
3966
  const environments: ReadonlyArray<
3513
3967
  'src/core' | 'src/browser' | 'src/server' | 'app/core' | 'app/browser' | 'app/server'
3514
3968
  > = ['src/core', 'src/browser', 'src/server', 'app/core', 'app/browser', 'app/server']
3515
- const owner = environments.find((environment) => existsSync(resolve(root, environment)))
3969
+ const owner = environments.find((environment) => existsSync(resolve(POLICY_ROOT, environment)))
3516
3970
  if (owner === undefined) throw new Error('The workspace carries no environment plugin target')
3517
- const scratch = createPolicyScratch({ parent: resolve(root, owner), prefix: 'config-build-' })
3971
+ const scratch = createPolicyScratch({
3972
+ parent: resolve(POLICY_ROOT, owner),
3973
+ prefix: 'config-build-',
3974
+ })
3518
3975
  const workspace = scratch.path
3519
3976
  try {
3520
3977
  const source = resolve(workspace, 'index.ts')
3521
3978
  writeFileSync(source, 'export const control = true\n', 'utf8')
3522
3979
  await expect(
3523
3980
  build({
3524
- root,
3981
+ root: POLICY_ROOT,
3525
3982
  configFile: false,
3526
3983
  logLevel: 'silent',
3527
3984
  publicDir: false,
@@ -3568,7 +4025,7 @@ describe('configuration helpers', () => {
3568
4025
  // raise its passed count with nothing measured. Every generated workspace runs its projects
3569
4026
  // under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
3570
4027
  // condition in it are read by re-running the project with a reporter that names skipped cases.
3571
- it.skipIf(!publishes)(
4028
+ it.skipIf(!POLICY_PUBLISHES)(
3572
4029
  'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
3573
4030
  () => {
3574
4031
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
@@ -3576,10 +4033,10 @@ describe('configuration helpers', () => {
3576
4033
  // no core project, so this walks to the first face the workspace actually carries.
3577
4034
  const faces = ['core', 'browser', 'server']
3578
4035
  const face = faces.find((candidate) =>
3579
- existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
4036
+ existsSync(resolve(POLICY_ROOT, `configs/src/tsconfig.${candidate}.json`)),
3580
4037
  )
3581
4038
  if (face === undefined) throw new Error('The workspace declares no face project')
3582
- const project = resolve(root, `configs/src/tsconfig.${face}.json`)
4039
+ const project = resolve(POLICY_ROOT, `configs/src/tsconfig.${face}.json`)
3583
4040
  const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
3584
4041
  if (typeof declared !== 'object' || declared === null) {
3585
4042
  throw new Error(`The ${face} project is not a TypeScript configuration record`)
@@ -3634,7 +4091,7 @@ describe('configuration helpers', () => {
3634
4091
  const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
3635
4092
  // Any project path resolves these, because each reading is refused by the text before the
3636
4093
  // path is read. The root project is the one file every workspace carries.
3637
- const project = resolve(root, 'tsconfig.json')
4094
+ const project = resolve(POLICY_ROOT, 'tsconfig.json')
3638
4095
  expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
3639
4096
  expect(
3640
4097
  configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
@@ -3647,7 +4104,7 @@ describe('configuration helpers', () => {
3647
4104
  configHelpers.readCompilerOutput(compiler, [
3648
4105
  '--showConfig',
3649
4106
  '-p',
3650
- resolve(root, 'configs/tsconfig.absent.json'),
4107
+ resolve(POLICY_ROOT, 'configs/tsconfig.absent.json'),
3651
4108
  ]),
3652
4109
  ).toThrow('The declaration compiler failed')
3653
4110
 
@@ -3720,12 +4177,12 @@ describe('configuration helpers', () => {
3720
4177
 
3721
4178
  // A hoisted install can place the extractor anywhere `require.resolve` reaches, so the skip
3722
4179
  // 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)
4180
+ it.skipIf(POLICY_EXTRACTOR === undefined)('finds the resolved extractor on disk', () => {
4181
+ if (POLICY_EXTRACTOR === undefined) throw new Error('The extractor resolution left no path')
4182
+ expect(existsSync(POLICY_EXTRACTOR)).toBe(true)
3726
4183
  })
3727
4184
 
3728
- it.skipIf(extractorPath !== undefined)('rejects resolving the unavailable extractor', () => {
4185
+ it.skipIf(POLICY_EXTRACTOR !== undefined)('rejects resolving the unavailable extractor', () => {
3729
4186
  expect(() => createRequire(import.meta.url).resolve('@microsoft/api-extractor')).toThrow(
3730
4187
  'Cannot find module',
3731
4188
  )
@@ -3733,7 +4190,7 @@ describe('configuration helpers', () => {
3733
4190
 
3734
4191
  // The roll-up loads the declaration extractor, which only a workspace publishing source from
3735
4192
  // `src` installs. Where `require.resolve` rejects that package, this proof does not apply.
3736
- it.skipIf(extractorPath === undefined)(
4193
+ it.skipIf(POLICY_EXTRACTOR === undefined)(
3737
4194
  'rolls one face into a single declaration and rewrites its core and browser specifiers',
3738
4195
  async () => {
3739
4196
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-rollup-' })
@@ -3878,3 +4335,133 @@ describe('configuration helpers', () => {
3878
4335
  },
3879
4336
  )
3880
4337
  })
4338
+
4339
+ // A `for…of` loop reads one entry, works on it, and stops at the first failure, where a copy of the
4340
+ // list reads every entry first. Each case runs a policy reader over a recorded list and pins the
4341
+ // outcome and the entries it read, as the loop the reader replaced gave them.
4342
+ describe('policy readers consume a caller-supplied list one entry at a time', () => {
4343
+ const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
4344
+ const declarator = (): PolicyExpression => ({
4345
+ type: 'VariableDeclarator',
4346
+ range: [0, 0],
4347
+ id: name,
4348
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
4349
+ })
4350
+ const exported = (): PolicyExpression => ({
4351
+ type: 'ExportNamedDeclaration',
4352
+ range: [47, 63],
4353
+ declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
4354
+ })
4355
+ const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
4356
+ // A revoked proxy refuses every read, which fails the reader once it holds the entry.
4357
+ const refused =
4358
+ <T extends object>(entry: () => T) =>
4359
+ (): T => {
4360
+ const { proxy, revoke } = Proxy.revocable(entry(), {})
4361
+ revoke()
4362
+ return proxy
4363
+ }
4364
+ const throwing = [
4365
+ [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
4366
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
4367
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
4368
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
4369
+ [POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
4370
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
4371
+ ] as const
4372
+
4373
+ it.each([
4374
+ ...createPolicyLazyCases(
4375
+ 'statementToPolicyBindings',
4376
+ (declarations: PolicyExpression[]) =>
4377
+ statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
4378
+ (binding) => binding.name,
4379
+ ),
4380
+ { valid: declarator, refusal: refused(declarator) },
4381
+ [
4382
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4383
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4384
+ ...throwing,
4385
+ ],
4386
+ ),
4387
+ ...createPolicyLazyCases(
4388
+ 'reportDomain',
4389
+ (body: PolicyExpression[]) => {
4390
+ const reports: Array<string | undefined> = []
4391
+ reportDomain(
4392
+ {
4393
+ filename: '/ws/src/server/execution/run.ts',
4394
+ cwd: '/ws',
4395
+ sourceCode: { text: '', getAllComments: () => [] },
4396
+ report: (diagnostic) => {
4397
+ reports.push(diagnostic.messageId)
4398
+ },
4399
+ },
4400
+ { type: 'Program', range: [0, 0], body },
4401
+ )
4402
+ return reports
4403
+ },
4404
+ { valid: exported, refusal: refused(exported) },
4405
+ [
4406
+ [POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
4407
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4408
+ ...throwing,
4409
+ ],
4410
+ ),
4411
+ ...createPolicyLazyCases(
4412
+ 'programToPolicyDocs statements',
4413
+ (body: PolicyExpression[]) =>
4414
+ programToPolicyDocs(
4415
+ { type: 'Program', range: [0, 0], body },
4416
+ { text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
4417
+ ).map((doc) => doc.name),
4418
+ { valid: exported, refusal: refused(exported) },
4419
+ [
4420
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4421
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4422
+ ...throwing,
4423
+ ],
4424
+ ),
4425
+ ...createPolicyLazyCases(
4426
+ 'programToPolicyDocs comments',
4427
+ (comments: PolicyComment[]) =>
4428
+ programToPolicyDocs(
4429
+ { type: 'Program', range: [0, 0], body: [exported()] },
4430
+ { text: '/** One. */\n', getAllComments: () => comments },
4431
+ ).map((doc) => doc.name),
4432
+ { valid: comment, refusal: refused(comment) },
4433
+ [
4434
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4435
+ [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4436
+ ...throwing,
4437
+ ],
4438
+ ),
4439
+ ...createPolicyLazyCases(
4440
+ 'programToPolicyDocs comments of a program with no export',
4441
+ (comments: PolicyComment[]) =>
4442
+ programToPolicyDocs(
4443
+ { type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
4444
+ { text: 'import x', getAllComments: () => comments },
4445
+ ).map((doc) => doc.name),
4446
+ { valid: comment, refusal: refused(comment) },
4447
+ [
4448
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
4449
+ [POLICY_LAZY_SHAPES.dense, 'return []', []],
4450
+ [POLICY_LAZY_SHAPES.hole, 'return []', []],
4451
+ [POLICY_LAZY_SHAPES.holeFailure, 'return []', []],
4452
+ [POLICY_LAZY_SHAPES.failureFirst, 'return []', []],
4453
+ [POLICY_LAZY_SHAPES.refusalFirst, 'return []', []],
4454
+ [POLICY_LAZY_SHAPES.holeBetween, 'return []', []],
4455
+ [POLICY_LAZY_SHAPES.unlisted, 'return []', []],
4456
+ ],
4457
+ ),
4458
+ ])('$label', ({ observe, outcome, reads }) => {
4459
+ const read: number[] = []
4460
+ expect(
4461
+ observe((index) => {
4462
+ read.push(index)
4463
+ }),
4464
+ ).toBe(outcome)
4465
+ expect(read).toStrictEqual(reads)
4466
+ })
4467
+ })