@orkestrel/scaffold 0.0.72 → 0.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/main.js +75 -34
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/skills/orkestrel-prove-journey/SKILL.md +175 -29
- package/dist/host/agents/skills/orkestrel-prove-journey/references/captures.md +42 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/decide.md +34 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/layer.md +248 -52
- package/dist/host/agents/skills/orkestrel-prove-journey/references/statechart.md +255 -49
- package/dist/host/agents/skills/orkestrel-prove-journey/references/styles.md +111 -23
- package/dist/host/agents/transports/codex.md +7 -0
- package/dist/host/claude/agents/orkestrel.md +46 -46
- package/dist/host/claude/rules/documentation.md +2 -0
- package/dist/host/claude/rules/tests.md +5 -3
- package/dist/host/claude/rules/workspace.md +26 -15
- package/dist/host/dotfiles/gitignore +3 -0
- package/dist/host/guides/README.md +5 -0
- package/dist/host/guides/scaffold.md +97 -13
- package/dist/host/guides/test.md +788 -206
- package/dist/host/manifest.json +22 -32
- package/dist/host/tests/config.test.ts +272 -121
- package/dist/host/tests/policy.test.ts +11 -1
- package/dist/host/tests/setupPolicy.ts +571 -7
- package/dist/src/core/index.cjs +109 -18
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +19 -6
- package/dist/src/core/index.d.ts +19 -6
- package/dist/src/core/index.js +109 -19
- package/dist/src/core/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
5
5
|
import {
|
|
6
6
|
existsSync,
|
|
7
7
|
globSync,
|
|
8
|
+
lstatSync,
|
|
8
9
|
mkdtempSync,
|
|
9
10
|
mkdirSync,
|
|
10
11
|
readdirSync,
|
|
@@ -63,6 +64,33 @@ import { describe, expect, it } from 'vitest'
|
|
|
63
64
|
|
|
64
65
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
65
66
|
|
|
67
|
+
// The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
|
|
68
|
+
// a physical directory rather than from a `src` entry of any shape. A regular file, an empty
|
|
69
|
+
// directory, a directory holding no recognized environment, a symbolic link, and an entry this host
|
|
70
|
+
// refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
|
|
71
|
+
// put every one of them under the face proof. The inspection is caught rather than excused by
|
|
72
|
+
// option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
|
|
73
|
+
// throw on this line is a collection error that takes this whole file down in that target rather
|
|
74
|
+
// than failing one case. `isPhysicalDirectory` reads the same two facts off an `lstat` and returns
|
|
75
|
+
// false for every inspection error, and this reads them the same way. This is the derivation
|
|
76
|
+
// `targetToEnvironments` uses, which filters the environment names by a contained physical
|
|
77
|
+
// directory, and it answers the same question `blueprintToScripts`
|
|
78
|
+
// answers from a blueprint's own `src` list. A workspace declaring app environments alone publishes
|
|
79
|
+
// no library, so it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face
|
|
80
|
+
// proof is conditioned on this axis rather than on a face wrapper: a workspace that declares the
|
|
81
|
+
// axis and vendors no wrapper carries the defect such a proof reports, so that workspace must still
|
|
82
|
+
// fail the proof. The manifest cases read the same fact from their own `private` flag and name it
|
|
83
|
+
// `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
|
|
84
|
+
// workspace whose manifest and shape disagree fails one of the two rather than neither.
|
|
85
|
+
const publishes = ['core', 'browser', 'server'].some((environment) => {
|
|
86
|
+
try {
|
|
87
|
+
const entry = lstatSync(resolve(root, 'src', environment))
|
|
88
|
+
return entry.isDirectory() && !entry.isSymbolicLink()
|
|
89
|
+
} catch {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
66
94
|
// A declaration roll-up loads the extractor package, which only a workspace publishing source from
|
|
67
95
|
// `src` installs. The resolution below is the mechanism its proof is conditioned on.
|
|
68
96
|
let extractorPath: string | undefined
|
|
@@ -199,8 +227,8 @@ describe('root configuration', () => {
|
|
|
199
227
|
setup: ['./tests/setup.ts'],
|
|
200
228
|
})
|
|
201
229
|
// A row that is a configuration rather than a factory. Every generated
|
|
202
|
-
// workspace registers the factory itself, so this shape is required
|
|
203
|
-
// rather than observed: the proof exercises that resolution wherever it runs
|
|
230
|
+
// workspace registers the factory itself, so this shape is required by the
|
|
231
|
+
// expectation rather than observed: the proof exercises that resolution wherever it runs
|
|
204
232
|
// instead of only where a hand-written configuration happens to produce it.
|
|
205
233
|
expected.set('concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] })
|
|
206
234
|
|
|
@@ -418,6 +446,9 @@ describe('root configuration', () => {
|
|
|
418
446
|
if (existsSync(resolve(root, 'configs/app/vite.showcase.config.ts'))) {
|
|
419
447
|
required.push('configs/app/vite.showcase.config.ts')
|
|
420
448
|
}
|
|
449
|
+
if (existsSync(resolve(root, 'configs/app/vite.journey.config.ts'))) {
|
|
450
|
+
required.push('configs/app/vite.journey.config.ts')
|
|
451
|
+
}
|
|
421
452
|
if (required[0] === undefined) {
|
|
422
453
|
throw new Error('The workspace selects no configuration target')
|
|
423
454
|
}
|
|
@@ -432,80 +463,172 @@ describe('root configuration', () => {
|
|
|
432
463
|
).map((path) => path.replaceAll('\\', '/'))
|
|
433
464
|
const extra = 'configs/app/vite.core.config.ts'
|
|
434
465
|
const controlled = found.concat(extra)
|
|
466
|
+
const planted = createPolicyScratch({ prefix: 'config-journey-' })
|
|
467
|
+
const journey = 'configs/app/vite.journey.config.ts'
|
|
468
|
+
planted.write(
|
|
469
|
+
journey,
|
|
470
|
+
`export default {
|
|
471
|
+
test: { projects: [() => ({
|
|
472
|
+
test: {
|
|
473
|
+
name: { label: 'journey:desktop' },
|
|
474
|
+
include: ['tests/app/browser/integration.test.ts'],
|
|
475
|
+
exclude: [],
|
|
476
|
+
setupFiles: ['./tests/setup.ts', './tests/setupBrowser.ts'],
|
|
477
|
+
provide: { variant: 'desktop', variants: [{ name: 'desktop', width: 1280, height: 800 }], capture: false },
|
|
478
|
+
browser: { enabled: true, instances: [{ browser: 'chromium', headless: true }] },
|
|
479
|
+
},
|
|
480
|
+
})] },
|
|
481
|
+
}\n`,
|
|
482
|
+
)
|
|
483
|
+
const journeys: object[][] = []
|
|
484
|
+
const targets = required.map((wrapper) => ({ wrapper, directory: root }))
|
|
485
|
+
targets.push({ wrapper: journey, directory: planted.path })
|
|
486
|
+
controlled.push(journey)
|
|
435
487
|
|
|
436
488
|
// Required wrappers come from selected src/app targets. Only that set is loaded and validated.
|
|
437
489
|
// Extra wrappers remain in the found population but are ignored before their content is read.
|
|
438
490
|
expect(controlled).toContain(extra)
|
|
439
491
|
expect(required).not.toContain(extra)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
492
|
+
try {
|
|
493
|
+
for (const { wrapper, directory } of targets) {
|
|
494
|
+
expect(controlled).toContain(wrapper)
|
|
495
|
+
const viteMatch =
|
|
496
|
+
/^configs\/(src|app)\/vite\.(core|browser|server|bin|showcase|journey)\.config\.ts$/u.exec(
|
|
497
|
+
wrapper,
|
|
498
|
+
)
|
|
499
|
+
if (viteMatch !== null) {
|
|
500
|
+
const [, axis, environment] = viteMatch
|
|
501
|
+
if (axis === undefined || environment === undefined) {
|
|
502
|
+
throw new Error(`${wrapper} carries no target`)
|
|
503
|
+
}
|
|
504
|
+
const loaded = await loadConfigFromFile(
|
|
505
|
+
{ command: 'build', mode: 'test', isSsrBuild: false, isPreview: false },
|
|
506
|
+
resolve(directory, wrapper),
|
|
507
|
+
directory,
|
|
508
|
+
'silent',
|
|
509
|
+
)
|
|
510
|
+
if (loaded === null) throw new Error(`${wrapper} did not load`)
|
|
511
|
+
if (environment === 'journey') {
|
|
512
|
+
const projects = loaded.config.test?.projects
|
|
513
|
+
if (!Array.isArray(projects) || projects.length === 0) {
|
|
514
|
+
throw new Error(`${wrapper} carries no journey projects`)
|
|
515
|
+
}
|
|
516
|
+
const tests: object[] = []
|
|
517
|
+
for (const factory of projects) {
|
|
518
|
+
if (typeof factory !== 'function')
|
|
519
|
+
throw new Error(`${wrapper} carries no project factory`)
|
|
520
|
+
const project: unknown = await Reflect.apply(factory, undefined, [
|
|
521
|
+
{ command: 'serve', mode: 'test' },
|
|
522
|
+
])
|
|
523
|
+
if (typeof project !== 'object' || project === null)
|
|
524
|
+
throw new Error('A journey project is not a configuration')
|
|
525
|
+
const test: unknown = Object.getOwnPropertyDescriptor(project, 'test')?.value
|
|
526
|
+
if (typeof test !== 'object' || test === null)
|
|
527
|
+
throw new Error('A journey project carries no test block')
|
|
528
|
+
tests.push(test)
|
|
529
|
+
}
|
|
530
|
+
journeys.push(tests)
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
const output = loaded.config.build?.outDir
|
|
534
|
+
if (output === undefined) throw new Error(`${wrapper} carries no output`)
|
|
535
|
+
const expected =
|
|
536
|
+
environment === 'bin'
|
|
537
|
+
? 'dist/bin'
|
|
538
|
+
: environment === 'showcase'
|
|
539
|
+
? 'dist/showcase'
|
|
540
|
+
: `dist/${axis}/${environment}`
|
|
541
|
+
if (resolve(root, output) !== resolve(root, expected)) {
|
|
542
|
+
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
543
|
+
}
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const tsconfigMatch =
|
|
548
|
+
/^configs\/(src|app)\/tsconfig\.(core|browser|server|bin)\.json$/u.exec(wrapper)
|
|
549
|
+
if (tsconfigMatch === null) {
|
|
550
|
+
throw new Error(`${wrapper} is not a required target wrapper`)
|
|
551
|
+
}
|
|
552
|
+
const [, axis, environment] = tsconfigMatch
|
|
446
553
|
if (axis === undefined || environment === undefined) {
|
|
447
|
-
throw new Error(`${wrapper} carries no
|
|
554
|
+
throw new Error(`${wrapper} carries no TypeScript scope`)
|
|
448
555
|
}
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
root,
|
|
453
|
-
'silent',
|
|
454
|
-
)
|
|
455
|
-
if (loaded === null) throw new Error(`${wrapper} did not load`)
|
|
456
|
-
const output = loaded.config.build?.outDir
|
|
457
|
-
if (output === undefined) throw new Error(`${wrapper} carries no output`)
|
|
458
|
-
const expected =
|
|
459
|
-
environment === 'bin'
|
|
460
|
-
? 'dist/bin'
|
|
461
|
-
: environment === 'showcase'
|
|
462
|
-
? 'dist/showcase'
|
|
463
|
-
: `dist/${axis}/${environment}`
|
|
464
|
-
if (resolve(root, output) !== resolve(root, expected)) {
|
|
465
|
-
throw new Error(`${wrapper} resolves to the wrong output`)
|
|
556
|
+
const parsed: unknown = JSON.parse(readFileSync(resolve(root, wrapper), 'utf8'))
|
|
557
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
558
|
+
throw new Error(`${wrapper} is not a TypeScript configuration record`)
|
|
466
559
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
560
|
+
const compilerOptions: unknown = Object.getOwnPropertyDescriptor(
|
|
561
|
+
parsed,
|
|
562
|
+
'compilerOptions',
|
|
563
|
+
)?.value
|
|
564
|
+
if (typeof compilerOptions !== 'object' || compilerOptions === null) {
|
|
565
|
+
throw new Error(`${wrapper} carries no compiler options`)
|
|
566
|
+
}
|
|
567
|
+
const lib: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'lib')?.value
|
|
568
|
+
const types: unknown = Object.getOwnPropertyDescriptor(compilerOptions, 'types')?.value
|
|
569
|
+
const expectedLib =
|
|
570
|
+
environment === 'core'
|
|
571
|
+
? ['ESNext', 'WebWorker']
|
|
572
|
+
: environment === 'browser'
|
|
573
|
+
? ['ESNext', 'DOM', 'DOM.Iterable']
|
|
574
|
+
: ['ESNext']
|
|
575
|
+
const expectedTypes =
|
|
576
|
+
environment === 'core'
|
|
577
|
+
? []
|
|
578
|
+
: environment === 'browser'
|
|
579
|
+
? axis === 'app'
|
|
580
|
+
? ['vite/client', 'vue']
|
|
581
|
+
: ['vite/client']
|
|
582
|
+
: ['node']
|
|
583
|
+
expect(lib).toStrictEqual(expectedLib)
|
|
584
|
+
expect(types).toStrictEqual(expectedTypes)
|
|
483
585
|
}
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
586
|
+
for (const tests of journeys) {
|
|
587
|
+
const names: string[] = []
|
|
588
|
+
for (const test of tests) {
|
|
589
|
+
expect(Object.getOwnPropertyDescriptor(test, 'include')?.value).toStrictEqual([
|
|
590
|
+
'tests/app/browser/integration.test.ts',
|
|
591
|
+
])
|
|
592
|
+
expect(Object.getOwnPropertyDescriptor(test, 'exclude')?.value).toStrictEqual([])
|
|
593
|
+
expect(Object.getOwnPropertyDescriptor(test, 'setupFiles')?.value).toStrictEqual([
|
|
594
|
+
'./tests/setup.ts',
|
|
595
|
+
'./tests/setupBrowser.ts',
|
|
596
|
+
])
|
|
597
|
+
const name: unknown = Object.getOwnPropertyDescriptor(test, 'name')?.value
|
|
598
|
+
const provide: unknown = Object.getOwnPropertyDescriptor(test, 'provide')?.value
|
|
599
|
+
const browser: unknown = Object.getOwnPropertyDescriptor(test, 'browser')?.value
|
|
600
|
+
if (
|
|
601
|
+
typeof name !== 'object' ||
|
|
602
|
+
name === null ||
|
|
603
|
+
typeof provide !== 'object' ||
|
|
604
|
+
provide === null ||
|
|
605
|
+
typeof browser !== 'object' ||
|
|
606
|
+
browser === null
|
|
607
|
+
)
|
|
608
|
+
throw new Error('A journey project carries no name, provide, or browser block')
|
|
609
|
+
const variant: unknown = Object.getOwnPropertyDescriptor(provide, 'variant')?.value
|
|
610
|
+
if (typeof variant !== 'string' || variant.length === 0)
|
|
611
|
+
throw new Error('A journey project carries no variant name')
|
|
612
|
+
expect(Object.getOwnPropertyDescriptor(name, 'label')?.value).toBe(`journey:${variant}`)
|
|
613
|
+
expect(typeof Object.getOwnPropertyDescriptor(provide, 'capture')?.value).toBe('boolean')
|
|
614
|
+
expect(Object.getOwnPropertyDescriptor(browser, 'enabled')?.value).toBe(true)
|
|
615
|
+
const variants: unknown = Object.getOwnPropertyDescriptor(provide, 'variants')?.value
|
|
616
|
+
if (
|
|
617
|
+
!Array.isArray(variants) ||
|
|
618
|
+
!variants.some(
|
|
619
|
+
(candidate: unknown) =>
|
|
620
|
+
typeof candidate === 'object' &&
|
|
621
|
+
candidate !== null &&
|
|
622
|
+
Object.getOwnPropertyDescriptor(candidate, 'name')?.value === variant,
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
throw new Error('A journey variant is absent from its declared set')
|
|
626
|
+
expect(names).not.toContain(variant)
|
|
627
|
+
names.push(variant)
|
|
628
|
+
}
|
|
490
629
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const expectedLib =
|
|
494
|
-
environment === 'core'
|
|
495
|
-
? ['ESNext', 'WebWorker']
|
|
496
|
-
: environment === 'browser'
|
|
497
|
-
? ['ESNext', 'DOM', 'DOM.Iterable']
|
|
498
|
-
: ['ESNext']
|
|
499
|
-
const expectedTypes =
|
|
500
|
-
environment === 'core'
|
|
501
|
-
? []
|
|
502
|
-
: environment === 'browser'
|
|
503
|
-
? axis === 'app'
|
|
504
|
-
? ['vite/client', 'vue']
|
|
505
|
-
: ['vite/client']
|
|
506
|
-
: ['node']
|
|
507
|
-
expect(lib).toStrictEqual(expectedLib)
|
|
508
|
-
expect(types).toStrictEqual(expectedTypes)
|
|
630
|
+
} finally {
|
|
631
|
+
planted.destroy()
|
|
509
632
|
}
|
|
510
633
|
|
|
511
634
|
const controlRequired = [
|
|
@@ -530,7 +653,7 @@ describe('root configuration', () => {
|
|
|
530
653
|
if (typeof scripts !== 'object' || scripts === null) {
|
|
531
654
|
throw new Error('The package manifest carries no scripts')
|
|
532
655
|
}
|
|
533
|
-
const
|
|
656
|
+
const manifestPublishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
|
|
534
657
|
const test = Object.getOwnPropertyDescriptor(scripts, 'test')?.value
|
|
535
658
|
const config = Object.getOwnPropertyDescriptor(scripts, 'test:config')?.value
|
|
536
659
|
const distribution = Object.getOwnPropertyDescriptor(scripts, 'test:distribution')?.value
|
|
@@ -576,9 +699,9 @@ describe('root configuration', () => {
|
|
|
576
699
|
)
|
|
577
700
|
expect(typeof test === 'string' && test.includes('test:distribution')).toBe(false)
|
|
578
701
|
expect(typeof publish === 'string' && publish.includes('npm run test:distribution')).toBe(
|
|
579
|
-
hasDistribution &&
|
|
702
|
+
hasDistribution && manifestPublishes,
|
|
580
703
|
)
|
|
581
|
-
expect(typeof publish === 'string').toBe(
|
|
704
|
+
expect(typeof publish === 'string').toBe(manifestPublishes)
|
|
582
705
|
expect(integration).toBe(
|
|
583
706
|
hasIntegration
|
|
584
707
|
? 'vitest run --config vite.config.ts --no-cache --reporter=dot --project integration'
|
|
@@ -610,10 +733,10 @@ describe('root configuration', () => {
|
|
|
610
733
|
: undefined,
|
|
611
734
|
)
|
|
612
735
|
expect(typeof test === 'string' && test.includes('npm run test:service')).toBe(
|
|
613
|
-
hasService && !
|
|
736
|
+
hasService && !manifestPublishes,
|
|
614
737
|
)
|
|
615
738
|
expect(typeof publish === 'string' && publish.includes('npm run test:service')).toBe(
|
|
616
|
-
hasService &&
|
|
739
|
+
hasService && manifestPublishes,
|
|
617
740
|
)
|
|
618
741
|
})
|
|
619
742
|
|
|
@@ -626,14 +749,14 @@ describe('root configuration', () => {
|
|
|
626
749
|
if (typeof scripts !== 'object' || scripts === null) {
|
|
627
750
|
throw new Error('The package manifest carries no scripts')
|
|
628
751
|
}
|
|
629
|
-
const
|
|
752
|
+
const manifestPublishes = Object.getOwnPropertyDescriptor(manifest, 'private')?.value !== true
|
|
630
753
|
const prepack = Object.getOwnPropertyDescriptor(scripts, 'prepack')?.value
|
|
631
|
-
expect(prepack).toBe(
|
|
754
|
+
expect(prepack).toBe(manifestPublishes ? 'npm run build' : undefined)
|
|
632
755
|
|
|
633
756
|
const controlled = { ...scripts, prepack: 'npm run control' }
|
|
634
757
|
expect(() => {
|
|
635
758
|
const control = Object.getOwnPropertyDescriptor(controlled, 'prepack')?.value
|
|
636
|
-
expect(control).toBe(
|
|
759
|
+
expect(control).toBe(manifestPublishes ? 'npm run build' : undefined)
|
|
637
760
|
}).toThrow(/expected/u)
|
|
638
761
|
})
|
|
639
762
|
|
|
@@ -2150,54 +2273,81 @@ describe('configuration helpers', () => {
|
|
|
2150
2273
|
}
|
|
2151
2274
|
})
|
|
2152
2275
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
'
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2276
|
+
// The scope reading applies only where the workspace publishes source from `src`, which is what
|
|
2277
|
+
// `publishes` reads, and the name ends at that mechanism. An absent face project is the defect
|
|
2278
|
+
// this case exists to report rather than a second reason to excuse it, so a workspace holding
|
|
2279
|
+
// the axis and vendoring no `configs/src/` wrapper reaches the throw inside the body.
|
|
2280
|
+
// A skip raises the run's skipped count where a guard inside the case would
|
|
2281
|
+
// raise its passed count with nothing measured. Every generated workspace runs its projects
|
|
2282
|
+
// under `--reporter=dot`, which prints a skip as an unnamed `-`, so the case name and the
|
|
2283
|
+
// condition in it are read by re-running the project with a reporter that names skipped cases.
|
|
2284
|
+
it.skipIf(!publishes)(
|
|
2285
|
+
'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
|
|
2286
|
+
() => {
|
|
2287
|
+
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
2288
|
+
// The order mirrors ENVIRONMENTS in src/core/constants.ts; a server-only workspace vendors
|
|
2289
|
+
// no core project, so this walks to the first face the workspace actually carries.
|
|
2290
|
+
const faces = ['core', 'browser', 'server']
|
|
2291
|
+
const face = faces.find((candidate) =>
|
|
2292
|
+
existsSync(resolve(root, `configs/src/tsconfig.${candidate}.json`)),
|
|
2293
|
+
)
|
|
2294
|
+
if (face === undefined) throw new Error('The workspace declares no face project')
|
|
2295
|
+
const project = resolve(root, `configs/src/tsconfig.${face}.json`)
|
|
2296
|
+
const declared: unknown = JSON.parse(readFileSync(project, 'utf8'))
|
|
2297
|
+
if (typeof declared !== 'object' || declared === null) {
|
|
2298
|
+
throw new Error(`The ${face} project is not a TypeScript configuration record`)
|
|
2299
|
+
}
|
|
2300
|
+
const declaredOptions: unknown = Object.getOwnPropertyDescriptor(
|
|
2301
|
+
declared,
|
|
2302
|
+
'compilerOptions',
|
|
2303
|
+
)?.value
|
|
2304
|
+
if (typeof declaredOptions !== 'object' || declaredOptions === null) {
|
|
2305
|
+
throw new Error(`The ${face} project carries no compiler options`)
|
|
2306
|
+
}
|
|
2307
|
+
const declaredLib: unknown = Object.getOwnPropertyDescriptor(declaredOptions, 'lib')?.value
|
|
2308
|
+
const declaredTypes: unknown = Object.getOwnPropertyDescriptor(
|
|
2309
|
+
declaredOptions,
|
|
2310
|
+
'types',
|
|
2311
|
+
)?.value
|
|
2312
|
+
if (!configHelpers.isStringList(declaredLib) || !configHelpers.isStringList(declaredTypes)) {
|
|
2313
|
+
throw new Error(`The ${face} project declares no lib or types`)
|
|
2314
|
+
}
|
|
2315
|
+
const declaredRootDir: unknown = Object.getOwnPropertyDescriptor(
|
|
2316
|
+
declaredOptions,
|
|
2317
|
+
'rootDir',
|
|
2318
|
+
)?.value
|
|
2319
|
+
if (typeof declaredRootDir !== 'string') {
|
|
2320
|
+
throw new Error(`The ${face} project declares no rootDir`)
|
|
2321
|
+
}
|
|
2322
|
+
const expectedRoot = resolve(dirname(project), declaredRootDir)
|
|
2187
2323
|
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2324
|
+
const scope = configHelpers.parseProjectScope(
|
|
2325
|
+
configHelpers.readCompilerOutput(compiler, ['--showConfig', '-p', project]),
|
|
2326
|
+
project,
|
|
2327
|
+
)
|
|
2328
|
+
if (scope === undefined) throw new Error(`The ${face} project resolved no compiler scope`)
|
|
2329
|
+
// The compiler lowercases every resolved library name, so the committed project is the
|
|
2330
|
+
// second mechanism this reading is compared against rather than the reading itself.
|
|
2331
|
+
expect(scope.lib.map((entry) => entry.toLowerCase())).toStrictEqual(
|
|
2332
|
+
declaredLib.map((entry) => entry.toLowerCase()),
|
|
2333
|
+
)
|
|
2334
|
+
expect(scope.types).toStrictEqual(declaredTypes)
|
|
2335
|
+
expect(scope.root).toBe(expectedRoot)
|
|
2336
|
+
},
|
|
2337
|
+
)
|
|
2200
2338
|
|
|
2339
|
+
// No reading in this case reads anything under `configs/src/`, so each one applies to a workspace
|
|
2340
|
+
// on either axis. The refusals are decided by the text handed to them; the compiler refusal by a
|
|
2341
|
+
// path no workspace shape carries; the manifest name, the module resolution, and the extractor
|
|
2342
|
+
// guard by files and packages a workspace carries on either axis. They sit apart from the
|
|
2343
|
+
// preceding scope reading for that reason: the skip that excuses an app-only workspace from
|
|
2344
|
+
// reading a face project must not excuse it from the helpers behind the roll-up, which it
|
|
2345
|
+
// vendors whether or not it publishes.
|
|
2346
|
+
it('reads the refusals, guards, overrides, and rewrites a declaration roll-up requires from every workspace', () => {
|
|
2347
|
+
const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
|
|
2348
|
+
// Any project path resolves these, because each reading is refused by the text before the
|
|
2349
|
+
// path is read. The root project is the one file every workspace carries.
|
|
2350
|
+
const project = resolve(root, 'tsconfig.json')
|
|
2201
2351
|
expect(configHelpers.parseProjectScope('not a configuration', project)).toBeUndefined()
|
|
2202
2352
|
expect(
|
|
2203
2353
|
configHelpers.parseProjectScope('{"compilerOptions":{"lib":[],"types":[]}}', project),
|
|
@@ -2205,11 +2355,12 @@ describe('configuration helpers', () => {
|
|
|
2205
2355
|
expect(
|
|
2206
2356
|
configHelpers.parseProjectScope('{"compilerOptions":{"lib":[1],"rootDir":"."}}', project),
|
|
2207
2357
|
).toBeUndefined()
|
|
2358
|
+
// The compiler's own refusal, read from a path no workspace shape carries.
|
|
2208
2359
|
expect(() =>
|
|
2209
2360
|
configHelpers.readCompilerOutput(compiler, [
|
|
2210
2361
|
'--showConfig',
|
|
2211
2362
|
'-p',
|
|
2212
|
-
resolve(root, 'configs/
|
|
2363
|
+
resolve(root, 'configs/tsconfig.absent.json'),
|
|
2213
2364
|
]),
|
|
2214
2365
|
).toThrow('The declaration compiler failed')
|
|
2215
2366
|
|
|
@@ -524,7 +524,17 @@ describe('skill family policy', () => {
|
|
|
524
524
|
expect(frontmatter?.description).toBe('Use this skill when a continuation contains: a colon.')
|
|
525
525
|
})
|
|
526
526
|
|
|
527
|
-
for (const control of SKILL_POLICY_CONTROLS
|
|
527
|
+
for (const control of SKILL_POLICY_CONTROLS.filter(
|
|
528
|
+
(candidate) => candidate.violations !== undefined,
|
|
529
|
+
)) {
|
|
530
|
+
it(`${control.label} [membership: ${control.membership}]`, () => {
|
|
531
|
+
expect(inspectPolicyControl(control)).toEqual(control.violations)
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
for (const control of SKILL_POLICY_CONTROLS.filter(
|
|
536
|
+
(candidate) => candidate.violations === undefined,
|
|
537
|
+
)) {
|
|
528
538
|
it(`${control.label} [membership: ${control.membership}]`, () => {
|
|
529
539
|
const violations = inspectPolicyControl(control)
|
|
530
540
|
expect(violations).toHaveLength(1)
|