@orkestrel/scaffold 0.0.100 → 0.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
  7. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
  8. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
  9. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
  10. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
  12. package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
  13. package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
  14. package/dist/bin/main.js +483 -444
  15. package/dist/bin/main.js.map +1 -1
  16. package/dist/host/AGENTS.md +1 -0
  17. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
  20. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
  21. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
  22. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
  23. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
  24. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
  25. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
  26. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
  27. package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
  28. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
  29. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
  30. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
  31. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
  32. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
  33. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
  34. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
  35. package/dist/host/claude/rules/architecture.md +2 -2
  36. package/dist/host/claude/rules/tests.md +8 -2
  37. package/dist/host/claude/rules/typescript.md +130 -0
  38. package/dist/host/claude/rules/workspace.md +36 -12
  39. package/dist/host/configs/helpers.ts +214 -133
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/dotfiles/gitignore +1 -0
  42. package/dist/host/dotfiles/oxlintrc.json +10 -0
  43. package/dist/host/guides/scaffold.md +244 -170
  44. package/dist/host/manifest.json +37 -31
  45. package/dist/host/tests/config.test.ts +1305 -619
  46. package/dist/host/tests/policy.test.ts +19 -0
  47. package/dist/host/tests/setupPolicy.ts +1710 -995
  48. package/dist/src/core/index.cjs +1595 -1686
  49. package/dist/src/core/index.cjs.map +1 -1
  50. package/dist/src/core/index.d.cts +202 -18
  51. package/dist/src/core/index.d.ts +202 -18
  52. package/dist/src/core/index.js +1590 -1687
  53. package/dist/src/core/index.js.map +1 -1
  54. package/dist/src/server/index.cjs +448 -392
  55. package/dist/src/server/index.cjs.map +1 -1
  56. package/dist/src/server/index.d.cts +1182 -1052
  57. package/dist/src/server/index.d.ts +1182 -1052
  58. package/dist/src/server/index.js +447 -395
  59. package/dist/src/server/index.js.map +1 -1
  60. package/package.json +2 -2
@@ -10,13 +10,16 @@ import { spawnSync } from 'node:child_process'
10
10
  import { createRequire } from 'node:module'
11
11
  import {
12
12
  realpathSync,
13
+ chmodSync,
13
14
  existsSync,
14
15
  globSync,
16
+ lstatSync,
15
17
  mkdirSync,
16
18
  mkdtempSync,
17
19
  readFileSync,
18
20
  readdirSync,
19
21
  rmSync,
22
+ statSync,
20
23
  writeFileSync,
21
24
  } from 'node:fs'
22
25
  import { tmpdir } from 'node:os'
@@ -160,6 +163,15 @@ export function createPolicySurfaceFixture(): PolicyScratchInterface {
160
163
  }
161
164
  }
162
165
 
166
+ /** Describes the include, setup files, and probe settings one workspace project registers. */
167
+ export interface PolicyProject {
168
+ readonly benchmark?: readonly string[]
169
+ readonly include: string | readonly string[]
170
+ readonly parallel?: boolean
171
+ readonly pool?: string
172
+ readonly setup: readonly string[]
173
+ }
174
+
163
175
  /** Describes one physical negative control, including the population boundary it attacks. */
164
176
  export interface PolicyControl {
165
177
  readonly label: string
@@ -212,6 +224,513 @@ export function createPolicyScratch(options: {
212
224
  }
213
225
  }
214
226
 
227
+ /**
228
+ * Creates a list with the given length that holds only the entries written at their indexes.
229
+ *
230
+ * @remarks
231
+ * A literal cannot express a hole, so the list is sized first and then written once per index. A
232
+ * `for…of` loop and a spread visit each hole as `undefined`, where `map`, `filter`, and `flatMap`
233
+ * skip it, so a caller-supplied list built here separates the two.
234
+ *
235
+ * @param length - The length of the list, holes included.
236
+ * @param entries - The entry to write at each index; every other index stays a hole.
237
+ * @returns The sparse list.
238
+ */
239
+ export function createPolicySparse<T>(length: number, entries: ReadonlyMap<number, T>): T[] {
240
+ const list: T[] = []
241
+ list.length = length
242
+ for (const [index, entry] of entries) list[index] = entry
243
+ return list
244
+ }
245
+
246
+ /**
247
+ * Creates a list whose entries are getters, so a reader's every read of an entry is recorded.
248
+ *
249
+ * @remarks
250
+ * Only the indexes in `entries` hold a getter, and every other index stays a hole. A getter reports
251
+ * its index to `record` and then produces its value, which may throw, so a case sees which entries
252
+ * a reader touched before it stopped. An iterator reads an entry once as it yields it, where a copy
253
+ * of the list reads every entry before the reader sees the first.
254
+ *
255
+ * @param length - The length of the list, holes included.
256
+ * @param entries - The producer behind each getter, by index.
257
+ * @param record - The function that receives an index each time its getter runs.
258
+ * @param iterable - If `true`, the list keeps the iterator every array has; if `false`, its
259
+ * `Symbol.iterator` is `undefined`, which a loop refuses and `Array.from` and `values()` do not.
260
+ * Default: `true`.
261
+ * @returns The list.
262
+ */
263
+ export function createPolicyReads<T>(
264
+ length: number,
265
+ entries: ReadonlyMap<number, () => T>,
266
+ record: (index: number) => void,
267
+ iterable = true,
268
+ ): T[] {
269
+ const list = createPolicySparse<T>(length, new Map())
270
+ for (const [index, produce] of entries) {
271
+ Object.defineProperty(list, index, {
272
+ configurable: true,
273
+ enumerable: true,
274
+ get() {
275
+ record(index)
276
+ return produce()
277
+ },
278
+ })
279
+ }
280
+ if (!iterable) Object.defineProperty(list, Symbol.iterator, { value: undefined })
281
+ return list
282
+ }
283
+
284
+ /**
285
+ * Throws the error a getter raises when its entry refuses to be read.
286
+ *
287
+ * @returns Never.
288
+ * @throws RangeError - Thrown when called.
289
+ */
290
+ export function failPolicyRead(): never {
291
+ throw new RangeError('The entry refused its read')
292
+ }
293
+
294
+ /**
295
+ * Settles one action into text a case compares for equality.
296
+ *
297
+ * @param action - The action to run.
298
+ * @returns `return` and the JSON of the returned value, or `throw` and the constructor name.
299
+ */
300
+ export function settlePolicyOutcome(action: () => unknown): string {
301
+ try {
302
+ return `return ${JSON.stringify(action())}`
303
+ } catch (error) {
304
+ return `throw ${error instanceof Error ? error.constructor.name : typeof error}`
305
+ }
306
+ }
307
+
308
+ /** Describes one reader run over a recorded list, with the outcome and entry reads it must show. */
309
+ export interface PolicyLazyCase {
310
+ readonly label: string
311
+ readonly outcome: string
312
+ readonly reads: readonly number[]
313
+ readonly observe: (record: (index: number) => void) => string
314
+ }
315
+
316
+ /**
317
+ * Creates one case that runs a reader over a recorded list.
318
+ *
319
+ * @param label - The text naming the reader and the list.
320
+ * @param reader - The reader to run over the list.
321
+ * @param list - The length of the list and the producer behind each getter, by index.
322
+ * @param expected - The settled outcome and the indexes the reader must read, in order.
323
+ * @returns The case.
324
+ */
325
+ export function createPolicyLazyCase<T>(
326
+ label: string,
327
+ reader: (list: T[]) => unknown,
328
+ list: {
329
+ readonly length: number
330
+ readonly entries: ReadonlyMap<number, () => T>
331
+ readonly iterable?: boolean
332
+ },
333
+ expected: { readonly outcome: string; readonly reads: readonly number[] },
334
+ ): PolicyLazyCase {
335
+ return {
336
+ label,
337
+ outcome: expected.outcome,
338
+ reads: expected.reads,
339
+ observe: (record) =>
340
+ settlePolicyOutcome(() =>
341
+ reader(createPolicyReads(list.length, list.entries, record, list.iterable)),
342
+ ),
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Describes the entries of one recorded list.
348
+ *
349
+ * @remarks
350
+ * An index in `entries` produces a value the reader accepts, an index in `refusals` produces a
351
+ * value the reader fails on once it has the value, and an index in `failures` fails when the
352
+ * getter runs. Every other index is a hole. A list with `iterable` set to `false` has no iterator,
353
+ * which a loop refuses and `Array.from` and `values()` do not; it is an iterator list otherwise.
354
+ */
355
+ export interface PolicyLazyShape {
356
+ readonly label: string
357
+ readonly length: number
358
+ readonly entries: readonly number[]
359
+ readonly refusals: readonly number[]
360
+ readonly failures: readonly number[]
361
+ readonly iterable?: boolean
362
+ }
363
+
364
+ /** Holds the list shapes that tell one-entry-at-a-time reading from a copy that reads ahead. */
365
+ export const POLICY_LAZY_SHAPES = Object.freeze({
366
+ empty: { label: 'an empty list', length: 0, entries: [], refusals: [], failures: [] },
367
+ dense: { label: 'one entry', length: 1, entries: [0], refusals: [], failures: [] },
368
+ hole: { label: 'a hole, then an entry', length: 2, entries: [1], refusals: [], failures: [] },
369
+ holeFailure: {
370
+ label: 'a hole, then a failing entry',
371
+ length: 2,
372
+ entries: [],
373
+ refusals: [],
374
+ failures: [1],
375
+ },
376
+ failureFirst: {
377
+ label: 'a failing entry, then an entry',
378
+ length: 2,
379
+ entries: [1],
380
+ refusals: [],
381
+ failures: [0],
382
+ },
383
+ refusalFirst: {
384
+ label: 'a refused entry, then an entry',
385
+ length: 2,
386
+ entries: [1],
387
+ refusals: [0],
388
+ failures: [],
389
+ },
390
+ holeBetween: {
391
+ label: 'an entry, a hole, then an entry',
392
+ length: 3,
393
+ entries: [0, 2],
394
+ refusals: [],
395
+ failures: [],
396
+ },
397
+ unlisted: {
398
+ label: 'two entries on a list without an iterator',
399
+ length: 2,
400
+ entries: [0, 1],
401
+ refusals: [],
402
+ failures: [],
403
+ iterable: false,
404
+ },
405
+ } satisfies Record<string, PolicyLazyShape>)
406
+
407
+ /** Holds values a reader's signature refuses, which a case feeds it to make processing fail. */
408
+ export const POLICY_REFUSALS = Object.freeze({ number: 7, symbol: Symbol('refused') })
409
+
410
+ /**
411
+ * Creates a revoked proxy, which refuses every guard that inspects it.
412
+ *
413
+ * @returns The proxy, whose `Array.isArray` check throws a `TypeError`.
414
+ */
415
+ export function createPolicyRevoked(): object {
416
+ const { proxy, revoke } = Proxy.revocable({}, {})
417
+ revoke()
418
+ return proxy
419
+ }
420
+
421
+ /**
422
+ * Creates one case per list shape for a reader of a caller-supplied list.
423
+ *
424
+ * @param subject - The text naming the reader.
425
+ * @param reader - The reader to run over each list.
426
+ * @param producers - The producer behind each entry the reader accepts, and behind each it refuses.
427
+ * @param rows - Each shape with the settled outcome and the indexes the reader must read, in order.
428
+ * @returns One case per row.
429
+ */
430
+ export function createPolicyLazyCases<T>(
431
+ subject: string,
432
+ reader: (list: T[]) => unknown,
433
+ producers: { readonly valid: () => T; readonly refusal: () => T },
434
+ rows: ReadonlyArray<readonly [PolicyLazyShape, string, readonly number[]]>,
435
+ ): readonly PolicyLazyCase[] {
436
+ return rows.map(([shape, outcome, reads]) =>
437
+ createPolicyLazyCase(
438
+ `${subject}: ${shape.label}`,
439
+ reader,
440
+ {
441
+ length: shape.length,
442
+ entries: new Map<number, () => T>([
443
+ ...shape.entries.map((index): [number, () => T] => [index, producers.valid]),
444
+ ...shape.refusals.map((index): [number, () => T] => [index, producers.refusal]),
445
+ ...shape.failures.map((index): [number, () => T] => [index, failPolicyRead]),
446
+ ]),
447
+ iterable: shape.iterable ?? true,
448
+ },
449
+ { outcome, reads },
450
+ ),
451
+ )
452
+ }
453
+
454
+ /**
455
+ * Holds one case per reader and list shape for the readers that take a caller-supplied list.
456
+ *
457
+ * @remarks
458
+ * Each outcome and read list is what the loop each reader replaced produced. A hole is visited as
459
+ * `undefined`, an entry after a failing one is never read, a refusal names its constructor, and a
460
+ * list without an iterator is refused with a `TypeError` before any entry is read.
461
+ */
462
+ export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
463
+ ...createPolicyLazyCases(
464
+ 'inspectPolicyMirrorPaths',
465
+ (list: string[]) => inspectPolicyMirrorPaths(list, new Set()).map(({ path }) => path),
466
+ {
467
+ valid: () => 'tests/src/core/A.test.ts',
468
+ // The descriptor read answers `any`, so a value the signature refuses reaches the reader.
469
+ refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
470
+ },
471
+ [
472
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
473
+ [POLICY_LAZY_SHAPES.dense, 'return ["tests/src/core/A.test.ts"]', [0]],
474
+ [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
475
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
476
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
477
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
478
+ [POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
479
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
480
+ ],
481
+ ),
482
+ ...createPolicyLazyCases(
483
+ 'inspectPolicyWiring rules',
484
+ (list: string[]) => inspectPolicyWiring({}, list, []),
485
+ {
486
+ valid: () => 'a',
487
+ refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
488
+ },
489
+ [
490
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
491
+ [
492
+ POLICY_LAZY_SHAPES.dense,
493
+ 'return ["a is enabled by no top-level or override rules record"]',
494
+ [0],
495
+ ],
496
+ [
497
+ POLICY_LAZY_SHAPES.hole,
498
+ 'return ["undefined is enabled by no top-level or override rules record","a is enabled by no top-level or override rules record"]',
499
+ [1],
500
+ ],
501
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
502
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
503
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
504
+ [
505
+ POLICY_LAZY_SHAPES.holeBetween,
506
+ 'return ["a is enabled by no top-level or override rules record","undefined is enabled by no top-level or override rules record","a is enabled by no top-level or override rules record"]',
507
+ [0, 2],
508
+ ],
509
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
510
+ ],
511
+ ),
512
+ ...createPolicyLazyCases(
513
+ 'inspectPolicyWiring populations',
514
+ (list: Array<readonly string[]>) => inspectPolicyWiring({}, [], list),
515
+ {
516
+ valid: () => ['a'],
517
+ refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'number')?.value,
518
+ },
519
+ [
520
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
521
+ [
522
+ POLICY_LAZY_SHAPES.dense,
523
+ 'return ["no override files list declares the population exactly: a"]',
524
+ [0],
525
+ ],
526
+ [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
527
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
528
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
529
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
530
+ [POLICY_LAZY_SHAPES.holeBetween, 'throw TypeError', [0]],
531
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
532
+ ],
533
+ ),
534
+ ...createPolicyLazyCases(
535
+ 'inspectPolicyWiring overrides',
536
+ (list: unknown[]) => inspectPolicyWiring({ overrides: list }, [], []),
537
+ { valid: () => ({ files: ['a'] }), refusal: createPolicyRevoked },
538
+ [
539
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
540
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
541
+ [POLICY_LAZY_SHAPES.hole, 'return []', [1]],
542
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
543
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
544
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
545
+ [POLICY_LAZY_SHAPES.holeBetween, 'return []', [0, 2]],
546
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
547
+ ],
548
+ ),
549
+ ...createPolicyLazyCases(
550
+ 'inspectPolicyConfiguration overrides',
551
+ (list: unknown[]) =>
552
+ inspectPolicyConfiguration({
553
+ rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
554
+ overrides: list,
555
+ }),
556
+ { valid: () => ({}), refusal: createPolicyRevoked },
557
+ [
558
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
559
+ [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
560
+ [POLICY_LAZY_SHAPES.hole, 'return ["override entries must be records"]', [1]],
561
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
562
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
563
+ [POLICY_LAZY_SHAPES.refusalFirst, 'throw TypeError', [0]],
564
+ [POLICY_LAZY_SHAPES.holeBetween, 'return ["override entries must be records"]', [0, 2]],
565
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
566
+ ],
567
+ ),
568
+ ...createPolicyLazyCases(
569
+ 'inspectPolicyConfiguration ignorePatterns',
570
+ (list: unknown[]) =>
571
+ inspectPolicyConfiguration({
572
+ rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
573
+ ignorePatterns: list,
574
+ }),
575
+ // The reader skips every entry that is not a string, so no entry refuses it and no row uses the
576
+ // refusal.
577
+ { valid: () => 'src', refusal: createPolicyRevoked },
578
+ [
579
+ [POLICY_LAZY_SHAPES.empty, 'return []', []],
580
+ [POLICY_LAZY_SHAPES.dense, 'return ["ignorePatterns must not reach src"]', [0]],
581
+ [POLICY_LAZY_SHAPES.hole, 'return ["ignorePatterns must not reach src"]', [1]],
582
+ [POLICY_LAZY_SHAPES.holeFailure, 'throw RangeError', [1]],
583
+ [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
584
+ [
585
+ POLICY_LAZY_SHAPES.holeBetween,
586
+ 'return ["ignorePatterns must not reach src","ignorePatterns must not reach src"]',
587
+ [0, 2],
588
+ ],
589
+ [POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []],
590
+ ],
591
+ ),
592
+ ...createPolicyLazyCases(
593
+ 'inspectPolicyFilenamePaths',
594
+ (list: string[]) => inspectPolicyFilenamePaths(list).map(({ path }) => path),
595
+ {
596
+ valid: () => 'src/a.ts',
597
+ refusal: () => Object.getOwnPropertyDescriptor(POLICY_REFUSALS, 'symbol')?.value,
598
+ },
599
+ [[POLICY_LAZY_SHAPES.unlisted, 'throw TypeError', []]],
600
+ ),
601
+ ])
602
+
603
+ /** Describes a population a guide states, the words that mark a sentence stating it, and its exclusion. */
604
+ export interface PolicyPopulation {
605
+ readonly subject: string
606
+ readonly markers: readonly string[]
607
+ readonly exclusion: string
608
+ }
609
+
610
+ /** Describes the population of test files that selects the `studies` fact, and what it leaves out. */
611
+ export const POLICY_STUDIES_POPULATION: PolicyPopulation = Object.freeze({
612
+ subject: '`studies`',
613
+ markers: Object.freeze(['`*.test.ts`', 'test files']),
614
+ exclusion: 'outside any `node_modules` or `.git` directory',
615
+ })
616
+
617
+ /**
618
+ * Collects the guide prose sentences that state one population.
619
+ *
620
+ * @remarks
621
+ * A sentence states the population when it names the subject and one marker. A fenced code block
622
+ * is not prose. The lines of a block that are not table rows are joined into one text, and each
623
+ * cell of a table row is a text of its own, so a `Summary` cell that equals its export's
624
+ * description is held to the same sentences as the prose: the source doc block is where that cell's
625
+ * wording is decided.
626
+ *
627
+ * @param markdown - The guide Markdown.
628
+ * @param population - The subject, markers, and exclusion that describe the population.
629
+ * @returns Each sentence that states the population, in document order.
630
+ */
631
+ export function collectGuideStatements(
632
+ markdown: string,
633
+ population: PolicyPopulation,
634
+ ): readonly string[] {
635
+ return markdown
636
+ .replaceAll(/^```[^\n]*\n[\s\S]*?^```[^\n]*$/gmu, '')
637
+ .split(/(?:\r\n|\n){2,}/u)
638
+ .flatMap((block) => {
639
+ const lines = block.split(/\r\n|\n/u)
640
+ return [
641
+ lines.filter((line) => !line.startsWith('|')).join(' '),
642
+ ...lines.filter((line) => line.startsWith('|')).flatMap((row) => row.split(/(?<!\\)\|/u)),
643
+ ].flatMap((text) =>
644
+ text
645
+ .replaceAll(/\s+/gu, ' ')
646
+ .trim()
647
+ .split(/(?<=[.!?])\s+/u),
648
+ )
649
+ })
650
+ .filter(
651
+ (sentence) =>
652
+ sentence.includes(population.subject) &&
653
+ population.markers.some((marker) => sentence.includes(marker)),
654
+ )
655
+ }
656
+
657
+ /** Names the security identifier of the group every access token holds, in the form `icacls` accepts. */
658
+ export const POLICY_DENY_PRINCIPAL = '*S-1-1-0'
659
+
660
+ /**
661
+ * Runs one executable to completion and returns what it printed.
662
+ *
663
+ * @param file - The executable to run, resolved by name.
664
+ * @param args - The arguments, passed without a shell.
665
+ * @returns The standard output.
666
+ * @throws Error - Thrown when the executable cannot start or exits with a non-zero status.
667
+ */
668
+ export function runPolicyCommand(file: string, args: readonly string[]): string {
669
+ const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true })
670
+ if (result.error !== undefined) throw result.error
671
+ if (result.status !== 0)
672
+ throw new Error(
673
+ `${file} ${args.join(' ')} exited with ${result.status}: ${result.stdout}${result.stderr}`,
674
+ )
675
+ return result.stdout
676
+ }
677
+
678
+ /**
679
+ * Reads the permission state of one file as text two readings compare for equality.
680
+ *
681
+ * @remarks
682
+ * Permission state is platform-defined. A POSIX file carries a mode, read as its octal digits
683
+ * through `statSync`, and a Windows file carries an access-control list, read as the `icacls`
684
+ * listing of the path, so the text holds every explicit and inherited entry in order.
685
+ *
686
+ * @param path - The absolute path of an existing file.
687
+ * @returns The octal mode on POSIX, and the `icacls` listing on Windows.
688
+ * @throws Error - Thrown when the Windows listing command exits with a non-zero status.
689
+ */
690
+ export function readPolicyPermissions(path: string): string {
691
+ return process.platform === 'win32'
692
+ ? runPolicyCommand('icacls', [path])
693
+ : (statSync(path).mode & 0o7777).toString(8)
694
+ }
695
+
696
+ /**
697
+ * Withholds read access to one file from the current user.
698
+ *
699
+ * @remarks
700
+ * Read permission is platform-defined. A POSIX mode of zero withholds it from every user but root,
701
+ * and a Windows access-control entry that denies read withholds it from the holder of every token.
702
+ * A `package.json` that is a directory cannot stand in for this: the regular-file check refuses it
703
+ * before any read starts, so only a file the host refuses to open reaches the read boundary.
704
+ *
705
+ * The restore returns the permission state it found. A POSIX restore writes back the mode read
706
+ * before the change. A Windows restore removes the deny entries of the one principal the denial
707
+ * names, because `icacls /restore` of a saved list needs a restore privilege a standard token does
708
+ * not hold, and `/remove:d` of the user would also strip a deny entry the file already carried. A
709
+ * restore that leaves the state different, as an earlier deny entry for that principal would,
710
+ * throws rather than report success.
711
+ *
712
+ * @param path - The absolute path of an existing file.
713
+ * @returns A function that restores the access, to run before the owning directory is removed.
714
+ * @throws Error - Thrown when a command exits with a non-zero status or the restore differs.
715
+ */
716
+ export function denyPolicyRead(path: string): () => void {
717
+ const before = readPolicyPermissions(path)
718
+ if (process.platform === 'win32') {
719
+ runPolicyCommand('icacls', [path, '/deny', `${POLICY_DENY_PRINCIPAL}:(R)`])
720
+ } else {
721
+ chmodSync(path, 0o000)
722
+ }
723
+ return () => {
724
+ if (process.platform === 'win32') {
725
+ runPolicyCommand('icacls', [path, '/remove:d', POLICY_DENY_PRINCIPAL])
726
+ } else {
727
+ chmodSync(path, before)
728
+ }
729
+ if (readPolicyPermissions(path) !== before)
730
+ throw new Error(`The permissions of ${path} did not restore exactly`)
731
+ }
732
+ }
733
+
215
734
  /** Holds parsed skill frontmatter and the exact scalar source used for bridge comparison. */
216
735
  export interface SkillFrontmatter {
217
736
  readonly keys: readonly string[]
@@ -223,6 +742,15 @@ export interface SkillFrontmatter {
223
742
  }
224
743
  }
225
744
 
745
+ /** Holds the keys and fields read so far from one frontmatter and the last line a folded block consumed. */
746
+ export interface SkillFrontmatterScan {
747
+ readonly keys: readonly string[]
748
+ readonly skipThrough: number
749
+ readonly fields: ReadonlyArray<
750
+ readonly [string, { readonly value: string; readonly source: string }]
751
+ >
752
+ }
753
+
226
754
  /** Names the directory whose immediate child directories form the complete skill family. */
227
755
  export const SKILL_FAMILY_ROOT = '.agents/skills'
228
756
 
@@ -436,6 +964,51 @@ export function isPolicyRecord(value: unknown): value is Readonly<Record<string,
436
964
  return typeof value === 'object' && value !== null && !Array.isArray(value)
437
965
  }
438
966
 
967
+ /**
968
+ * Parses JSON text without throwing.
969
+ *
970
+ * @param content - The raw JSON text.
971
+ * @returns The parsed value, or `undefined` when the text is not JSON.
972
+ */
973
+ export function parsePolicyJSON(content: string): unknown {
974
+ try {
975
+ return JSON.parse(content)
976
+ } catch {
977
+ return undefined
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Reads one file and parses its JSON without throwing.
983
+ *
984
+ * @remarks
985
+ * One boundary holds the read and the parse, so a file the host refuses to open answers as a file
986
+ * that holds no JSON.
987
+ *
988
+ * @param path - The absolute path of the file to read.
989
+ * @returns The parsed value, or `undefined` when the file cannot be read or holds no JSON.
990
+ */
991
+ export function readPolicyJSON(path: string): unknown {
992
+ try {
993
+ return JSON.parse(readFileSync(path, 'utf8'))
994
+ } catch {
995
+ return undefined
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Extracts the lines beneath one heading, up to the next second-level heading.
1001
+ *
1002
+ * @param lines - The document lines.
1003
+ * @param heading - The index of the heading line whose section is read.
1004
+ * @returns Each line after the heading and before the next line starting with `## `, in order.
1005
+ */
1006
+ export function extractPolicySection(lines: readonly string[], heading: number): readonly string[] {
1007
+ const following = lines.slice(heading + 1)
1008
+ const end = following.findIndex((line) => line.startsWith('## '))
1009
+ return end === -1 ? following : following.slice(0, end)
1010
+ }
1011
+
439
1012
  /**
440
1013
  * Creates one stable violation for an inspection result.
441
1014
  *
@@ -477,12 +1050,11 @@ export function stemToPolicyCandidates(stem: string): readonly string[] {
477
1050
  const normalized = normalizePolicyPath(stem)
478
1051
  const directory = dirname(normalized).replaceAll('\\', '/')
479
1052
  const name = basename(normalized)
480
- const candidates = POLICY_MODULE_EXTENSIONS.map((extension) => `${normalized}.${extension}`)
481
- for (const extension of POLICY_PARTIAL_EXTENSIONS) {
482
- candidates.push(`${directory}/_${name}.${extension}`)
483
- }
484
- if (name.startsWith(POLICY_TESTS_MODULE_PREFIX)) candidates.push(`tests/${normalized}.ts`)
485
- return candidates
1053
+ return [
1054
+ ...POLICY_MODULE_EXTENSIONS.map((extension) => `${normalized}.${extension}`),
1055
+ ...POLICY_PARTIAL_EXTENSIONS.map((extension) => `${directory}/_${name}.${extension}`),
1056
+ ...(name.startsWith(POLICY_TESTS_MODULE_PREFIX) ? [`tests/${normalized}.ts`] : []),
1057
+ ]
486
1058
  }
487
1059
 
488
1060
  /**
@@ -496,23 +1068,25 @@ export function inspectPolicyMirrorPaths(
496
1068
  tests: readonly string[],
497
1069
  modules: ReadonlySet<string>,
498
1070
  ): readonly PolicyViolation[] {
499
- const violations: PolicyViolation[] = []
500
- for (const test of tests) {
501
- const path = normalizePolicyPath(test)
502
- const stem = testToPolicyStem(path)
503
- if (stem === undefined) continue
504
- const candidates = stemToPolicyCandidates(stem)
505
- if (!candidates.some((candidate) => modules.has(candidate))) {
506
- violations.push(
507
- createPolicyViolation(
508
- 'mirror',
509
- path,
510
- `module test requires one matching module: ${candidates.join(', ')}`,
511
- ),
512
- )
513
- }
514
- }
515
- return violations
1071
+ // The mapper runs on each entry as the iterator yields it, so a hole reaches normalization as
1072
+ // `undefined` and no entry after a failing one is read, as in the loop this replaced.
1073
+ return Iterator.from(tests)
1074
+ .flatMap((test) => {
1075
+ const path = normalizePolicyPath(test)
1076
+ const stem = testToPolicyStem(path)
1077
+ if (stem === undefined) return []
1078
+ const candidates = stemToPolicyCandidates(stem)
1079
+ return candidates.some((candidate) => modules.has(candidate))
1080
+ ? []
1081
+ : [
1082
+ createPolicyViolation(
1083
+ 'mirror',
1084
+ path,
1085
+ `module test requires one matching module: ${candidates.join(', ')}`,
1086
+ ),
1087
+ ]
1088
+ })
1089
+ .toArray()
516
1090
  }
517
1091
 
518
1092
  /**
@@ -541,15 +1115,15 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
541
1115
  const shared = paths.has('tests/setup.test.ts')
542
1116
  ? readFileSync(join(root, 'tests/setup.test.ts'), 'utf8')
543
1117
  : ''
544
- const imports = new Set<string>()
545
- for (const declaration of parseAst(shared, { lang: 'ts' }).body) {
546
- if (declaration.type !== 'ImportDeclaration') continue
547
- const specifier = declaration.source.value
548
- if (/^\.\/setup[^/]*\.(?:js|ts)$/u.test(specifier))
549
- imports.add('tests/' + specifier.slice(2).replace(/\.js$/u, '.ts'))
550
- }
551
- const violations: PolicyViolation[] = []
552
- for (const path of [...paths].sort()) {
1118
+ const imports = new Set(
1119
+ parseAst(shared, { lang: 'ts' }).body.flatMap((declaration) =>
1120
+ declaration.type === 'ImportDeclaration' &&
1121
+ /^\.\/setup[^/]*\.(?:js|ts)$/u.test(declaration.source.value)
1122
+ ? ['tests/' + declaration.source.value.slice(2).replace(/\.js$/u, '.ts')]
1123
+ : [],
1124
+ ),
1125
+ )
1126
+ return [...paths].sort().flatMap((path) => {
553
1127
  const module = path.endsWith('.test.ts') ? path.replace(/\.test\.ts$/u, '.ts') : path
554
1128
  if (
555
1129
  HOST_PATHS.some(
@@ -558,27 +1132,24 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
558
1132
  module.startsWith(normalizePolicyPath(vendored) + '/'),
559
1133
  )
560
1134
  )
561
- continue
1135
+ return []
562
1136
  if (path.endsWith('.test.ts')) {
563
- if (!paths.has(module))
564
- violations.push(
565
- createPolicyViolation('mirror', path, `setup proof requires its module: ${module}`),
566
- )
567
- } else if (
568
- /^export /m.test(readFileSync(join(root, path), 'utf8')) &&
1137
+ return paths.has(module)
1138
+ ? []
1139
+ : [createPolicyViolation('mirror', path, `setup proof requires its module: ${module}`)]
1140
+ }
1141
+ return /^export /m.test(readFileSync(join(root, path), 'utf8')) &&
569
1142
  !paths.has(path.replace(/\.ts$/u, '.test.ts')) &&
570
1143
  !imports.has(path)
571
- ) {
572
- violations.push(
573
- createPolicyViolation(
574
- 'mirror',
575
- path,
576
- 'exporting setup module requires its sibling proof or an import from tests/setup.test.ts',
577
- ),
578
- )
579
- }
580
- }
581
- return violations
1144
+ ? [
1145
+ createPolicyViolation(
1146
+ 'mirror',
1147
+ path,
1148
+ 'exporting setup module requires its sibling proof or an import from tests/setup.test.ts',
1149
+ ),
1150
+ ]
1151
+ : []
1152
+ })
582
1153
  }
583
1154
 
584
1155
  /**
@@ -588,25 +1159,23 @@ export function inspectPolicySetup(root: string): readonly PolicyViolation[] {
588
1159
  * @returns Every suppression occurrence in path and line order.
589
1160
  */
590
1161
  export function inspectPolicySuppressions(root: string): readonly PolicyViolation[] {
591
- const violations: PolicyViolation[] = []
592
1162
  const paths = globSync(POLICY_SUPPRESSION_GLOB, { cwd: root }).map(normalizePolicyPath).sort()
593
- for (const path of paths) {
594
- const lines = readFileSync(join(root, path), 'utf8').split('\n')
595
- for (let index = 0; index < lines.length; index += 1) {
596
- const line = lines[index]
597
- if (line !== undefined && POLICY_SUPPRESSION_PATTERN.test(line)) {
598
- violations.push(
599
- createPolicyViolation(
600
- 'suppression',
601
- path,
602
- 'file carries a lint or formatter suppression directive',
603
- index + 1,
604
- ),
605
- )
606
- }
607
- }
608
- }
609
- return violations
1163
+ return paths.flatMap((path) =>
1164
+ readFileSync(join(root, path), 'utf8')
1165
+ .split('\n')
1166
+ .flatMap((line, index) =>
1167
+ POLICY_SUPPRESSION_PATTERN.test(line)
1168
+ ? [
1169
+ createPolicyViolation(
1170
+ 'suppression',
1171
+ path,
1172
+ 'file carries a lint or formatter suppression directive',
1173
+ index + 1,
1174
+ ),
1175
+ ]
1176
+ : [],
1177
+ ),
1178
+ )
610
1179
  }
611
1180
 
612
1181
  /**
@@ -616,63 +1185,59 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
616
1185
  * @returns Every wiring violation in rule and configuration order.
617
1186
  */
618
1187
  export function inspectPolicyConfiguration(configuration: unknown): readonly string[] {
619
- const violations: string[] = []
620
1188
  if (!isPolicyRecord(configuration)) {
621
1189
  return ['Oxlint configuration must be a record']
622
1190
  }
623
1191
 
624
1192
  const rules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
625
- for (const rule of POLICY_WIRING_RULES) {
626
- const setting = isPolicyRecord(rules)
627
- ? Object.getOwnPropertyDescriptor(rules, rule)?.value
628
- : undefined
629
- const severity = Array.isArray(setting) ? setting[0] : setting
630
- if (severity !== 'error') violations.push(`${rule} must have top-level error severity`)
631
- }
632
-
633
1193
  const ignorePatterns: unknown = Object.getOwnPropertyDescriptor(
634
1194
  configuration,
635
1195
  'ignorePatterns',
636
1196
  )?.value
637
- if (ignorePatterns !== undefined && !Array.isArray(ignorePatterns)) {
638
- violations.push('ignorePatterns must be an array when declared')
639
- } else if (Array.isArray(ignorePatterns)) {
640
- for (const pattern of ignorePatterns) {
641
- if (typeof pattern !== 'string' || pattern.startsWith('!')) continue
642
- const normalized = normalizePolicyPath(pattern).replace(/^\.\//u, '').replace(/^\//u, '')
643
- const [first = ''] = normalized.split('/')
644
- if (
645
- POLICY_WIRING_ROOTS.some(
646
- (root) => first === root || (first !== '' && matchesGlob(root, first)),
647
- )
648
- ) {
649
- violations.push(`ignorePatterns must not reach ${pattern}`)
650
- }
651
- }
652
- }
653
-
654
1197
  const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
655
- if (overrides !== undefined && !Array.isArray(overrides)) {
656
- violations.push('overrides must be an array when declared')
657
- } else if (Array.isArray(overrides)) {
658
- for (const override of overrides) {
659
- if (!isPolicyRecord(override)) {
660
- violations.push('override entries must be records')
661
- continue
662
- }
663
- const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
664
- if (!isPolicyRecord(overrideRules)) {
665
- continue
666
- }
667
- for (const rule of POLICY_WIRING_RULES) {
668
- if (Object.getOwnPropertyDescriptor(overrideRules, rule) !== undefined) {
669
- violations.push(`overrides must not configure ${rule}`)
670
- }
671
- }
672
- }
673
- }
674
-
675
- return violations
1198
+ return [
1199
+ ...POLICY_WIRING_RULES.flatMap((rule) => {
1200
+ const setting = isPolicyRecord(rules)
1201
+ ? Object.getOwnPropertyDescriptor(rules, rule)?.value
1202
+ : undefined
1203
+ const severity = Array.isArray(setting) ? setting[0] : setting
1204
+ return severity === 'error' ? [] : [`${rule} must have top-level error severity`]
1205
+ }),
1206
+ ...(ignorePatterns !== undefined && !Array.isArray(ignorePatterns)
1207
+ ? ['ignorePatterns must be an array when declared']
1208
+ : // The mapper runs on each pattern as the iterator yields it, as the loop it replaced did.
1209
+ Iterator.from(Array.isArray(ignorePatterns) ? ignorePatterns : [])
1210
+ .flatMap((pattern) => {
1211
+ if (typeof pattern !== 'string' || pattern.startsWith('!')) return []
1212
+ const normalized = normalizePolicyPath(pattern)
1213
+ .replace(/^\.\//u, '')
1214
+ .replace(/^\//u, '')
1215
+ const [first = ''] = normalized.split('/')
1216
+ return POLICY_WIRING_ROOTS.some(
1217
+ (root) => first === root || (first !== '' && matchesGlob(root, first)),
1218
+ )
1219
+ ? [`ignorePatterns must not reach ${pattern}`]
1220
+ : []
1221
+ })
1222
+ .toArray()),
1223
+ ...(overrides !== undefined && !Array.isArray(overrides)
1224
+ ? ['overrides must be an array when declared']
1225
+ : // The mapper runs on each entry as the iterator yields it, so a hole is a malformed entry as it
1226
+ // was in the loop, and no entry after a failing one is read.
1227
+ Iterator.from(Array.isArray(overrides) ? overrides : [])
1228
+ .flatMap((override) => {
1229
+ if (!isPolicyRecord(override)) return ['override entries must be records']
1230
+ const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
1231
+ return isPolicyRecord(overrideRules)
1232
+ ? POLICY_WIRING_RULES.flatMap((rule) =>
1233
+ Object.getOwnPropertyDescriptor(overrideRules, rule) === undefined
1234
+ ? []
1235
+ : [`overrides must not configure ${rule}`],
1236
+ )
1237
+ : []
1238
+ })
1239
+ .toArray()),
1240
+ ]
676
1241
  }
677
1242
 
678
1243
  /**
@@ -692,36 +1257,39 @@ export function inspectPolicyWiring(
692
1257
  return ['Oxlint configuration must be a record']
693
1258
  }
694
1259
 
695
- const records: unknown[] = [Object.getOwnPropertyDescriptor(configuration, 'rules')?.value]
696
- const declaredPopulations: string[] = []
1260
+ const topRules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
697
1261
  const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
698
- if (Array.isArray(overrides)) {
699
- for (const entry of overrides) {
700
- if (!isPolicyRecord(entry)) continue
701
- records.push(Object.getOwnPropertyDescriptor(entry, 'rules')?.value)
1262
+ // Each mapper runs on an entry as the iterator yields it, so a hole reaches the guard as
1263
+ // `undefined` and no entry after a failing one is read, as in the loops these replaced.
1264
+ const entries = Iterator.from(Array.isArray(overrides) ? overrides : [])
1265
+ .flatMap((entry) => {
1266
+ if (!isPolicyRecord(entry)) return []
1267
+ const record: unknown = Object.getOwnPropertyDescriptor(entry, 'rules')?.value
702
1268
  const files: unknown = Object.getOwnPropertyDescriptor(entry, 'files')?.value
703
- if (Array.isArray(files)) declaredPopulations.push(files.join(' '))
704
- }
705
- }
706
-
707
- const enabled = new Set<string>()
708
- for (const record of records) {
709
- if (!isPolicyRecord(record)) continue
710
- for (const name of Object.getOwnPropertyNames(record)) enabled.add(name)
711
- }
712
-
713
- const violations: string[] = []
714
- for (const rule of rules) {
715
- if (!enabled.has(rule))
716
- violations.push(`${rule} is enabled by no top-level or override rules record`)
717
- }
718
- for (const population of populations) {
719
- const joined = population.join(' ')
720
- if (!declaredPopulations.includes(joined)) {
721
- violations.push(`no override files list declares the population exactly: ${joined}`)
722
- }
723
- }
724
- return violations
1269
+ return [{ record, declared: Array.isArray(files) ? [files.join(' ')] : [] }]
1270
+ })
1271
+ .toArray()
1272
+ const enabled = new Set(
1273
+ [topRules, ...entries.map(({ record }) => record)].flatMap((record) =>
1274
+ isPolicyRecord(record) ? Object.getOwnPropertyNames(record) : [],
1275
+ ),
1276
+ )
1277
+ const declaredPopulations = entries.flatMap(({ declared }) => declared)
1278
+ return [
1279
+ ...Iterator.from(rules)
1280
+ .flatMap((rule) =>
1281
+ enabled.has(rule) ? [] : [`${rule} is enabled by no top-level or override rules record`],
1282
+ )
1283
+ .toArray(),
1284
+ ...Iterator.from(populations)
1285
+ .flatMap((population) => {
1286
+ const joined = population.join(' ')
1287
+ return declaredPopulations.includes(joined)
1288
+ ? []
1289
+ : [`no override files list declares the population exactly: ${joined}`]
1290
+ })
1291
+ .toArray(),
1292
+ ]
725
1293
  }
726
1294
 
727
1295
  /**
@@ -734,15 +1302,13 @@ export function inspectPolicyWiring(
734
1302
  export function resolvePolicyDirectory(root: string, path: string): string | undefined {
735
1303
  const normalized = normalizePolicyPath(path)
736
1304
  if (normalized === '' || normalized === '.') return root
737
- let current = root
738
- for (const segment of normalized.split('/')) {
1305
+ return normalized.split('/').reduce<string | undefined>((current, segment) => {
1306
+ if (current === undefined) return undefined
739
1307
  const entry = readdirSync(current, { withFileTypes: true }).find(
740
1308
  (candidate) => candidate.name === segment && candidate.isDirectory(),
741
1309
  )
742
- if (entry === undefined) return undefined
743
- current = join(current, entry.name)
744
- }
745
- return current
1310
+ return entry === undefined ? undefined : join(current, entry.name)
1311
+ }, root)
746
1312
  }
747
1313
 
748
1314
  /**
@@ -800,76 +1366,68 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | undef
800
1366
  if (lines[0] !== '---') return undefined
801
1367
  const boundary = lines.indexOf('---', 1)
802
1368
  if (boundary === -1) return undefined
803
- const keys: string[] = []
804
- let name: string | undefined
805
- let description: string | undefined
806
- let nameSource: string | undefined
807
- let descriptionSource: string | undefined
808
-
809
- for (let index = 1; index < boundary; index += 1) {
810
- const line = lines[index]
811
- if (line === undefined) return undefined
812
- const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(.*)$/u)
813
- const key = match?.[1]
814
- const scalar = match?.[2]
815
- if (key === undefined || scalar === undefined) return undefined
816
- if (scalar !== '' && !scalar.startsWith(' ')) return undefined
817
- keys.push(key)
818
- let value = scalar === '' ? '' : scalar.slice(1)
819
- let source = rawLines[index]?.slice(line.indexOf(':') + 1)
820
- if (source === undefined) return undefined
821
- if (value === '>-') {
822
- if (key !== 'description') return undefined
823
- const folded: string[] = []
824
- const sourceLines: string[] = [source]
825
- for (index += 1; index < boundary; index += 1) {
826
- const continuation = lines[index]
827
- if (continuation === undefined) return undefined
828
- if (continuation.trim() === '') {
829
- folded.push('')
830
- const rawContinuation = rawLines[index]
831
- if (rawContinuation === undefined) return undefined
832
- sourceLines.push(rawContinuation)
833
- continue
834
- }
835
- if (!continuation.startsWith(' ')) {
836
- index -= 1
837
- break
838
- }
839
- folded.push(continuation.slice(2))
840
- const rawContinuation = rawLines[index]
841
- if (rawContinuation === undefined) return undefined
842
- sourceLines.push(rawContinuation)
843
- }
844
- value = ''
845
- let blanks = 0
846
- for (const foldedLine of folded) {
847
- if (foldedLine === '') {
848
- blanks += 1
849
- continue
850
- }
851
- if (value !== '') value += blanks === 0 ? ' ' : '\n'.repeat(blanks)
852
- value += foldedLine
853
- blanks = 0
1369
+ // A folded description consumes the lines after its key, so `skipThrough` carries the last of them.
1370
+ const scan = lines.slice(0, boundary).reduce<SkillFrontmatterScan | undefined>(
1371
+ (state, line, index) => {
1372
+ if (state === undefined || index === 0 || index <= state.skipThrough) return state
1373
+ const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(.*)$/u)
1374
+ const key = match?.[1]
1375
+ const scalar = match?.[2]
1376
+ if (key === undefined || scalar === undefined) return undefined
1377
+ if (scalar !== '' && !scalar.startsWith(' ')) return undefined
1378
+ const initial = scalar === '' ? '' : scalar.slice(1)
1379
+ const own = rawLines[index]?.slice(line.indexOf(':') + 1)
1380
+ if (own === undefined) return undefined
1381
+ if (initial === '>-' && key !== 'description') return undefined
1382
+ if (
1383
+ initial !== '>-' &&
1384
+ key === 'description' &&
1385
+ (/^['"]/u.test(initial) || /^[>|][+-]?$/u.test(initial))
1386
+ )
1387
+ return undefined
1388
+ const rest = initial === '>-' ? lines.slice(index + 1, boundary) : []
1389
+ const ended = rest.findIndex(
1390
+ (continuation) => continuation.trim() !== '' && !continuation.startsWith(' '),
1391
+ )
1392
+ const span = ended === -1 ? rest.length : ended
1393
+ const block = rest.slice(0, span)
1394
+ const rawBlock = rawLines.slice(index + 1, index + 1 + span)
1395
+ if (rawBlock.length !== span) return undefined
1396
+ const folded = block.map((continuation) =>
1397
+ continuation.trim() === '' ? '' : continuation.slice(2),
1398
+ )
1399
+ // Each kept line joins to the one before it by a space, or by one newline per blank line between.
1400
+ const kept = folded.flatMap((continuation, position) =>
1401
+ continuation === '' ? [] : [{ continuation, position }],
1402
+ )
1403
+ const value =
1404
+ initial === '>-'
1405
+ ? kept
1406
+ .map((entry, rank) => {
1407
+ const before = kept[rank - 1]
1408
+ if (before === undefined) return entry.continuation
1409
+ const blanks = entry.position - before.position - 1
1410
+ return `${blanks === 0 ? ' ' : '\n'.repeat(blanks)}${entry.continuation}`
1411
+ })
1412
+ .join('')
1413
+ : initial
1414
+ const source = initial === '>-' ? [own, ...rawBlock].join('\n') : own
1415
+ return {
1416
+ keys: [...state.keys, key],
1417
+ skipThrough: index + span,
1418
+ fields: [...state.fields, [key, { value, source }]],
854
1419
  }
855
- source = sourceLines.join('\n')
856
- } else if (key === 'description' && (/^['"]/u.test(value) || /^[>|][+-]?$/u.test(value))) {
857
- return undefined
858
- }
859
- if (key === 'name') {
860
- name = value
861
- nameSource = source
862
- } else if (key === 'description') {
863
- description = value
864
- descriptionSource = source
865
- }
866
- }
1420
+ },
1421
+ { keys: [], skipThrough: 0, fields: [] },
1422
+ )
1423
+ if (scan === undefined) return undefined
1424
+ const fields = new Map(scan.fields)
867
1425
 
868
1426
  return {
869
- keys,
870
- name,
871
- description,
872
- source: { name: nameSource, description: descriptionSource },
1427
+ keys: scan.keys,
1428
+ name: fields.get('name')?.value,
1429
+ description: fields.get('description')?.value,
1430
+ source: { name: fields.get('name')?.source, description: fields.get('description')?.source },
873
1431
  }
874
1432
  }
875
1433
 
@@ -928,16 +1486,16 @@ export function readSkillScripts(root: string, name: string): readonly string[]
928
1486
  * @returns The distinct script paths, relative to the skill directory, in first-mention order.
929
1487
  */
930
1488
  export function extractSkillScripts(content: string, name: string): readonly string[] {
931
- const found: string[] = []
932
- for (const match of content.matchAll(
933
- /(?:\.agents\/skills\/([A-Za-z0-9-]+)\/)?(scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts)/gu,
934
- )) {
1489
+ const named = [
1490
+ ...content.matchAll(
1491
+ /(?:\.agents\/skills\/([A-Za-z0-9-]+)\/)?(scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts)/gu,
1492
+ ),
1493
+ ].flatMap((match) => {
935
1494
  const owner = match[1]
936
1495
  const script = match[2]
937
- if (script === undefined || (owner !== undefined && owner !== name)) continue
938
- if (!found.includes(script)) found.push(script)
939
- }
940
- return found
1496
+ return script === undefined || (owner !== undefined && owner !== name) ? [] : [script]
1497
+ })
1498
+ return named.filter((script, index) => named.indexOf(script) === index)
941
1499
  }
942
1500
 
943
1501
  /**
@@ -948,14 +1506,14 @@ export function extractSkillScripts(content: string, name: string): readonly str
948
1506
  * @returns The distinct `.agents/skills/<other>/scripts/*.ts` paths, in first-mention order.
949
1507
  */
950
1508
  export function extractForeignSkillScripts(content: string, name: string): readonly string[] {
951
- const found: string[] = []
952
- for (const match of content.matchAll(
953
- /\.agents\/skills\/([A-Za-z0-9-]+)\/scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts/gu,
954
- )) {
955
- if (match[1] === name || found.includes(match[0])) continue
956
- found.push(match[0])
957
- }
958
- return found
1509
+ const named = [
1510
+ ...content.matchAll(
1511
+ /\.agents\/skills\/([A-Za-z0-9-]+)\/scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts/gu,
1512
+ ),
1513
+ ]
1514
+ .filter((match) => match[1] !== name)
1515
+ .map((match) => match[0])
1516
+ return named.filter((script, index) => named.indexOf(script) === index)
959
1517
  }
960
1518
 
961
1519
  /**
@@ -1008,11 +1566,14 @@ export function parseSkillPrompt(content: string): string | undefined {
1008
1566
  */
1009
1567
  export function matchesSkillToken(prompt: string, name: string): boolean {
1010
1568
  const token = `$${name}`
1011
- for (let index = prompt.indexOf(token); index !== -1; index = prompt.indexOf(token, index + 1)) {
1012
- const next = prompt.charAt(index + token.length)
1013
- if (next === '' || !/[a-z-]/u.test(next)) return true
1014
- }
1015
- return false
1569
+ // The token opens with `$`, so each `$` in the prompt is a start to try, and a name that carries
1570
+ // `$` makes the token overlap itself.
1571
+ return prompt
1572
+ .matchAll(/\$/gu)
1573
+ .some(
1574
+ ({ index }) =>
1575
+ prompt.startsWith(token, index) && !/[a-z-]/u.test(prompt.charAt(index + token.length)),
1576
+ )
1016
1577
  }
1017
1578
 
1018
1579
  /**
@@ -1022,12 +1583,12 @@ export function matchesSkillToken(prompt: string, name: string): boolean {
1022
1583
  * @returns Each distinct references/name.md token in sorted order.
1023
1584
  */
1024
1585
  export function extractSkillReferences(content: string): readonly string[] {
1025
- const references = new Set<string>()
1026
1586
  // This raw-text scan includes fenced examples. Over-matching safely requires the named file.
1027
- for (const match of content.matchAll(/references\/[A-Za-z0-9._-]+\.md/gu)) {
1028
- const reference = match[0]
1029
- if (reference !== undefined) references.add(reference)
1030
- }
1587
+ const references = new Set(
1588
+ [...content.matchAll(/references\/[A-Za-z0-9._-]+\.md/gu)].flatMap((match) =>
1589
+ match[0] === undefined ? [] : [match[0]],
1590
+ ),
1591
+ )
1031
1592
  return [...references].sort()
1032
1593
  }
1033
1594
 
@@ -1052,41 +1613,32 @@ export function inspectSkillTemplateTODOs(
1052
1613
  path: string,
1053
1614
  content: string,
1054
1615
  ): readonly PolicyViolation[] {
1055
- const violations: PolicyViolation[] = []
1056
1616
  const lines = content.split(/\r\n|\r|\n/u)
1057
- let fence: string | undefined
1058
- for (let index = 0; index < lines.length; index += 1) {
1059
- const line = lines[index]
1060
- if (line === undefined) continue
1061
- const fenceLine = line.replace(/^ {0,3}/u, '')
1062
- if (fence !== undefined) {
1063
- if (fenceLine.startsWith(fence)) fence = undefined
1064
- continue
1065
- }
1066
- const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1067
- if (opening !== undefined) {
1068
- fence = opening
1069
- continue
1070
- }
1071
- let cursor = 0
1072
- while (cursor < line.length) {
1073
- const openingBacktick = line.indexOf('`', cursor)
1074
- const end = openingBacktick === -1 ? line.length : openingBacktick
1075
- for (
1076
- let todo = line.indexOf('TODO', cursor);
1077
- todo !== -1 && todo < end;
1078
- todo = line.indexOf('TODO', todo + 4)
1079
- ) {
1080
- violations.push(
1081
- createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
1082
- )
1617
+ // Each line the scan reads is one outside every fence; the fence open at a line rides the fold.
1618
+ const scanned = lines.reduce<{
1619
+ readonly fence: string | undefined
1620
+ readonly indexes: readonly number[]
1621
+ }>(
1622
+ (state, line, index) => {
1623
+ const fenceLine = line.replace(/^ {0,3}/u, '')
1624
+ if (state.fence !== undefined) {
1625
+ return fenceLine.startsWith(state.fence)
1626
+ ? { fence: undefined, indexes: state.indexes }
1627
+ : state
1083
1628
  }
1084
- if (openingBacktick === -1) break
1085
- const closingBacktick = line.indexOf('`', openingBacktick + 1)
1086
- cursor = closingBacktick === -1 ? openingBacktick + 1 : closingBacktick + 1
1087
- }
1088
- }
1089
- return violations
1629
+ const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1630
+ if (opening !== undefined) return { fence: opening, indexes: state.indexes }
1631
+ return { fence: undefined, indexes: [...state.indexes, index] }
1632
+ },
1633
+ { fence: undefined, indexes: [] },
1634
+ ).indexes
1635
+ return scanned.flatMap((index) => {
1636
+ // A matched pair of single backticks on one line is an inline span; a lone backtick is not.
1637
+ const outside = (lines[index] ?? '').replace(/`[^`]*`/gu, ' ')
1638
+ return Array.from({ length: outside.split('TODO').length - 1 }, () =>
1639
+ createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
1640
+ )
1641
+ })
1090
1642
  }
1091
1643
 
1092
1644
  /** Names why a declaration reading produced no inventory. */
@@ -1335,8 +1887,8 @@ export function readSkillDeclarations(
1335
1887
  const source = parseSync(path, readFileSync(path, 'utf8'))
1336
1888
  const [failure] = source.errors
1337
1889
  if (failure !== undefined) return { outcome: 'syntax', names: [], detail: failure.message }
1338
- const names = new Set<string>()
1339
- for (const statement of source.program.body) {
1890
+ // Each statement answers its own declared names or the refusal it raises; the first refusal wins.
1891
+ const declared = source.program.body.map((statement): SkillDeclarationResult => {
1340
1892
  if (statement.type === 'ExportDefaultDeclaration')
1341
1893
  return { outcome: 'form', names: [], detail: 'export default' }
1342
1894
  if (statement.type === 'TSExportAssignment')
@@ -1345,69 +1897,90 @@ export function readSkillDeclarations(
1345
1897
  return { outcome: 'form', names: [], detail: 'export as namespace' }
1346
1898
  if (statement.type === 'TSModuleDeclaration')
1347
1899
  return { outcome: 'form', names: [], detail: 'ambient module' }
1348
- if (statement.type !== 'ExportNamedDeclaration' || statement.source !== null) continue
1349
- for (const binding of statement.specifiers) {
1350
- const name =
1351
- binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value
1352
- if (name === 'default') return { outcome: 'form', names: [], detail: 'default export list' }
1353
- names.add(name)
1354
- }
1900
+ if (statement.type !== 'ExportNamedDeclaration' || statement.source !== null)
1901
+ return { outcome: 'read', names: [] }
1902
+ const listed = statement.specifiers.map((binding) =>
1903
+ binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value,
1904
+ )
1905
+ if (listed.includes('default'))
1906
+ return { outcome: 'form', names: [], detail: 'default export list' }
1355
1907
  const declaration = statement.declaration
1356
- if (declaration === null) continue
1908
+ if (declaration === null) return { outcome: 'read', names: listed }
1357
1909
  if (declaration.type === 'VariableDeclaration') {
1358
- for (const variable of declaration.declarations) {
1359
- if (variable.id.type !== 'Identifier')
1360
- return { outcome: 'form', names: [], detail: 'destructured declaration' }
1361
- names.add(variable.id.name)
1362
- }
1363
- } else if (declaration.type === 'TSModuleDeclaration') {
1910
+ const identifiers = declaration.declarations.flatMap((variable) =>
1911
+ variable.id.type === 'Identifier' ? [variable.id.name] : [],
1912
+ )
1913
+ return identifiers.length === declaration.declarations.length
1914
+ ? { outcome: 'read', names: [...listed, ...identifiers] }
1915
+ : { outcome: 'form', names: [], detail: 'destructured declaration' }
1916
+ }
1917
+ if (declaration.type === 'TSModuleDeclaration')
1364
1918
  return { outcome: 'form', names: [], detail: 'exported namespace' }
1365
- } else if (
1919
+ if (
1366
1920
  declaration.type === 'TSDeclareFunction' ||
1367
1921
  declaration.type === 'ClassDeclaration' ||
1368
1922
  declaration.type === 'TSEnumDeclaration' ||
1369
1923
  declaration.type === 'TSInterfaceDeclaration' ||
1370
1924
  declaration.type === 'TSTypeAliasDeclaration'
1371
1925
  ) {
1372
- if (declaration.id === null)
1373
- return { outcome: 'form', names: [], detail: 'anonymous declaration' }
1374
- names.add(declaration.id.name)
1375
- } else return { outcome: 'form', names: [], detail: 'unsupported exported declaration' }
1376
- }
1377
- if (ancestors.includes(path)) return { outcome: 'cycle', names: [...names] }
1378
- for (const statement of source.program.body) {
1379
- if (statement.type !== 'ExportAllDeclaration' && statement.type !== 'ExportNamedDeclaration')
1380
- continue
1381
- const origin = statement.source
1382
- if (origin === null) continue
1383
- if (statement.type === 'ExportAllDeclaration' && statement.exported !== null)
1384
- return { outcome: 'form', names: [], detail: 'star export alias' }
1385
- if (!origin.value.startsWith('./') && !origin.value.startsWith('../'))
1386
- return { outcome: 'form', names: [], detail: 'non-relative re-export' }
1387
- const declaration = /\.d\.(?:ts|mts|cts)$/u.test(origin.value)
1388
- ? origin.value
1389
- : origin.value.replace(/\.(js|mjs|cjs)$/u, (_, extension: string) =>
1390
- extension === 'mjs' ? '.d.mts' : extension === 'cjs' ? '.d.cts' : '.d.ts',
1391
- )
1392
- if (!/\.d\.(?:ts|mts|cts)$/u.test(declaration))
1393
- return { outcome: 'form', names: [], detail: 'untyped re-export target' }
1394
- const targets = readSkillDeclarations(resolve(dirname(path), declaration), [...ancestors, path])
1395
- if (targets.outcome !== 'read' && targets.outcome !== 'cycle') return targets
1396
- if (statement.type === 'ExportAllDeclaration') {
1397
- if (targets.outcome === 'read') for (const name of targets.names) names.add(name)
1398
- continue
1926
+ return declaration.id === null
1927
+ ? { outcome: 'form', names: [], detail: 'anonymous declaration' }
1928
+ : { outcome: 'read', names: [...listed, declaration.id.name] }
1399
1929
  }
1400
- for (const binding of statement.specifiers) {
1401
- const name =
1402
- binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value
1403
- const original =
1404
- binding.local.type === 'Identifier' ? binding.local.name : binding.local.value
1405
- if (name === 'default') return { outcome: 'form', names: [], detail: 'default re-export' }
1406
- if (!targets.names.includes(original)) return { outcome: 'name', names: [], detail: original }
1407
- names.add(name)
1408
- }
1409
- }
1410
- return { outcome: 'read', names: [...names].sort() }
1930
+ return { outcome: 'form', names: [], detail: 'unsupported exported declaration' }
1931
+ })
1932
+ const refused = declared.find((result) => result.outcome !== 'read')
1933
+ if (refused !== undefined) return refused
1934
+ const names = [...new Set(declared.flatMap((result) => result.names))]
1935
+ if (ancestors.includes(path)) return { outcome: 'cycle', names }
1936
+ // A refusal short-circuits the fold, so no later re-export is read.
1937
+ const reexported = source.program.body.reduce<SkillDeclarationResult>(
1938
+ (state, statement) => {
1939
+ if (state.outcome !== 'read') return state
1940
+ if (statement.type !== 'ExportAllDeclaration' && statement.type !== 'ExportNamedDeclaration')
1941
+ return state
1942
+ const origin = statement.source
1943
+ if (origin === null) return state
1944
+ if (statement.type === 'ExportAllDeclaration' && statement.exported !== null)
1945
+ return { outcome: 'form', names: [], detail: 'star export alias' }
1946
+ if (!origin.value.startsWith('./') && !origin.value.startsWith('../'))
1947
+ return { outcome: 'form', names: [], detail: 'non-relative re-export' }
1948
+ const declaration = /\.d\.(?:ts|mts|cts)$/u.test(origin.value)
1949
+ ? origin.value
1950
+ : origin.value.replace(/\.(js|mjs|cjs)$/u, (_, extension: string) =>
1951
+ extension === 'mjs' ? '.d.mts' : extension === 'cjs' ? '.d.cts' : '.d.ts',
1952
+ )
1953
+ if (!/\.d\.(?:ts|mts|cts)$/u.test(declaration))
1954
+ return { outcome: 'form', names: [], detail: 'untyped re-export target' }
1955
+ const targets = readSkillDeclarations(resolve(dirname(path), declaration), [
1956
+ ...ancestors,
1957
+ path,
1958
+ ])
1959
+ if (targets.outcome !== 'read' && targets.outcome !== 'cycle') return targets
1960
+ if (statement.type === 'ExportAllDeclaration')
1961
+ return {
1962
+ outcome: 'read',
1963
+ names: [...state.names, ...(targets.outcome === 'read' ? targets.names : [])],
1964
+ }
1965
+ const bindings = statement.specifiers.map((binding) => ({
1966
+ name:
1967
+ binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value,
1968
+ original: binding.local.type === 'Identifier' ? binding.local.name : binding.local.value,
1969
+ }))
1970
+ const rejected = bindings.find(
1971
+ ({ name, original }) => name === 'default' || !targets.names.includes(original),
1972
+ )
1973
+ if (rejected === undefined)
1974
+ return { outcome: 'read', names: [...state.names, ...bindings.map(({ name }) => name)] }
1975
+ return rejected.name === 'default'
1976
+ ? { outcome: 'form', names: [], detail: 'default re-export' }
1977
+ : { outcome: 'name', names: [], detail: rejected.original }
1978
+ },
1979
+ { outcome: 'read', names },
1980
+ )
1981
+ return reexported.outcome === 'read'
1982
+ ? { outcome: 'read', names: [...new Set(reexported.names)].sort() }
1983
+ : reexported
1411
1984
  }
1412
1985
 
1413
1986
  /**
@@ -1434,63 +2007,84 @@ export function inspectSkillImports(
1434
2007
  path: string,
1435
2008
  content: string,
1436
2009
  ): readonly PolicyViolation[] {
1437
- const violations: PolicyViolation[] = []
1438
- const entries = new Map<string, SkillDeclarationResult>()
1439
- for (const fence of createGuide(content).fences()) {
1440
- const source = parseSync('skill.ts', fence.code)
1441
- const [failure] = source.errors
1442
- if (failure !== undefined && fence.code.includes('@orkestrel/')) {
1443
- violations.push(
2010
+ const fences = createGuide(content)
2011
+ .fences()
2012
+ .map((fence) => {
2013
+ const source = parseSync('skill.ts', fence.code)
2014
+ const [failure] = source.errors
2015
+ const refused = failure !== undefined && fence.code.includes('@orkestrel/')
2016
+ return {
2017
+ failure: refused ? failure : undefined,
2018
+ imports: refused
2019
+ ? []
2020
+ : source.program.body.flatMap((statement) => {
2021
+ if (statement.type !== 'ImportDeclaration') return []
2022
+ const specifier = statement.source.value
2023
+ const bindings = statement.specifiers.filter(
2024
+ (binding) => binding.type === 'ImportSpecifier',
2025
+ )
2026
+ return specifier.startsWith('@orkestrel/') && bindings.length > 0
2027
+ ? [{ specifier, bindings }]
2028
+ : []
2029
+ }),
2030
+ }
2031
+ })
2032
+ const specifiers = [
2033
+ ...new Set(
2034
+ fences
2035
+ .flatMap(({ imports }) => imports.map(({ specifier }) => specifier))
2036
+ .filter((specifier) =>
2037
+ Object.hasOwn(BASE_DEV_DEPENDENCIES, specifier.split('/').slice(0, 2).join('/')),
2038
+ ),
2039
+ ),
2040
+ ]
2041
+ // Each distinct supported specifier is read once, in first-seen order.
2042
+ const entries = new Map(
2043
+ specifiers.map((specifier) => [specifier, readSkillExports(root, specifier)]),
2044
+ )
2045
+ return fences.flatMap(({ failure, imports }) => {
2046
+ if (failure !== undefined) {
2047
+ return [
1444
2048
  createPolicyViolation('skill', path, `skill fence could not be parsed: ${failure.message}`),
1445
- )
1446
- continue
2049
+ ]
1447
2050
  }
1448
- for (const statement of source.program.body) {
1449
- if (statement.type !== 'ImportDeclaration') continue
1450
- const specifier = statement.source.value
1451
- const bindings = statement.specifiers.filter((binding) => binding.type === 'ImportSpecifier')
1452
- if (!specifier.startsWith('@orkestrel/') || bindings.length === 0) continue
2051
+ return imports.flatMap(({ specifier, bindings }) => {
1453
2052
  const packageName = specifier.split('/').slice(0, 2).join('/')
1454
2053
  if (!Object.hasOwn(BASE_DEV_DEPENDENCIES, packageName)) {
1455
- violations.push(
2054
+ return [
1456
2055
  createPolicyViolation(
1457
2056
  'skill',
1458
2057
  path,
1459
2058
  `skill fence import ${specifier} is outside BASE_DEV_DEPENDENCIES: ${packageName}`,
1460
2059
  ),
1461
- )
1462
- continue
2060
+ ]
1463
2061
  }
1464
- const cached = entries.get(specifier)
1465
- const entry = cached ?? readSkillExports(root, specifier)
1466
- if (cached === undefined) entries.set(specifier, entry)
2062
+ const entry = entries.get(specifier) ?? readSkillExports(root, specifier)
1467
2063
  if (entry.outcome !== 'read' && entry.outcome !== 'cycle') {
1468
2064
  const phrase = SKILL_DECLARATION_MESSAGES[entry.outcome]
1469
- violations.push(
2065
+ return [
1470
2066
  createPolicyViolation(
1471
2067
  'skill',
1472
2068
  path,
1473
2069
  `skill fence import ${specifier} ${entry.detail === undefined ? phrase : `${phrase} ${entry.detail}`}`,
1474
2070
  ),
1475
- )
1476
- continue
2071
+ ]
1477
2072
  }
1478
- for (const binding of bindings) {
2073
+ return bindings.flatMap((binding) => {
1479
2074
  const name =
1480
2075
  binding.imported.type === 'Identifier' ? binding.imported.name : binding.imported.value
1481
- if (!entry.names.includes(name)) {
1482
- violations.push(
1483
- createPolicyViolation(
1484
- 'skill',
1485
- path,
1486
- `skill fence import ${specifier} does not export ${name}`,
1487
- ),
1488
- )
1489
- }
1490
- }
1491
- }
1492
- }
1493
- return violations
2076
+ return entry.names.includes(name)
2077
+ ? []
2078
+ : [
2079
+ createPolicyViolation(
2080
+ 'skill',
2081
+ path,
2082
+ `skill fence import ${specifier} does not export ${name}`,
2083
+ ),
2084
+ ]
2085
+ })
2086
+ })
2087
+ })
1494
2088
  }
1495
2089
 
1496
2090
  /**
@@ -1509,228 +2103,231 @@ export function inspectSkill(
1509
2103
  const base = `${SKILL_FAMILY_ROOT}/${name}`
1510
2104
  const skill = `${base}/SKILL.md`
1511
2105
  const metadata = `${base}/agents/openai.yaml`
1512
- const violations: PolicyViolation[] = []
1513
- let content: string | undefined
1514
2106
  const hasSkill = isPolicyFile(root, skill)
1515
- if (!hasSkill) {
1516
- violations.push(
1517
- createPolicyViolation('skill', skill, 'skill requires an exact-case regular SKILL.md'),
1518
- )
1519
- } else {
1520
- content = readFileSync(join(root, skill), 'utf8')
1521
- const frontmatter = parseSkillFrontmatter(content)
1522
- if (frontmatter === undefined) {
1523
- violations.push(
1524
- createPolicyViolation('skill', skill, 'SKILL.md frontmatter exists and parses'),
1525
- )
1526
- } else {
1527
- const keys = new Set(frontmatter.keys)
1528
- if (
1529
- frontmatter.keys.length !== 2 ||
1530
- keys.size !== 2 ||
1531
- !keys.has('name') ||
1532
- !keys.has('description')
1533
- ) {
1534
- violations.push(
1535
- createPolicyViolation(
1536
- 'skill',
1537
- skill,
1538
- 'SKILL.md frontmatter contains exactly name and description',
1539
- ),
1540
- )
1541
- }
1542
- if (frontmatter.name !== name) {
1543
- violations.push(
1544
- createPolicyViolation('skill', skill, 'SKILL.md frontmatter name matches its directory'),
1545
- )
1546
- }
1547
- if (frontmatter.description === undefined || frontmatter.description.trim() === '') {
1548
- violations.push(createPolicyViolation('skill', skill, 'SKILL.md description is non-empty'))
1549
- } else if (!matchesSkillTrigger(frontmatter.description)) {
1550
- violations.push(
1551
- createPolicyViolation(
1552
- 'skill',
1553
- skill,
1554
- 'SKILL.md description names when to use the skill in a sentence beginning Use',
1555
- ),
1556
- )
1557
- }
1558
- }
1559
- violations.push(...inspectSkillTemplateTODOs('skill', skill, content))
1560
- violations.push(...inspectSkillImports(installation, skill, content))
1561
- }
2107
+ const content = hasSkill ? readFileSync(join(root, skill), 'utf8') : undefined
2108
+ const frontmatter = content === undefined ? undefined : parseSkillFrontmatter(content)
2109
+ const keys = new Set(frontmatter?.keys)
2110
+ const documented =
2111
+ content === undefined
2112
+ ? [createPolicyViolation('skill', skill, 'skill requires an exact-case regular SKILL.md')]
2113
+ : [
2114
+ ...(frontmatter === undefined
2115
+ ? [createPolicyViolation('skill', skill, 'SKILL.md frontmatter exists and parses')]
2116
+ : [
2117
+ ...(frontmatter.keys.length !== 2 ||
2118
+ keys.size !== 2 ||
2119
+ !keys.has('name') ||
2120
+ !keys.has('description')
2121
+ ? [
2122
+ createPolicyViolation(
2123
+ 'skill',
2124
+ skill,
2125
+ 'SKILL.md frontmatter contains exactly name and description',
2126
+ ),
2127
+ ]
2128
+ : []),
2129
+ ...(frontmatter.name === name
2130
+ ? []
2131
+ : [
2132
+ createPolicyViolation(
2133
+ 'skill',
2134
+ skill,
2135
+ 'SKILL.md frontmatter name matches its directory',
2136
+ ),
2137
+ ]),
2138
+ ...(frontmatter.description === undefined || frontmatter.description.trim() === ''
2139
+ ? [createPolicyViolation('skill', skill, 'SKILL.md description is non-empty')]
2140
+ : matchesSkillTrigger(frontmatter.description)
2141
+ ? []
2142
+ : [
2143
+ createPolicyViolation(
2144
+ 'skill',
2145
+ skill,
2146
+ 'SKILL.md description names when to use the skill in a sentence beginning Use',
2147
+ ),
2148
+ ]),
2149
+ ]),
2150
+ ...inspectSkillTemplateTODOs('skill', skill, content),
2151
+ ...inspectSkillImports(installation, skill, content),
2152
+ ]
1562
2153
  const hasMetadata = isPolicyFile(root, metadata)
1563
- if (!hasMetadata) {
1564
- violations.push(
1565
- createPolicyViolation(
1566
- 'skill',
1567
- metadata,
1568
- 'skill requires an exact-case regular agents/openai.yaml',
1569
- ),
1570
- )
1571
- } else {
1572
- const prompt = parseSkillPrompt(readFileSync(join(root, metadata), 'utf8'))
1573
- if (prompt === undefined) {
1574
- violations.push(
1575
- createPolicyViolation(
1576
- 'skill',
1577
- metadata,
1578
- 'agents/openai.yaml matches the canonical four-line interface schema',
1579
- ),
1580
- )
1581
- } else if (!matchesSkillToken(prompt, name)) {
1582
- violations.push(
2154
+ const prompt = hasMetadata
2155
+ ? parseSkillPrompt(readFileSync(join(root, metadata), 'utf8'))
2156
+ : undefined
2157
+ const described = !hasMetadata
2158
+ ? [
1583
2159
  createPolicyViolation(
1584
2160
  'skill',
1585
2161
  metadata,
1586
- `agents/openai.yaml default_prompt contains the complete token $${name}`,
2162
+ 'skill requires an exact-case regular agents/openai.yaml',
1587
2163
  ),
1588
- )
1589
- }
1590
- }
1591
- const named = content === undefined ? [] : extractSkillReferences(content)
1592
- if (content !== undefined) {
1593
- for (const reference of named) {
1594
- const path = `${base}/${reference}`
1595
- if (!isPolicyFile(root, path)) {
1596
- violations.push(
2164
+ ]
2165
+ : prompt === undefined
2166
+ ? [
1597
2167
  createPolicyViolation(
1598
2168
  'skill',
1599
- path,
1600
- `SKILL.md reference resolves to an exact-case regular file: ${reference}`,
2169
+ metadata,
2170
+ 'agents/openai.yaml matches the canonical four-line interface schema',
1601
2171
  ),
1602
- )
1603
- } else {
1604
- const referenceContent = readFileSync(join(root, path), 'utf8')
1605
- violations.push(
1606
- ...inspectSkillTemplateTODOs('skill', path, referenceContent),
1607
- ...inspectSkillImports(installation, path, referenceContent),
1608
- )
1609
- }
1610
- }
1611
- }
1612
- for (const reference of readSkillReferences(root, name)) {
1613
- if (!named.includes(reference)) {
1614
- violations.push(
1615
- createPolicyViolation(
1616
- 'skill',
1617
- `${base}/${reference}`,
1618
- `references Markdown file is named by SKILL.md: ${reference}`,
1619
- ),
1620
- )
1621
- }
1622
- }
1623
- const scripts = content === undefined ? [] : extractSkillScripts(content, name)
1624
- for (const script of scripts) {
1625
- if (!isPolicyFile(root, `${base}/${script}`)) {
1626
- violations.push(
1627
- createPolicyViolation(
1628
- 'skill',
1629
- `${base}/${script}`,
1630
- `SKILL.md script resolves to an exact-case regular file: ${script}`,
1631
- ),
1632
- )
1633
- }
1634
- }
1635
- for (const script of readSkillScripts(root, name)) {
1636
- if (!scripts.includes(script)) {
1637
- violations.push(
1638
- createPolicyViolation(
1639
- 'skill',
1640
- `${base}/${script}`,
1641
- `scripts TypeScript file is named by SKILL.md: ${script}`,
1642
- ),
1643
- )
1644
- }
1645
- const proof = `${SKILL_PROOF_ROOT}/${name}/${script.replace(/\.ts$/u, '.test.ts')}`
1646
- if (!isPolicyFile(root, proof)) {
1647
- violations.push(
1648
- createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`),
1649
- )
1650
- }
1651
- }
1652
- for (const script of content === undefined ? [] : extractForeignSkillScripts(content, name)) {
1653
- if (!isPolicyFile(root, script)) {
1654
- violations.push(
2172
+ ]
2173
+ : matchesSkillToken(prompt, name)
2174
+ ? []
2175
+ : [
2176
+ createPolicyViolation(
2177
+ 'skill',
2178
+ metadata,
2179
+ `agents/openai.yaml default_prompt contains the complete token $${name}`,
2180
+ ),
2181
+ ]
2182
+ const named = content === undefined ? [] : extractSkillReferences(content)
2183
+ const referenced = named.flatMap((reference) => {
2184
+ const path = `${base}/${reference}`
2185
+ if (!isPolicyFile(root, path)) {
2186
+ return [
1655
2187
  createPolicyViolation(
1656
2188
  'skill',
1657
- script,
1658
- `SKILL.md names another skill's script that resolves to an exact-case regular file: ${script}`,
2189
+ path,
2190
+ `SKILL.md reference resolves to an exact-case regular file: ${reference}`,
1659
2191
  ),
1660
- )
1661
- }
1662
- }
1663
- const references = resolvePolicyDirectory(root, `${base}/references`)
1664
- if (references !== undefined) {
1665
- for (const entry of readdirSync(references, { withFileTypes: true })) {
1666
- if (entry.isDirectory()) {
1667
- violations.push(
1668
- createPolicyViolation(
1669
- 'skill',
1670
- `${base}/references/${entry.name}`,
1671
- 'skill references directory contains no subdirectories',
1672
- ),
1673
- )
1674
- }
2192
+ ]
1675
2193
  }
1676
- }
1677
- const directory = resolvePolicyDirectory(root, base)
1678
- if (directory !== undefined) {
1679
- for (const path of globSync('**/*', { cwd: directory }).map(normalizePolicyPath).sort()) {
1680
- if (resolvePolicyDirectory(directory, path) !== undefined) {
1681
- if (
1682
- path === 'agents' ||
1683
- path === 'references' ||
1684
- path === 'scripts' ||
1685
- path.startsWith('references/') ||
1686
- (!hasSkill && path.toLowerCase() === 'skill.md') ||
1687
- (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
1688
- ) {
1689
- continue
1690
- }
1691
- violations.push(
2194
+ const referenceContent = readFileSync(join(root, path), 'utf8')
2195
+ return [
2196
+ ...inspectSkillTemplateTODOs('skill', path, referenceContent),
2197
+ ...inspectSkillImports(installation, path, referenceContent),
2198
+ ]
2199
+ })
2200
+ const unnamed = readSkillReferences(root, name).flatMap((reference) =>
2201
+ named.includes(reference)
2202
+ ? []
2203
+ : [
1692
2204
  createPolicyViolation(
1693
2205
  'skill',
1694
- `${base}/${path}`,
1695
- 'skill directory contains only agents/, references/, and scripts/ directories',
2206
+ `${base}/${reference}`,
2207
+ `references Markdown file is named by SKILL.md: ${reference}`,
1696
2208
  ),
1697
- )
1698
- continue
1699
- }
1700
- if (!isPolicyFile(directory, path)) continue
1701
- const file = basename(path).toLowerCase()
1702
- if (file === 'readme.md' || file === 'changelog.md') {
1703
- violations.push(
2209
+ ],
2210
+ )
2211
+ const scripts = content === undefined ? [] : extractSkillScripts(content, name)
2212
+ const unresolved = scripts.flatMap((script) =>
2213
+ isPolicyFile(root, `${base}/${script}`)
2214
+ ? []
2215
+ : [
1704
2216
  createPolicyViolation(
1705
2217
  'skill',
1706
- `${base}/${path}`,
1707
- 'skill directory contains no README.md or CHANGELOG.md',
2218
+ `${base}/${script}`,
2219
+ `SKILL.md script resolves to an exact-case regular file: ${script}`,
1708
2220
  ),
2221
+ ],
2222
+ )
2223
+ const unproved = readSkillScripts(root, name).flatMap((script) => {
2224
+ const proof = `${SKILL_PROOF_ROOT}/${name}/${script.replace(/\.ts$/u, '.test.ts')}`
2225
+ return [
2226
+ ...(scripts.includes(script)
2227
+ ? []
2228
+ : [
2229
+ createPolicyViolation(
2230
+ 'skill',
2231
+ `${base}/${script}`,
2232
+ `scripts TypeScript file is named by SKILL.md: ${script}`,
2233
+ ),
2234
+ ]),
2235
+ ...(isPolicyFile(root, proof)
2236
+ ? []
2237
+ : [createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`)]),
2238
+ ]
2239
+ })
2240
+ const foreign = (content === undefined ? [] : extractForeignSkillScripts(content, name)).flatMap(
2241
+ (script) =>
2242
+ isPolicyFile(root, script)
2243
+ ? []
2244
+ : [
2245
+ createPolicyViolation(
2246
+ 'skill',
2247
+ script,
2248
+ `SKILL.md names another skill's script that resolves to an exact-case regular file: ${script}`,
2249
+ ),
2250
+ ],
2251
+ )
2252
+ const references = resolvePolicyDirectory(root, `${base}/references`)
2253
+ const nested =
2254
+ references === undefined
2255
+ ? []
2256
+ : readdirSync(references, { withFileTypes: true }).flatMap((entry) =>
2257
+ entry.isDirectory()
2258
+ ? [
2259
+ createPolicyViolation(
2260
+ 'skill',
2261
+ `${base}/references/${entry.name}`,
2262
+ 'skill references directory contains no subdirectories',
2263
+ ),
2264
+ ]
2265
+ : [],
1709
2266
  )
1710
- continue
1711
- }
1712
- if (
1713
- path === 'SKILL.md' ||
1714
- path === 'agents/openai.yaml' ||
1715
- /^references\/[^/]+\.md$/u.test(path) ||
1716
- /^scripts\/[^/]+\.ts$/u.test(path) ||
1717
- (!hasSkill &&
1718
- (path.toLowerCase() === 'skill.md' || path.toLowerCase().startsWith('skill.md/'))) ||
1719
- (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml') ||
1720
- (path.startsWith('references/') && path.slice('references/'.length).includes('/'))
1721
- ) {
1722
- continue
1723
- }
1724
- violations.push(
1725
- createPolicyViolation(
1726
- 'skill',
1727
- `${base}/${path}`,
1728
- 'skill directory contains only SKILL.md, agents/openai.yaml, references/*.md, and scripts/*.ts',
1729
- ),
1730
- )
1731
- }
1732
- }
1733
- return violations
2267
+ const directory = resolvePolicyDirectory(root, base)
2268
+ const stray =
2269
+ directory === undefined
2270
+ ? []
2271
+ : globSync('**/*', { cwd: directory })
2272
+ .map(normalizePolicyPath)
2273
+ .sort()
2274
+ .flatMap((path) => {
2275
+ if (resolvePolicyDirectory(directory, path) !== undefined) {
2276
+ return path === 'agents' ||
2277
+ path === 'references' ||
2278
+ path === 'scripts' ||
2279
+ path.startsWith('references/') ||
2280
+ (!hasSkill && path.toLowerCase() === 'skill.md') ||
2281
+ (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
2282
+ ? []
2283
+ : [
2284
+ createPolicyViolation(
2285
+ 'skill',
2286
+ `${base}/${path}`,
2287
+ 'skill directory contains only agents/, references/, and scripts/ directories',
2288
+ ),
2289
+ ]
2290
+ }
2291
+ if (!isPolicyFile(directory, path)) return []
2292
+ const file = basename(path).toLowerCase()
2293
+ if (file === 'readme.md' || file === 'changelog.md') {
2294
+ return [
2295
+ createPolicyViolation(
2296
+ 'skill',
2297
+ `${base}/${path}`,
2298
+ 'skill directory contains no README.md or CHANGELOG.md',
2299
+ ),
2300
+ ]
2301
+ }
2302
+ return path === 'SKILL.md' ||
2303
+ path === 'agents/openai.yaml' ||
2304
+ /^references\/[^/]+\.md$/u.test(path) ||
2305
+ /^scripts\/[^/]+\.ts$/u.test(path) ||
2306
+ (!hasSkill &&
2307
+ (path.toLowerCase() === 'skill.md' ||
2308
+ path.toLowerCase().startsWith('skill.md/'))) ||
2309
+ (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml') ||
2310
+ (path.startsWith('references/') && path.slice('references/'.length).includes('/'))
2311
+ ? []
2312
+ : [
2313
+ createPolicyViolation(
2314
+ 'skill',
2315
+ `${base}/${path}`,
2316
+ 'skill directory contains only SKILL.md, agents/openai.yaml, references/*.md, and scripts/*.ts',
2317
+ ),
2318
+ ]
2319
+ })
2320
+ return [
2321
+ ...documented,
2322
+ ...described,
2323
+ ...referenced,
2324
+ ...unnamed,
2325
+ ...unresolved,
2326
+ ...unproved,
2327
+ ...foreign,
2328
+ ...nested,
2329
+ ...stray,
2330
+ ]
1734
2331
  }
1735
2332
 
1736
2333
  /**
@@ -1741,10 +2338,7 @@ export function inspectSkill(
1741
2338
  * @returns Every skill-family violation in directory and invariant order.
1742
2339
  */
1743
2340
  export function inspectSkillFamily(root: string, installation = root): readonly PolicyViolation[] {
1744
- const violations: PolicyViolation[] = []
1745
- for (const name of readSkillFamily(root))
1746
- violations.push(...inspectSkill(root, name, installation))
1747
- return violations
2341
+ return readSkillFamily(root).flatMap((name) => inspectSkill(root, name, installation))
1748
2342
  }
1749
2343
 
1750
2344
  /**
@@ -1768,110 +2362,101 @@ export function inspectBridge(root: string, name: string): readonly PolicyViolat
1768
2362
  const canonical = isPolicyFile(root, canonicalPath)
1769
2363
  ? parseSkillFrontmatter(readFileSync(join(root, canonicalPath), 'utf8'))
1770
2364
  : undefined
1771
- const violations: PolicyViolation[] = []
1772
- if (bridge === undefined) {
1773
- violations.push(
1774
- createPolicyViolation('bridge', bridgePath, 'bridge SKILL.md frontmatter parses'),
1775
- )
1776
- } else {
1777
- const keys = new Set(bridge.keys)
1778
- if (
1779
- bridge.keys.length !== 2 ||
1780
- keys.size !== 2 ||
1781
- !keys.has('name') ||
1782
- !keys.has('description')
1783
- ) {
1784
- violations.push(
1785
- createPolicyViolation(
1786
- 'bridge',
1787
- bridgePath,
1788
- 'bridge SKILL.md frontmatter contains exactly name and description',
1789
- ),
1790
- )
1791
- }
1792
- }
1793
- if (bridge !== undefined && canonical !== undefined) {
1794
- if (bridge.source.name !== canonical.source.name) {
1795
- violations.push(
1796
- createPolicyViolation(
1797
- 'bridge',
1798
- bridgePath,
1799
- 'bridge frontmatter name matches its canonical twin',
1800
- ),
1801
- )
1802
- }
1803
- if (bridge.source.description !== canonical.source.description) {
1804
- violations.push(
1805
- createPolicyViolation(
1806
- 'bridge',
1807
- bridgePath,
1808
- 'bridge frontmatter description matches its canonical twin',
1809
- ),
1810
- )
1811
- }
1812
- }
2365
+ const keys = new Set(bridge?.keys)
1813
2366
  const normalized = content.replaceAll('\r\n', '\n')
1814
2367
  const boundary = normalized.indexOf('\n---', 3)
1815
2368
  const body = boundary === -1 ? normalized : normalized.slice(boundary + '\n---'.length)
1816
- if (!body.includes(canonicalPath)) {
1817
- violations.push(
1818
- createPolicyViolation(
1819
- 'bridge',
1820
- bridgePath,
1821
- `bridge body names its canonical workflow: ${canonicalPath}`,
1822
- ),
1823
- )
1824
- }
1825
- violations.push(...inspectSkillTemplateTODOs('bridge', bridgePath, content))
1826
- if (resolvePolicyDirectory(root, `${bridgeBase}/references`) !== undefined) {
1827
- violations.push(
1828
- createPolicyViolation(
1829
- 'bridge',
1830
- `${bridgeBase}/references`,
1831
- 'bridge owns no references directory',
1832
- ),
1833
- )
1834
- }
1835
- return violations
2369
+ return [
2370
+ ...(bridge === undefined
2371
+ ? [createPolicyViolation('bridge', bridgePath, 'bridge SKILL.md frontmatter parses')]
2372
+ : bridge.keys.length !== 2 || keys.size !== 2 || !keys.has('name') || !keys.has('description')
2373
+ ? [
2374
+ createPolicyViolation(
2375
+ 'bridge',
2376
+ bridgePath,
2377
+ 'bridge SKILL.md frontmatter contains exactly name and description',
2378
+ ),
2379
+ ]
2380
+ : []),
2381
+ ...(bridge !== undefined &&
2382
+ canonical !== undefined &&
2383
+ bridge.source.name !== canonical.source.name
2384
+ ? [
2385
+ createPolicyViolation(
2386
+ 'bridge',
2387
+ bridgePath,
2388
+ 'bridge frontmatter name matches its canonical twin',
2389
+ ),
2390
+ ]
2391
+ : []),
2392
+ ...(bridge !== undefined &&
2393
+ canonical !== undefined &&
2394
+ bridge.source.description !== canonical.source.description
2395
+ ? [
2396
+ createPolicyViolation(
2397
+ 'bridge',
2398
+ bridgePath,
2399
+ 'bridge frontmatter description matches its canonical twin',
2400
+ ),
2401
+ ]
2402
+ : []),
2403
+ ...(body.includes(canonicalPath)
2404
+ ? []
2405
+ : [
2406
+ createPolicyViolation(
2407
+ 'bridge',
2408
+ bridgePath,
2409
+ `bridge body names its canonical workflow: ${canonicalPath}`,
2410
+ ),
2411
+ ]),
2412
+ ...inspectSkillTemplateTODOs('bridge', bridgePath, content),
2413
+ ...(resolvePolicyDirectory(root, `${bridgeBase}/references`) === undefined
2414
+ ? []
2415
+ : [
2416
+ createPolicyViolation(
2417
+ 'bridge',
2418
+ `${bridgeBase}/references`,
2419
+ 'bridge owns no references directory',
2420
+ ),
2421
+ ]),
2422
+ ]
1836
2423
  }
1837
2424
 
1838
2425
  /**
1839
- * Inspects the provider bridge set and every bridge shared with the canonical skill family.
1840
- *
1841
- * @param root - The workspace root to inspect.
1842
- * @returns Every bridge-set and bridge-content violation in directory order.
1843
- */
1844
- export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
1845
- const canonical = readSkillFamily(root)
1846
- const bridges = readPolicyDirectories(root, SKILL_BRIDGE_ROOT)
1847
- const bridgeSet = new Set(bridges)
1848
- const canonicalSet = new Set(canonical)
1849
- const violations: PolicyViolation[] = []
1850
- for (const name of canonical) {
1851
- if (!bridgeSet.has(name)) {
1852
- violations.push(
1853
- createPolicyViolation(
1854
- 'bridge',
1855
- `${SKILL_BRIDGE_ROOT}/${name}`,
1856
- 'canonical skill has a matching provider bridge directory',
1857
- ),
1858
- )
1859
- } else {
1860
- violations.push(...inspectBridge(root, name))
1861
- }
1862
- }
1863
- for (const name of bridges) {
1864
- if (!canonicalSet.has(name)) {
1865
- violations.push(
1866
- createPolicyViolation(
1867
- 'bridge',
1868
- `${SKILL_BRIDGE_ROOT}/${name}`,
1869
- 'provider bridge directory has a canonical skill twin',
1870
- ),
1871
- )
1872
- }
1873
- }
1874
- return violations
2426
+ * Inspects the provider bridge set and every bridge shared with the canonical skill family.
2427
+ *
2428
+ * @param root - The workspace root to inspect.
2429
+ * @returns Every bridge-set and bridge-content violation in directory order.
2430
+ */
2431
+ export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
2432
+ const canonical = readSkillFamily(root)
2433
+ const bridges = readPolicyDirectories(root, SKILL_BRIDGE_ROOT)
2434
+ const bridgeSet = new Set(bridges)
2435
+ const canonicalSet = new Set(canonical)
2436
+ return [
2437
+ ...canonical.flatMap((name) =>
2438
+ bridgeSet.has(name)
2439
+ ? inspectBridge(root, name)
2440
+ : [
2441
+ createPolicyViolation(
2442
+ 'bridge',
2443
+ `${SKILL_BRIDGE_ROOT}/${name}`,
2444
+ 'canonical skill has a matching provider bridge directory',
2445
+ ),
2446
+ ],
2447
+ ),
2448
+ ...bridges.flatMap((name) =>
2449
+ canonicalSet.has(name)
2450
+ ? []
2451
+ : [
2452
+ createPolicyViolation(
2453
+ 'bridge',
2454
+ `${SKILL_BRIDGE_ROOT}/${name}`,
2455
+ 'provider bridge directory has a canonical skill twin',
2456
+ ),
2457
+ ],
2458
+ ),
2459
+ ]
1875
2460
  }
1876
2461
 
1877
2462
  /**
@@ -1884,14 +2469,10 @@ export function readPolicyRuleMap(content: string): readonly string[] {
1884
2469
  const lines = content.replaceAll('\r\n', '\n').split('\n')
1885
2470
  const heading = lines.indexOf(POLICY_RULE_MAP_HEADING)
1886
2471
  if (heading === -1) return []
1887
- const paths: string[] = []
1888
- for (let index = heading + 1; index < lines.length; index += 1) {
1889
- const line = lines[index]
1890
- if (line === undefined || line.startsWith('## ')) break
2472
+ return extractPolicySection(lines, heading).flatMap((line) => {
1891
2473
  const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
1892
- if (cell !== undefined) paths.push(normalizePolicyPath(cell))
1893
- }
1894
- return paths
2474
+ return cell === undefined ? [] : [normalizePolicyPath(cell)]
2475
+ })
1895
2476
  }
1896
2477
 
1897
2478
  /**
@@ -1914,20 +2495,18 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
1914
2495
  ? readFileSync(join(root, POLICY_RULE_MAP_FILE), 'utf8')
1915
2496
  : ''
1916
2497
  const registered = new Set(readPolicyRuleMap(content))
1917
- const violations: PolicyViolation[] = []
1918
- for (const rule of rules) {
1919
- if (!registered.has(rule)) {
1920
- violations.push(createPolicyViolation('rules', rule, 'the rule map names every rule file'))
1921
- }
1922
- }
1923
- for (const path of registered) {
1924
- if (!isPolicyFile(root, path)) {
1925
- violations.push(
1926
- createPolicyViolation('rules', path, 'every rule-map row resolves to a rule file'),
1927
- )
1928
- }
1929
- }
1930
- return violations
2498
+ return [
2499
+ ...rules.flatMap((rule) =>
2500
+ registered.has(rule)
2501
+ ? []
2502
+ : [createPolicyViolation('rules', rule, 'the rule map names every rule file')],
2503
+ ),
2504
+ ...[...registered].flatMap((path) =>
2505
+ isPolicyFile(root, path)
2506
+ ? []
2507
+ : [createPolicyViolation('rules', path, 'every rule-map row resolves to a rule file')],
2508
+ ),
2509
+ ]
1931
2510
  }
1932
2511
 
1933
2512
  /**
@@ -1941,50 +2520,53 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
1941
2520
  * @returns Every unusable-name and case-collision violation in path order.
1942
2521
  */
1943
2522
  export function inspectPolicyFilenamePaths(paths: readonly string[]): readonly PolicyViolation[] {
1944
- const violations: PolicyViolation[] = []
1945
- const folded = new Map<string, string>()
1946
- for (const candidate of paths) {
1947
- const path = normalizePolicyPath(candidate)
2523
+ // The iterator visits a hole as `undefined`, which normalization refuses, as the loop did.
2524
+ const normalized = Iterator.from(paths).map(normalizePolicyPath).toArray()
2525
+ // Built from the reversed list, so the first path that folds to a key is the spelling a collision reports.
2526
+ const folded = new Map(normalized.toReversed().map((path) => [path.toLowerCase(), path]))
2527
+ return normalized.flatMap((path) => {
1948
2528
  const segment = basename(path)
1949
2529
  const [stem = ''] = segment.split('.')
1950
- if (POLICY_RESERVED_NAMES.includes(stem.toLowerCase())) {
1951
- violations.push(
1952
- createPolicyViolation(
1953
- 'portability',
1954
- path,
1955
- 'path segments avoid the names Windows reserves',
1956
- ),
1957
- )
1958
- }
1959
- if (POLICY_RESERVED_PATTERN.test(segment)) {
1960
- violations.push(
1961
- createPolicyViolation(
1962
- 'portability',
1963
- path,
1964
- 'path segments avoid the characters Windows refuses',
1965
- ),
1966
- )
1967
- }
1968
- if (segment.endsWith('.') || segment.endsWith(' ')) {
1969
- violations.push(
1970
- createPolicyViolation(
1971
- 'portability',
1972
- path,
1973
- 'path segments end with neither a dot nor a space',
1974
- ),
1975
- )
1976
- }
1977
- const key = path.toLowerCase()
1978
- const previous = folded.get(key)
1979
- if (previous === undefined) {
1980
- folded.set(key, path)
1981
- } else if (previous !== path) {
1982
- violations.push(
1983
- createPolicyViolation('portability', path, `path differs from ${previous} by case alone`),
1984
- )
1985
- }
1986
- }
1987
- return violations
2530
+ const previous = folded.get(path.toLowerCase())
2531
+ return [
2532
+ ...(POLICY_RESERVED_NAMES.includes(stem.toLowerCase())
2533
+ ? [
2534
+ createPolicyViolation(
2535
+ 'portability',
2536
+ path,
2537
+ 'path segments avoid the names Windows reserves',
2538
+ ),
2539
+ ]
2540
+ : []),
2541
+ ...(POLICY_RESERVED_PATTERN.test(segment)
2542
+ ? [
2543
+ createPolicyViolation(
2544
+ 'portability',
2545
+ path,
2546
+ 'path segments avoid the characters Windows refuses',
2547
+ ),
2548
+ ]
2549
+ : []),
2550
+ ...(segment.endsWith('.') || segment.endsWith(' ')
2551
+ ? [
2552
+ createPolicyViolation(
2553
+ 'portability',
2554
+ path,
2555
+ 'path segments end with neither a dot nor a space',
2556
+ ),
2557
+ ]
2558
+ : []),
2559
+ ...(previous === undefined || previous === path
2560
+ ? []
2561
+ : [
2562
+ createPolicyViolation(
2563
+ 'portability',
2564
+ path,
2565
+ `path differs from ${previous} by case alone`,
2566
+ ),
2567
+ ]),
2568
+ ]
2569
+ })
1988
2570
  }
1989
2571
 
1990
2572
  /**
@@ -2014,21 +2596,16 @@ export function inspectPolicyFilenames(root: string): readonly PolicyViolation[]
2014
2596
  * @returns Each script name and its command, in manifest order.
2015
2597
  */
2016
2598
  export function parsePolicyScripts(content: string): ReadonlyMap<string, string> {
2017
- const scripts = new Map<string, string>()
2018
- let manifest: unknown
2019
- try {
2020
- manifest = JSON.parse(content)
2021
- } catch {
2022
- return scripts
2023
- }
2024
- if (!isPolicyRecord(manifest)) return scripts
2599
+ const manifest = parsePolicyJSON(content)
2600
+ if (!isPolicyRecord(manifest)) return new Map()
2025
2601
  const record: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
2026
- if (!isPolicyRecord(record)) return scripts
2027
- for (const name of Object.getOwnPropertyNames(record)) {
2028
- const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
2029
- if (typeof command === 'string') scripts.set(name, command)
2030
- }
2031
- return scripts
2602
+ if (!isPolicyRecord(record)) return new Map()
2603
+ return new Map(
2604
+ Object.getOwnPropertyNames(record).flatMap((name): Array<[string, string]> => {
2605
+ const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
2606
+ return typeof command === 'string' ? [[name, command]] : []
2607
+ }),
2608
+ )
2032
2609
  }
2033
2610
 
2034
2611
  /**
@@ -2040,19 +2617,17 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
2040
2617
  export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
2041
2618
  if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return []
2042
2619
  const content = readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8')
2043
- const violations: PolicyViolation[] = []
2044
- for (const [name, command] of parsePolicyScripts(content)) {
2045
- if (POLICY_SHELL_PATTERN.test(command)) {
2046
- violations.push(
2047
- createPolicyViolation(
2048
- 'portability',
2049
- POLICY_MANIFEST_FILE,
2050
- `manifest scripts name no .sh file: ${name}`,
2051
- ),
2052
- )
2053
- }
2054
- }
2055
- return violations
2620
+ return [...parsePolicyScripts(content)].flatMap(([name, command]) =>
2621
+ POLICY_SHELL_PATTERN.test(command)
2622
+ ? [
2623
+ createPolicyViolation(
2624
+ 'portability',
2625
+ POLICY_MANIFEST_FILE,
2626
+ `manifest scripts name no .sh file: ${name}`,
2627
+ ),
2628
+ ]
2629
+ : [],
2630
+ )
2056
2631
  }
2057
2632
 
2058
2633
  /**
@@ -2091,12 +2666,7 @@ export function readPolicyProse(root: string): readonly string[] {
2091
2666
  */
2092
2667
  export function readPolicyPackage(root: string): string | undefined {
2093
2668
  if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return undefined
2094
- let manifest: unknown
2095
- try {
2096
- manifest = JSON.parse(readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8'))
2097
- } catch {
2098
- return undefined
2099
- }
2669
+ const manifest = readPolicyJSON(join(root, POLICY_MANIFEST_FILE))
2100
2670
  if (!isPolicyRecord(manifest)) return undefined
2101
2671
  const name: unknown = Object.getOwnPropertyDescriptor(manifest, 'name')?.value
2102
2672
  if (typeof name !== 'string') return undefined
@@ -2123,14 +2693,10 @@ export function readPolicyCatalog(
2123
2693
  const lines = readFileSync(join(root, path), 'utf8').replaceAll('\r\n', '\n').split('\n')
2124
2694
  const heading = lines.indexOf(POLICY_CATALOG_HEADING)
2125
2695
  if (heading === -1) return []
2126
- const names: string[] = []
2127
- for (let index = heading + 1; index < lines.length; index += 1) {
2128
- const line = lines[index]
2129
- if (line === undefined || line.startsWith('## ')) break
2696
+ return extractPolicySection(lines, heading).flatMap((line) => {
2130
2697
  const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
2131
- if (cell !== undefined) names.push(cell.slice(cell.lastIndexOf('/') + 1))
2132
- }
2133
- return names
2698
+ return cell === undefined ? [] : [cell.slice(cell.lastIndexOf('/') + 1)]
2699
+ })
2134
2700
  }
2135
2701
 
2136
2702
  /**
@@ -2200,31 +2766,30 @@ export function isPolicyStray(root: string, path: string): boolean {
2200
2766
  * order.
2201
2767
  */
2202
2768
  export function inspectPolicyProse(root: string): readonly PolicyViolation[] {
2203
- const violations: PolicyViolation[] = []
2204
- for (const path of readPolicyProse(root)) {
2205
- if (isPolicyStray(root, path)) {
2206
- violations.push(
2207
- createPolicyViolation(
2208
- 'prose',
2209
- path,
2210
- "guide is the package's own, the map, or a catalog row",
2211
- ),
2212
- )
2213
- }
2214
- if (isPolicyMirror(root, path)) continue
2769
+ return readPolicyProse(root).flatMap((path) => {
2770
+ const stray = isPolicyStray(root, path)
2771
+ ? [
2772
+ createPolicyViolation(
2773
+ 'prose',
2774
+ path,
2775
+ "guide is the package's own, the map, or a catalog row",
2776
+ ),
2777
+ ]
2778
+ : []
2779
+ if (isPolicyMirror(root, path)) return stray
2215
2780
  const prose = stripPolicyCode(readFileSync(join(root, path), 'utf8'))
2216
- for (const hit of textToPolicyHits(prose)) {
2217
- violations.push(
2781
+ return [
2782
+ ...stray,
2783
+ ...textToPolicyHits(prose).map((hit) =>
2218
2784
  createPolicyViolation(
2219
2785
  'prose',
2220
2786
  path,
2221
2787
  `prose carries no banned term: ${hit.term.term} (${hit.term.replacement})`,
2222
2788
  prose.slice(0, hit.index).split('\n').length,
2223
2789
  ),
2224
- )
2225
- }
2226
- }
2227
- return violations
2790
+ ),
2791
+ ]
2792
+ })
2228
2793
  }
2229
2794
 
2230
2795
  /**
@@ -2241,18 +2806,12 @@ export function readPolicyTerms(content: string): readonly string[] {
2241
2806
  const lines = content.replaceAll('\r\n', '\n').split('\n')
2242
2807
  const heading = lines.indexOf(POLICY_TERM_HEADING)
2243
2808
  if (heading === -1) return []
2244
- const terms: string[] = []
2245
- for (let index = heading + 1; index < lines.length; index += 1) {
2246
- const line = lines[index]
2247
- if (line === undefined || line.startsWith('## ')) break
2809
+ return extractPolicySection(lines, heading).flatMap((line) => {
2248
2810
  const cell = line.match(/^\|([^|]*)\|/u)?.[1]
2249
- if (cell === undefined) continue
2250
- for (const match of cell.matchAll(/`([^`]+)`/gu)) {
2251
- const term = match[1]
2252
- if (term !== undefined) terms.push(term)
2253
- }
2254
- }
2255
- return terms
2811
+ return cell === undefined
2812
+ ? []
2813
+ : [...cell.matchAll(/`([^`]+)`/gu)].flatMap((match) => match[1] ?? [])
2814
+ })
2256
2815
  }
2257
2816
 
2258
2817
  /**
@@ -2272,7 +2831,7 @@ export function inspectPolicyPortability(root: string): readonly PolicyViolation
2272
2831
  /**
2273
2832
  * Locates parsed exports at their physical declaration lines.
2274
2833
  *
2275
- * @param path - The declaring workspace-relative path.
2834
+ * @param written - The declaring workspace-relative path, normalized before it is read.
2276
2835
  * @param text - The declaration source text.
2277
2836
  * @param root - The workspace root used to resolve relative star exports, when supplied.
2278
2837
  * @param ancestors - The source paths already visited along this star-export branch.
@@ -2280,17 +2839,16 @@ export function inspectPolicyPortability(root: string): readonly PolicyViolation
2280
2839
  * @throws An `Error` when syntax, an export form, or a star target cannot be read.
2281
2840
  */
2282
2841
  export function readPolicyDeclarations(
2283
- path: string,
2842
+ written: string,
2284
2843
  text: string,
2285
2844
  root?: string,
2286
2845
  ancestors: readonly string[] = [],
2287
2846
  ): readonly PolicySurfaceDeclaration[] {
2288
- path = normalizePolicyPath(path)
2847
+ const path = normalizePolicyPath(written)
2289
2848
  if (ancestors.includes(path)) return []
2290
2849
  const source = parseSync(path, text)
2291
2850
  if (source.errors.length > 0) throw new Error(`export syntax is unreadable at ${path}`)
2292
- const declarations: PolicySurfaceDeclaration[] = []
2293
- for (const statement of source.program.body) {
2851
+ return source.program.body.flatMap((statement): readonly PolicySurfaceDeclaration[] => {
2294
2852
  const line = text.slice(0, statement.start).split(/\r\n|\n/u).length
2295
2853
  if (statement.type === 'ExportAllDeclaration') {
2296
2854
  if (statement.exported !== null) {
@@ -2299,8 +2857,7 @@ export function readPolicyDeclarations(
2299
2857
  ? statement.exported.name
2300
2858
  : statement.exported.value
2301
2859
  if (name === 'default') throw new Error(`default export is unsupported at ${path}:${line}`)
2302
- declarations.push({ name, path, line })
2303
- continue
2860
+ return [{ name, path, line }]
2304
2861
  }
2305
2862
  const target = statement.source.value
2306
2863
  const resolved = resolveLink(
@@ -2315,40 +2872,35 @@ export function readPolicyDeclarations(
2315
2872
  ) {
2316
2873
  throw new Error(`star export target is unreadable at ${path}:${line}: ${target}`)
2317
2874
  }
2318
- declarations.push(
2319
- ...readPolicyDeclarations(resolved, readFileSync(join(root, resolved), 'utf8'), root, [
2320
- ...ancestors,
2321
- path,
2322
- ]),
2323
- )
2324
- continue
2875
+ return readPolicyDeclarations(resolved, readFileSync(join(root, resolved), 'utf8'), root, [
2876
+ ...ancestors,
2877
+ path,
2878
+ ])
2325
2879
  }
2326
2880
  if (statement.type === 'ExportNamedDeclaration') {
2327
- for (const specifier of statement.specifiers) {
2881
+ const listed = statement.specifiers.map((specifier) => {
2328
2882
  const name =
2329
2883
  specifier.exported.type === 'Identifier'
2330
2884
  ? specifier.exported.name
2331
2885
  : specifier.exported.value
2332
2886
  if (name === 'default') throw new Error(`default export is unsupported at ${path}:${line}`)
2333
- declarations.push({
2334
- name,
2335
- path,
2336
- line: text.slice(0, specifier.start).split(/\r\n|\n/u).length,
2337
- })
2338
- }
2887
+ return { name, path, line: text.slice(0, specifier.start).split(/\r\n|\n/u).length }
2888
+ })
2339
2889
  const declaration = statement.declaration
2340
- if (declaration === null) continue
2890
+ if (declaration === null) return listed
2341
2891
  if (declaration.type === 'VariableDeclaration') {
2342
- for (const variable of declaration.declarations) {
2343
- if (variable.id.type !== 'Identifier')
2344
- throw new Error(`export binding is unsupported at ${path}:${line}`)
2345
- declarations.push({
2346
- name: variable.id.name,
2347
- path,
2348
- line: text.slice(0, variable.id.start).split(/\r\n|\n/u).length,
2349
- })
2350
- }
2351
- continue
2892
+ return [
2893
+ ...listed,
2894
+ ...declaration.declarations.map((variable) => {
2895
+ if (variable.id.type !== 'Identifier')
2896
+ throw new Error(`export binding is unsupported at ${path}:${line}`)
2897
+ return {
2898
+ name: variable.id.name,
2899
+ path,
2900
+ line: text.slice(0, variable.id.start).split(/\r\n|\n/u).length,
2901
+ }
2902
+ }),
2903
+ ]
2352
2904
  }
2353
2905
  if (
2354
2906
  declaration.type === 'FunctionDeclaration' ||
@@ -2361,8 +2913,7 @@ export function readPolicyDeclarations(
2361
2913
  ) {
2362
2914
  if (declaration.id?.type !== 'Identifier')
2363
2915
  throw new Error(`export name is unreadable at ${path}:${line}`)
2364
- declarations.push({ name: declaration.id.name, path, line })
2365
- continue
2916
+ return [...listed, { name: declaration.id.name, path, line }]
2366
2917
  }
2367
2918
  throw new Error(`export declaration is unsupported at ${path}:${line}: ${declaration.type}`)
2368
2919
  }
@@ -2373,8 +2924,8 @@ export function readPolicyDeclarations(
2373
2924
  ) {
2374
2925
  throw new Error(`export statement is unsupported at ${path}:${line}: ${statement.type}`)
2375
2926
  }
2376
- }
2377
- return declarations
2927
+ return []
2928
+ })
2378
2929
  }
2379
2930
 
2380
2931
  /**
@@ -2414,21 +2965,21 @@ export function collectPolicyDeclarations(
2414
2965
  * exactly one bare `./index.scss` import, including styles extensions and themes.
2415
2966
  */
2416
2967
  export function readPolicySurface(root: string): PolicySurfacePopulation {
2417
- const files: Record<string, string> = {}
2418
- for (const path of globSync('src/**/*.ts', { cwd: root }).map(normalizePolicyPath).sort()) {
2419
- files[path] = readFileSync(join(root, path), 'utf8')
2420
- }
2968
+ const files = Object.fromEntries(
2969
+ globSync('src/**/*.ts', { cwd: root })
2970
+ .map(normalizePolicyPath)
2971
+ .sort()
2972
+ .map((path): [string, string] => [path, readFileSync(join(root, path), 'utf8')]),
2973
+ )
2421
2974
  const barrels = Object.keys(files).filter((path) => path.endsWith('/index.ts'))
2422
- const targets = new Set<string>()
2423
- const violations: PolicyViolation[] = []
2424
2975
  const sheets = new Set([
2425
2976
  POLICY_SURFACE_STYLES_ENTRY,
2426
2977
  ...collectSheets(root).map((face) => `src/${face}/sheet.ts`),
2427
2978
  'src/styles/themes/sheet.ts',
2428
2979
  ])
2429
- for (const path of sheets) {
2980
+ const entryViolations = [...sheets].flatMap((path) => {
2430
2981
  const stylesEntry = files[path]
2431
- if (stylesEntry === undefined) continue
2982
+ if (stylesEntry === undefined) return []
2432
2983
  const stylesSource = parseSync(path, stylesEntry)
2433
2984
  const stylesStatement =
2434
2985
  stylesSource.errors.length === 0 ? stylesSource.program.body[0] : undefined
@@ -2438,79 +2989,85 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
2438
2989
  stylesStatement?.type === 'ImportDeclaration' &&
2439
2990
  stylesStatement.specifiers.length === 0 &&
2440
2991
  stylesStatement.source.value === './index.scss'
2441
- if (!stylesValid) {
2442
- const line =
2443
- stylesStatement === undefined
2444
- ? 1
2445
- : stylesEntry.slice(0, stylesStatement.start).split(/\r\n|\n/u).length
2446
- violations.push(createPolicyViolation('surface', path, POLICY_SURFACE_STYLES_MESSAGE, line))
2447
- }
2448
- }
2449
- for (const path of barrels) {
2450
- const text = files[path]
2451
- if (text === undefined) continue
2452
- const source = parseSync(path, text)
2453
- const lines = extractSourceLines(text)
2454
- if (source.errors.length > 0) {
2455
- violations.push(
2456
- createPolicyViolation(
2457
- 'surface',
2458
- path,
2459
- 'surface population incomplete: barrel syntax is unreadable',
2460
- ),
2461
- )
2462
- continue
2463
- }
2464
- for (const statement of source.program.body) {
2465
- const start = text.slice(0, statement.start).split(/\r\n|\n/u).length - 1
2466
- const end = text.slice(0, statement.end).split(/\r\n|\n/u).length - 1
2467
- const row = lines[start]?.code.match(POLICY_SURFACE_BARREL_PATTERN)
2468
- const target = row?.[1] ?? row?.[2]
2469
- if (
2470
- statement.type !== 'ExportAllDeclaration' ||
2471
- statement.exportKind !== 'value' ||
2472
- statement.exported !== null ||
2473
- start !== end ||
2474
- target === undefined
2475
- ) {
2476
- violations.push(
2477
- createPolicyViolation(
2478
- 'surface',
2479
- path,
2480
- 'surface population incomplete: barrel requires a relative .js star export on one line',
2481
- start + 1,
2482
- ),
2483
- )
2484
- continue
2485
- }
2486
- const resolved = resolveLink(path, `${target.slice(0, -3)}.ts`)
2487
- if (
2488
- !hasCanonicalSegments(resolved) ||
2489
- !resolved.startsWith('src/') ||
2490
- files[resolved] === undefined
2491
- ) {
2492
- violations.push(
2493
- createPolicyViolation(
2494
- 'surface',
2495
- path,
2496
- `surface population incomplete: barrel target is unreadable: ${target}`,
2497
- start + 1,
2498
- ),
2499
- )
2500
- continue
2992
+ const line =
2993
+ stylesStatement === undefined
2994
+ ? 1
2995
+ : stylesEntry.slice(0, stylesStatement.start).split(/\r\n|\n/u).length
2996
+ return stylesValid
2997
+ ? []
2998
+ : [createPolicyViolation('surface', path, POLICY_SURFACE_STYLES_MESSAGE, line)]
2999
+ })
3000
+ // Each barrel statement answers one violation, one star-export target, or neither.
3001
+ const readings = barrels.flatMap(
3002
+ (path): ReadonlyArray<{ readonly violation?: PolicyViolation; readonly target?: string }> => {
3003
+ const text = files[path]
3004
+ if (text === undefined) return []
3005
+ const source = parseSync(path, text)
3006
+ const lines = extractSourceLines(text)
3007
+ if (source.errors.length > 0) {
3008
+ return [
3009
+ {
3010
+ violation: createPolicyViolation(
3011
+ 'surface',
3012
+ path,
3013
+ 'surface population incomplete: barrel syntax is unreadable',
3014
+ ),
3015
+ },
3016
+ ]
2501
3017
  }
2502
- if (!resolved.endsWith('/index.ts')) targets.add(resolved)
2503
- }
2504
- }
2505
- const declarations: PolicySurfaceDeclaration[] = []
2506
- for (const path of targets) {
3018
+ return source.program.body.map((statement) => {
3019
+ const start = text.slice(0, statement.start).split(/\r\n|\n/u).length - 1
3020
+ const end = text.slice(0, statement.end).split(/\r\n|\n/u).length - 1
3021
+ const row = lines[start]?.code.match(POLICY_SURFACE_BARREL_PATTERN)
3022
+ const target = row?.[1] ?? row?.[2]
3023
+ if (
3024
+ statement.type !== 'ExportAllDeclaration' ||
3025
+ statement.exportKind !== 'value' ||
3026
+ statement.exported !== null ||
3027
+ start !== end ||
3028
+ target === undefined
3029
+ ) {
3030
+ return {
3031
+ violation: createPolicyViolation(
3032
+ 'surface',
3033
+ path,
3034
+ 'surface population incomplete: barrel requires a relative .js star export on one line',
3035
+ start + 1,
3036
+ ),
3037
+ }
3038
+ }
3039
+ const resolved = resolveLink(path, `${target.slice(0, -3)}.ts`)
3040
+ if (
3041
+ !hasCanonicalSegments(resolved) ||
3042
+ !resolved.startsWith('src/') ||
3043
+ files[resolved] === undefined
3044
+ ) {
3045
+ return {
3046
+ violation: createPolicyViolation(
3047
+ 'surface',
3048
+ path,
3049
+ `surface population incomplete: barrel target is unreadable: ${target}`,
3050
+ start + 1,
3051
+ ),
3052
+ }
3053
+ }
3054
+ return resolved.endsWith('/index.ts') ? {} : { target: resolved }
3055
+ })
3056
+ },
3057
+ )
3058
+ const targets = new Set(readings.flatMap(({ target }) => (target === undefined ? [] : [target])))
3059
+ const reads = [...targets].flatMap((path) => {
2507
3060
  const text = files[path]
2508
- if (text === undefined) continue
2509
- const read = collectPolicyDeclarations(root, path, text)
2510
- declarations.push(...read.declarations)
2511
- if (read.violation !== undefined) violations.push(read.violation)
3061
+ return text === undefined ? [] : [collectPolicyDeclarations(root, path, text)]
3062
+ })
3063
+ return {
3064
+ declarations: reads.flatMap((read) => read.declarations),
3065
+ violations: [
3066
+ ...entryViolations,
3067
+ ...readings.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3068
+ ...reads.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
3069
+ ],
2512
3070
  }
2513
- return { declarations, violations }
2514
3071
  }
2515
3072
 
2516
3073
  /**
@@ -2541,79 +3098,97 @@ export function inspectPolicySurface(root: string): readonly PolicyViolation[] {
2541
3098
  ),
2542
3099
  ]
2543
3100
  }
2544
- const violations: PolicyViolation[] = []
2545
- for (const name of names) {
2546
- if (!isPolicyFile(host, `guides/${name}.md`)) {
2547
- violations.push(
2548
- createPolicyViolation(
2549
- 'surface',
2550
- `${directory}/${name}.md`,
2551
- `surface evidence missing: catalog package ${name} has no hosted guide`,
2552
- ),
2553
- )
2554
- }
2555
- }
2556
- const grandfather = new Set<string>()
2557
- const owners = new Map<string, Set<string>>()
2558
- for (const path of globSync('guides/*.md', { cwd: host }).map(normalizePolicyPath).sort()) {
2559
- const owner = basename(path, '.md')
2560
- if (owner === POLICY_GUIDE_MAP) continue
2561
- const guide = createGuide(readFileSync(join(host, path), 'utf8'))
2562
- if (!guide.sections().includes('Surface')) {
2563
- violations.push(
2564
- createPolicyViolation(
2565
- 'surface',
2566
- `${directory}/${owner}.md`,
2567
- 'surface evidence missing: hosted guide has no Surface section',
2568
- ),
2569
- )
2570
- continue
2571
- }
2572
- for (const symbol of guide.surface()) {
2573
- if (owner === own) {
2574
- grandfather.add(symbol.name)
2575
- continue
2576
- }
2577
- const claimed = owners.get(symbol.name) ?? new Set<string>()
2578
- claimed.add(owner)
2579
- owners.set(symbol.name, claimed)
2580
- }
2581
- }
2582
- const population = readPolicySurface(root)
2583
- violations.push(...population.violations)
2584
- const declarations = population.declarations.filter(
2585
- (declaration) => !grandfather.has(declaration.name),
3101
+ const missing = [...names].flatMap((name) =>
3102
+ isPolicyFile(host, `guides/${name}.md`)
3103
+ ? []
3104
+ : [
3105
+ createPolicyViolation(
3106
+ 'surface',
3107
+ `${directory}/${name}.md`,
3108
+ `surface evidence missing: catalog package ${name} has no hosted guide`,
3109
+ ),
3110
+ ],
2586
3111
  )
2587
- for (const path of globSync('tests/setup*.ts', { cwd: root }).map(normalizePolicyPath).sort()) {
2588
- if (
3112
+ // A guide without a Surface section answers its violation and no names.
3113
+ const guides = globSync('guides/*.md', { cwd: host })
3114
+ .map(normalizePolicyPath)
3115
+ .sort()
3116
+ .flatMap(
3117
+ (
3118
+ path,
3119
+ ): ReadonlyArray<{
3120
+ readonly owner: string
3121
+ readonly symbols: readonly string[]
3122
+ readonly violation?: PolicyViolation
3123
+ }> => {
3124
+ const owner = basename(path, '.md')
3125
+ if (owner === POLICY_GUIDE_MAP) return []
3126
+ const guide = createGuide(readFileSync(join(host, path), 'utf8'))
3127
+ return guide.sections().includes('Surface')
3128
+ ? [{ owner, symbols: guide.surface().map((symbol) => symbol.name) }]
3129
+ : [
3130
+ {
3131
+ owner,
3132
+ symbols: [],
3133
+ violation: createPolicyViolation(
3134
+ 'surface',
3135
+ `${directory}/${owner}.md`,
3136
+ 'surface evidence missing: hosted guide has no Surface section',
3137
+ ),
3138
+ },
3139
+ ]
3140
+ },
3141
+ )
3142
+ const grandfather = new Set(
3143
+ guides.filter((guide) => guide.owner === own).flatMap(({ symbols }) => symbols),
3144
+ )
3145
+ const claimed = Map.groupBy(
3146
+ guides
3147
+ .filter((guide) => guide.owner !== own)
3148
+ .flatMap(({ owner, symbols }) => symbols.map((name) => ({ name, owner }))),
3149
+ ({ name }) => name,
3150
+ )
3151
+ const population = readPolicySurface(root)
3152
+ const setups = globSync('tests/setup*.ts', { cwd: root })
3153
+ .map(normalizePolicyPath)
3154
+ .sort()
3155
+ .flatMap((path) =>
2589
3156
  path.endsWith('.test.ts') ||
2590
3157
  HOST_PATHS.some(
2591
3158
  (vendored) =>
2592
3159
  path === normalizePolicyPath(vendored) ||
2593
3160
  path.startsWith(`${normalizePolicyPath(vendored)}/`),
2594
3161
  )
3162
+ ? []
3163
+ : [collectPolicyDeclarations(root, path, readFileSync(join(root, path), 'utf8'))],
2595
3164
  )
2596
- continue
2597
- const read = collectPolicyDeclarations(root, path, readFileSync(join(root, path), 'utf8'))
2598
- declarations.push(...read.declarations)
2599
- if (read.violation !== undefined) violations.push(read.violation)
2600
- }
2601
- const seen = new Set<string>()
2602
- for (const declaration of declarations) {
2603
- for (const owner of owners.get(declaration.name) ?? []) {
2604
- const key = `${declaration.path}\n${declaration.name}\n${owner}`
2605
- if (seen.has(key)) continue
2606
- seen.add(key)
2607
- violations.push(
3165
+ const declarations = [
3166
+ ...population.declarations.filter((declaration) => !grandfather.has(declaration.name)),
3167
+ ...setups.flatMap((read) => read.declarations),
3168
+ ]
3169
+ const claims = declarations.flatMap((declaration) =>
3170
+ [...new Set(claimed.get(declaration.name)?.map(({ owner }) => owner))].map((owner) => ({
3171
+ declaration,
3172
+ owner,
3173
+ key: `${declaration.path}\n${declaration.name}\n${owner}`,
3174
+ })),
3175
+ )
3176
+ const violations = [
3177
+ ...missing,
3178
+ ...guides.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3179
+ ...population.violations,
3180
+ ...setups.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
3181
+ ...claims
3182
+ .filter((claim, index) => claims.findIndex((other) => other.key === claim.key) === index)
3183
+ .map(({ declaration, owner }) =>
2608
3184
  createPolicyViolation(
2609
3185
  'surface',
2610
3186
  declaration.path,
2611
3187
  `surface name belongs to one package: ${declaration.name} (${owner})`,
2612
3188
  declaration.line,
2613
3189
  ),
2614
- )
2615
- }
2616
- }
3190
+ ),
3191
+ ]
2617
3192
  return violations.sort((first, second) => {
2618
3193
  if (first.path !== second.path) return first.path < second.path ? -1 : 1
2619
3194
  if (first.line !== second.line) return (first.line ?? 0) - (second.line ?? 0)
@@ -3890,6 +4465,41 @@ export function collectSheets(directory: string): readonly string[] {
3890
4465
  .sort()
3891
4466
  }
3892
4467
 
4468
+ /**
4469
+ * Collects the study tests a workspace holds beneath `tests/studies`.
4470
+ * @param directory - The workspace root.
4471
+ * @returns Sorted workspace-relative paths of the physical `*.test.ts` files, nested or direct,
4472
+ * beneath a `tests/studies` directory whose stored name has exactly that case, outside any
4473
+ * `node_modules` or `.git` directory, which Vitest's default exclude never collects; empty when
4474
+ * the directory is absent, is not a directory, or holds none.
4475
+ */
4476
+ export function collectStudies(directory: string): readonly string[] {
4477
+ const tests = resolve(directory, 'tests')
4478
+ if (!existsSync(tests)) return []
4479
+ // A directory entry carries the name the host stores, so matching it keeps the selection exact-case
4480
+ // on a host that folds case, and its type excludes a link without following it.
4481
+ const held = readdirSync(tests, { withFileTypes: true })
4482
+ if (!held.some((entry) => entry.name === 'studies' && entry.isDirectory())) return []
4483
+ const studies: string[] = []
4484
+ const pending = ['tests/studies']
4485
+ while (pending.length > 0) {
4486
+ const current = pending.pop() ?? ''
4487
+ for (const entry of readdirSync(resolve(directory, current), { withFileTypes: true })) {
4488
+ const path = `${current}/${entry.name}`
4489
+ // Vitest's default exclude collects nothing beneath these directories.
4490
+ if (entry.isDirectory()) {
4491
+ if (entry.name !== 'node_modules' && entry.name !== '.git') pending.push(path)
4492
+ } else if (
4493
+ entry.isFile() &&
4494
+ entry.name.endsWith('.test.ts') &&
4495
+ lstatSync(resolve(directory, path)).nlink === 1
4496
+ )
4497
+ studies.push(path)
4498
+ }
4499
+ }
4500
+ return studies.sort()
4501
+ }
4502
+
3893
4503
  /**
3894
4504
  * Collects occupied framework faces from the source and application trees.
3895
4505
  * @param directory - The workspace root.
@@ -3954,20 +4564,51 @@ export function collectFaceWrappers(directory: string): readonly string[] {
3954
4564
  export function inspectSheetConfiguration(project: unknown, script?: string): readonly string[] {
3955
4565
  const test = readConfigRecord(readConfigRecord(project).test)
3956
4566
  const setup = test.setupFiles
3957
- const failures: string[] = []
3958
- for (const path of ['./tests/setup.ts', './tests/setupBrowser.ts', './tests/setupStyles.ts'])
3959
- if (!Array.isArray(setup) || !setup.includes(path)) failures.push(path)
3960
- if (test.isolate !== false) failures.push('isolate')
3961
- if (
3962
- script !== undefined &&
4567
+ return [
4568
+ ...['./tests/setup.ts', './tests/setupBrowser.ts', './tests/setupStyles.ts'].filter(
4569
+ (path) => !Array.isArray(setup) || !setup.includes(path),
4570
+ ),
4571
+ ...(test.isolate !== false ? ['isolate'] : []),
4572
+ ...(script !== undefined &&
3963
4573
  (!script.includes('vite.styles.config.ts') ||
3964
4574
  !script.includes('vite.themes.config.ts') ||
3965
4575
  script.indexOf('vite.styles.config.ts') > script.indexOf('vite.themes.config.ts'))
3966
- )
3967
- failures.push('themes order')
3968
- return failures
4576
+ ? ['themes order']
4577
+ : []),
4578
+ ]
3969
4579
  }
3970
4580
 
4581
+ /** Lists artifact-loader paths and the computed ESM imports each boundary admits. */
4582
+ export const ARTIFACT_IMPORT_CASES: ReadonlyArray<readonly [string, boolean]> = Object.freeze([
4583
+ ['src/server/artifacts/Artifacts.ts', true],
4584
+ ['src/server/artifacts/nested/inspect.mts', true],
4585
+ ['src/server/artifacts/inspect.js', true],
4586
+ ['src/server/artifacts/inspect.mjs', true],
4587
+ ['src/server/artifacts-lookalike/inspect.ts', false],
4588
+ ['src/browser/artifacts/inspect.ts', false],
4589
+ ['src/core/artifacts/inspect.ts', false],
4590
+ ['app/server/artifacts/inspect.ts', false],
4591
+ ['src/server/artifacts/inspect.cts', false],
4592
+ ['src/server/artifacts/inspect.cjs', false],
4593
+ ['tools/artifacts/inspect.ts', true],
4594
+ ['tools/artifacts/nested/inspect.mts', true],
4595
+ ['tools/artifacts/inspect.js', true],
4596
+ ['tools/artifacts/inspect.mjs', true],
4597
+ ['.orkestrel/sample/instruments/tools/artifacts/inspect.ts', true],
4598
+ ['.orkestrel/sample/instruments/harness/tools/artifacts/nested/inspect.mts', true],
4599
+ ['.orkestrel/sample/instruments/harness/tools/artifacts/inspect.js', true],
4600
+ ['.orkestrel/sample/instruments/harness/tools/artifacts/inspect.mjs', true],
4601
+ ['src/server/inspect.ts', false],
4602
+ ['src/server/tools/artifacts/inspect.ts', false],
4603
+ ['configs/inspect.ts', false],
4604
+ ['scripts/inspect.ts', false],
4605
+ ['.orkestrel/sample/instruments/harness/inspect.ts', false],
4606
+ ['tools/artifacts-lookalike/inspect.ts', false],
4607
+ ['.orkestrel/sample/instruments/harness/tools/artifacts-lookalike/inspect.ts', false],
4608
+ ['tools/artifacts/inspect.cts', false],
4609
+ ['tools/artifacts/inspect.cjs', false],
4610
+ ])
4611
+
3971
4612
  /**
3972
4613
  * Reads import restrictions and collection sentinels from the installed linter.
3973
4614
  * @param directory - The fixture workspace containing the lint configuration.
@@ -4003,18 +4644,92 @@ export function readImportDiagnostics(
4003
4644
  )
4004
4645
  const diagnostics = readConfigRecord(JSON.parse(result.stdout)).diagnostics
4005
4646
  if (!Array.isArray(diagnostics)) throw new Error('Missing lint diagnostics')
4006
- const codes = new Set<string>()
4007
- for (const value of diagnostics) {
4008
- const diagnostic = readConfigRecord(value)
4009
- if (typeof diagnostic.code !== 'string' || typeof diagnostic.filename !== 'string')
4010
- throw new Error('Missing diagnostic location')
4011
- codes.add(
4012
- diagnostic.code +
4647
+ const codes = new Set(
4648
+ diagnostics.map((value: unknown) => {
4649
+ const diagnostic = readConfigRecord(value)
4650
+ if (typeof diagnostic.code !== 'string' || typeof diagnostic.filename !== 'string')
4651
+ throw new Error('Missing diagnostic location')
4652
+ return (
4653
+ diagnostic.code +
4013
4654
  ' ' +
4014
- normalizePolicyFilename(realpathSync.native(directory), diagnostic.filename),
4015
- )
4016
- }
4655
+ normalizePolicyFilename(realpathSync.native(directory), diagnostic.filename)
4656
+ )
4657
+ }),
4658
+ )
4017
4659
  for (const path of paths)
4018
4660
  if (!codes.has('eslint(no-debugger) ' + path)) throw new Error(`Uncollected fixture ${path}`)
4019
4661
  return codes
4020
4662
  }
4663
+
4664
+ /**
4665
+ * Reports whether a path is a physical directory, never a link to one.
4666
+ *
4667
+ * @remarks
4668
+ * Reads the same two facts `isPhysicalDirectory` reads off an `lstat` and returns false for every
4669
+ * inspection error, because this module is vendored byte-identical and imports nothing from the
4670
+ * package.
4671
+ *
4672
+ * @param path - The absolute path to inspect.
4673
+ * @returns True if the path is a directory and not a symbolic link; false otherwise, an unreadable
4674
+ * path included.
4675
+ */
4676
+ export function isPolicyDirectory(path: string): boolean {
4677
+ try {
4678
+ const entry = lstatSync(path)
4679
+ return entry.isDirectory() && !entry.isSymbolicLink()
4680
+ } catch {
4681
+ return false
4682
+ }
4683
+ }
4684
+
4685
+ /**
4686
+ * Resolves a module specifier from this module without throwing.
4687
+ *
4688
+ * @param specifier - The package or subpath specifier to resolve.
4689
+ * @returns The resolved file path, or `undefined` when the workspace installs no such package.
4690
+ */
4691
+ export function resolvePolicyPackage(specifier: string): string | undefined {
4692
+ try {
4693
+ return createRequire(import.meta.url).resolve(specifier)
4694
+ } catch {
4695
+ return undefined
4696
+ }
4697
+ }
4698
+
4699
+ /** Names the workspace root, the directory above `tests/`, that the vendored proofs inspect. */
4700
+ export const POLICY_ROOT: string = resolve(dirname(fileURLToPath(import.meta.url)), '..')
4701
+
4702
+ /**
4703
+ * Holds whether the workspace publishes source from `src`; false for one with application environments alone.
4704
+ *
4705
+ * @remarks
4706
+ * The `src` axis a declaration roll-up reads, derived from a recognized source environment that is
4707
+ * a physical directory rather than from a `src` entry of any shape. A regular file, an empty
4708
+ * directory, a directory holding no recognized environment, a symbolic link, and an entry this host
4709
+ * refuses to inspect each leave a workspace publishing nothing, and reading the entry alone would
4710
+ * put every one of them under the face proof. The inspection is caught rather than excused by
4711
+ * option: `throwIfNoEntry` excuses an absent path alone and rethrows a permission refusal, and a
4712
+ * throw at module scope is a collection error that takes a whole file down in that target rather
4713
+ * than failing one case. This is the derivation `targetToEnvironments` uses, which filters the
4714
+ * environment names by a contained physical directory, and it answers the same question
4715
+ * `blueprintToScripts` answers from a blueprint's own `src` list. A workspace declaring app
4716
+ * environments alone publishes no library, so
4717
+ * it vendors no `configs/src/` face wrapper and has no roll-up to measure. A face proof is
4718
+ * conditioned on this axis rather than on a face wrapper: a workspace that declares the axis and
4719
+ * vendors no wrapper carries the defect such a proof reports, so that workspace must still fail the
4720
+ * proof. The manifest cases read the same fact from their own `private` flag and name it
4721
+ * `manifestPublishes` for the source that decides it there; this reads the axis on disk, so a
4722
+ * workspace whose manifest and shape disagree fails one of the two rather than neither.
4723
+ */
4724
+ export const POLICY_PUBLISHES: boolean = ['core', 'browser', 'server'].some((environment) =>
4725
+ isPolicyDirectory(resolve(POLICY_ROOT, 'src', environment)),
4726
+ )
4727
+
4728
+ /**
4729
+ * Holds the declaration extractor's entry path, or `undefined` where the workspace installs none.
4730
+ *
4731
+ * @remarks
4732
+ * A declaration roll-up loads the extractor package, which only a workspace publishing source from
4733
+ * `src` installs. The resolution is the mechanism its proof is conditioned on.
4734
+ */
4735
+ export const POLICY_EXTRACTOR: string | undefined = resolvePolicyPackage('@microsoft/api-extractor')