@orkestrel/scaffold 0.0.107 → 0.0.109

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 (164) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +368 -27
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +26 -20
  25. package/dist/host/agents/orchestration.md +98 -58
  26. package/dist/host/agents/platforms/posix.md +15 -0
  27. package/dist/host/agents/platforms/windows.md +37 -0
  28. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  29. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  30. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  31. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  32. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  33. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  34. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  35. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  36. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  37. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +12 -14
  38. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
  39. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  48. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +428 -27
  49. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  50. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  51. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  52. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  53. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  54. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  55. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  56. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  58. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  59. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  60. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  63. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  64. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  65. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  66. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  67. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  68. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  69. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +91 -30
  70. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
  71. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
  72. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
  73. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
  74. package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
  75. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  76. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  77. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  78. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  79. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  80. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  81. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  82. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -10
  83. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  86. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  87. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  88. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  89. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  90. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  91. package/dist/host/agents/templates/brief.md +1 -1
  92. package/dist/host/agents/transports/claude-cli.md +3 -3
  93. package/dist/host/agents/transports/codex.md +3 -8
  94. package/dist/host/agents/transports/cursor.md +2 -3
  95. package/dist/host/claude/AGENTS.md +7 -10
  96. package/dist/host/claude/agents/analyst.md +3 -3
  97. package/dist/host/claude/agents/astra.md +3 -3
  98. package/dist/host/claude/agents/builder.md +1 -1
  99. package/dist/host/claude/agents/checker.md +1 -1
  100. package/dist/host/claude/agents/distiller.md +1 -1
  101. package/dist/host/claude/agents/grok.md +1 -1
  102. package/dist/host/claude/agents/opus.md +2 -2
  103. package/dist/host/claude/agents/orkestrel.md +57 -90
  104. package/dist/host/claude/agents/researcher.md +1 -1
  105. package/dist/host/claude/agents/reviewer.md +4 -0
  106. package/dist/host/claude/agents/scout.md +1 -1
  107. package/dist/host/claude/agents/verifier.md +1 -1
  108. package/dist/host/claude/rules/application.md +14 -21
  109. package/dist/host/claude/rules/architecture.md +34 -44
  110. package/dist/host/claude/rules/browser.md +3 -4
  111. package/dist/host/claude/rules/campaign.md +55 -0
  112. package/dist/host/claude/rules/documentation.md +26 -11
  113. package/dist/host/claude/rules/names.md +9 -4
  114. package/dist/host/claude/rules/patterns.md +6 -7
  115. package/dist/host/claude/rules/portability.md +2 -3
  116. package/dist/host/claude/rules/quality.md +4 -5
  117. package/dist/host/claude/rules/styles.md +2 -2
  118. package/dist/host/claude/rules/tests.md +57 -114
  119. package/dist/host/claude/rules/typescript.md +8 -10
  120. package/dist/host/claude/rules/workspace.md +21 -19
  121. package/dist/host/claude/rules/writing.md +3 -3
  122. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  123. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  124. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +1 -1
  125. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  126. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  127. package/dist/host/codex/agents/analyst.toml +9 -3
  128. package/dist/host/codex/agents/astra.toml +3 -4
  129. package/dist/host/codex/agents/builder.toml +1 -1
  130. package/dist/host/codex/agents/opus.toml +3 -4
  131. package/dist/host/codex/agents/orkestrel.toml +1 -1
  132. package/dist/host/codex/agents/researcher.toml +2 -2
  133. package/dist/host/codex/agents/verifier.toml +1 -1
  134. package/dist/host/codex/config.toml +3 -3
  135. package/dist/host/configs/helpers.ts +21 -1
  136. package/dist/host/configs/policy.ts +405 -16
  137. package/dist/host/cursor/rules/orchestration.mdc +1 -0
  138. package/dist/host/dotfiles/oxlintrc.json +141 -5
  139. package/dist/host/guides/agent.md +111 -134
  140. package/dist/host/guides/capture.md +801 -0
  141. package/dist/host/guides/probe.md +27 -8
  142. package/dist/host/guides/scaffold.md +152 -19
  143. package/dist/host/guides/study.md +534 -440
  144. package/dist/host/guides/test.md +111 -44
  145. package/dist/host/manifest.json +199 -242
  146. package/dist/host/tests/config.test.ts +1558 -219
  147. package/dist/host/tests/policy.test.ts +127 -1
  148. package/dist/host/tests/setupPolicy.ts +1389 -39
  149. package/dist/src/core/index.cjs +239 -87
  150. package/dist/src/core/index.cjs.map +1 -1
  151. package/dist/src/core/index.d.cts +123 -39
  152. package/dist/src/core/index.d.ts +123 -39
  153. package/dist/src/core/index.js +234 -86
  154. package/dist/src/core/index.js.map +1 -1
  155. package/dist/src/server/index.cjs +20 -27
  156. package/dist/src/server/index.cjs.map +1 -1
  157. package/dist/src/server/index.js +21 -28
  158. package/dist/src/server/index.js.map +1 -1
  159. package/package.json +11 -11
  160. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  161. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  162. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  163. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
  164. package/dist/host/guides/supervisor.md +0 -4890
@@ -1,4 +1,11 @@
1
1
  import type { Blueprint } from '@orkestrel/scaffold'
2
+ import type {
3
+ PolicyComment,
4
+ PolicyContext,
5
+ PolicyDiagnostic,
6
+ PolicyExpression,
7
+ PolicyScopeCode,
8
+ } from '../configs/policy.js'
2
9
  import {
3
10
  createGuide,
4
11
  extractSourceLines,
@@ -25,6 +32,7 @@ import {
25
32
  import { tmpdir } from 'node:os'
26
33
  import { basename, dirname, join, matchesGlob, relative as relativePath, resolve } from 'node:path'
27
34
  import { fileURLToPath } from 'node:url'
35
+ import { isDeepStrictEqual } from 'node:util'
28
36
  import { parseAst, parseSync } from 'vite'
29
37
  import { stripPolicyCode, textToPolicyHits } from '../configs/policy.js'
30
38
 
@@ -38,6 +46,7 @@ export type PolicyRule =
38
46
  | 'skill'
39
47
  | 'surface'
40
48
  | 'suppression'
49
+ | 'text'
41
50
 
42
51
  /** Describes one workspace file a physical control writes. */
43
52
  export interface PolicySource {
@@ -45,6 +54,18 @@ export interface PolicySource {
45
54
  readonly content: string
46
55
  }
47
56
 
57
+ /** Pairs a collected Vue import fixture with its native and independent restriction outcomes. */
58
+ export interface PolicyVueImportCase extends PolicySource {
59
+ readonly boundary: boolean
60
+ readonly restricted: boolean
61
+ }
62
+
63
+ /** Pairs a collected composable fixture with its placement and domain outcomes. */
64
+ export interface PolicyVueDomainCase extends PolicySource {
65
+ readonly misplaced: boolean
66
+ readonly malformed: boolean
67
+ }
68
+
48
69
  /** Describes one policy failure the sweep reports. */
49
70
  export interface PolicyViolation {
50
71
  readonly rule: PolicyRule
@@ -418,6 +439,72 @@ export function createPolicyRevoked(): object {
418
439
  return proxy
419
440
  }
420
441
 
442
+ /**
443
+ * Creates a producer of revoked proxies over what another producer creates.
444
+ *
445
+ * @param entry - The producer whose value each proxy wraps.
446
+ * @returns A producer whose value refuses every read, which fails a reader once it holds the entry.
447
+ */
448
+ export function createPolicyRefused<T extends object>(entry: () => T): () => T {
449
+ return () => {
450
+ const { proxy, revoke } = Proxy.revocable(entry(), {})
451
+ revoke()
452
+ return proxy
453
+ }
454
+ }
455
+
456
+ /** Creates a lint source over text and its comments that declares no variable. */
457
+ export function createPolicySource(
458
+ text: string,
459
+ comments: readonly PolicyComment[] = [],
460
+ ): PolicyScopeCode {
461
+ return { text, getAllComments: () => comments, getDeclaredVariables: () => [] }
462
+ }
463
+
464
+ /** Creates a lint context over an empty source that appends each diagnostic to the given log. */
465
+ export function createPolicyContext(
466
+ filename: string,
467
+ cwd: string,
468
+ reports: PolicyDiagnostic[],
469
+ ): PolicyContext {
470
+ return {
471
+ filename,
472
+ cwd,
473
+ sourceCode: createPolicySource(''),
474
+ report: (diagnostic) => {
475
+ reports.push(diagnostic)
476
+ },
477
+ }
478
+ }
479
+
480
+ /** Creates a variable declarator named `run` whose value is an arrow function. */
481
+ export function createPolicyDeclarator(): PolicyExpression {
482
+ return {
483
+ type: 'VariableDeclarator',
484
+ range: [0, 0],
485
+ id: { type: 'Identifier', range: [0, 0], name: 'run' },
486
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
487
+ }
488
+ }
489
+
490
+ /** Creates a named export of a function declaration named `run`. */
491
+ export function createPolicyExport(): PolicyExpression {
492
+ return {
493
+ type: 'ExportNamedDeclaration',
494
+ range: [47, 63],
495
+ declaration: {
496
+ type: 'FunctionDeclaration',
497
+ range: [0, 0],
498
+ id: { type: 'Identifier', range: [0, 0], name: 'run' },
499
+ },
500
+ }
501
+ }
502
+
503
+ /** Creates the one-sentence block comment a doc reader reads above an export. */
504
+ export function createPolicyComment(): PolicyComment {
505
+ return { type: 'Block', value: '* One.', range: [0, 11] }
506
+ }
507
+
421
508
  /**
422
509
  * Creates one case per list shape for a reader of a caller-supplied list.
423
510
  *
@@ -550,7 +637,7 @@ export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
550
637
  'inspectPolicyConfiguration overrides',
551
638
  (list: unknown[]) =>
552
639
  inspectPolicyConfiguration({
553
- rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
640
+ rules: POLICY_WIRING_SETTINGS,
554
641
  overrides: list,
555
642
  }),
556
643
  { valid: () => ({}), refusal: createPolicyRevoked },
@@ -569,7 +656,7 @@ export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
569
656
  'inspectPolicyConfiguration ignorePatterns',
570
657
  (list: unknown[]) =>
571
658
  inspectPolicyConfiguration({
572
- rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
659
+ rules: POLICY_WIRING_SETTINGS,
573
660
  ignorePatterns: list,
574
661
  }),
575
662
  // The reader skips every entry that is not a string, so no entry refuses it and no row uses the
@@ -812,30 +899,67 @@ export const POLICY_SUPPRESSION_DIRECTIVE = ['oxlint', '-disable'].join('')
812
899
  /** Names the formatter directive the text sweep refuses, composed so the instrument does not report itself. */
813
900
  export const POLICY_FORMATTER_DIRECTIVE = ['prettier', '-ignore'].join('')
814
901
 
815
- /** Matches the source, test, config, script, and sheet files inspected for lint and formatter suppression directives. */
902
+ /** Matches the source, test, config, script, skill script, and sheet files inspected for lint and formatter suppression directives. */
816
903
  export const POLICY_SUPPRESSION_GLOB: readonly string[] = Object.freeze([
817
904
  '{src,app,tests,configs,scripts}/**/*.{cjs,css,cts,js,jsx,mjs,mts,scss,ts,tsx,vue}',
818
905
  '*.{cjs,css,cts,js,jsx,mjs,mts,scss,ts,tsx,vue}',
906
+ '.agents/skills/*/scripts/*.ts',
819
907
  ])
820
908
 
821
- /** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
822
- export const POLICY_WIRING_RULES: readonly string[] = Object.freeze([
823
- 'policy/no-mocking',
824
- 'policy/no-keyword-privacy',
825
- 'policy/no-malformed-summary',
826
- 'policy/no-banned-term',
827
- 'typescript/parameter-properties',
828
- 'typescript/explicit-member-accessibility',
909
+ /**
910
+ * Matches the instruction, configuration, and root text files the text sweep reads beside the
911
+ * authored Markdown and the suppression population.
912
+ */
913
+ export const POLICY_TEXT_GLOB: readonly string[] = Object.freeze([
914
+ '{.agents,.claude,.codex,.cursor}/**/*.{toml,yaml,yml,mdc,json}',
915
+ '*.json',
916
+ '.*.json',
917
+ '.gitattributes',
918
+ '.prettierignore',
829
919
  ])
830
920
 
921
+ /**
922
+ * Maps each rule whose workspace-wide lint wiring must not be weakened to the whole top-level
923
+ * setting the shipped configuration declares for it.
924
+ *
925
+ * @remarks
926
+ * A setting carries its options as well as its severity, so a rule listed at `error` with an option
927
+ * that switches its check off differs from the value declared here.
928
+ */
929
+ export const POLICY_WIRING_SETTINGS: Readonly<Record<string, unknown>> = Object.freeze({
930
+ 'policy/no-mocking': 'error',
931
+ 'policy/no-keyword-privacy': 'error',
932
+ 'policy/no-let': 'error',
933
+ 'policy/no-malformed-summary': 'error',
934
+ 'policy/no-banned-term': 'error',
935
+ 'no-var': 'error',
936
+ 'prefer-const': 'error',
937
+ 'no-param-reassign': 'error',
938
+ 'no-ex-assign': 'error',
939
+ 'vitest/no-identical-title': 'error',
940
+ 'vitest/no-focused-tests': 'error',
941
+ 'vitest/no-disabled-tests': 'error',
942
+ 'typescript/parameter-properties': 'error',
943
+ 'typescript/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
944
+ })
945
+
946
+ /** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
947
+ export const POLICY_WIRING_RULES: readonly string[] = Object.freeze(
948
+ Object.keys(POLICY_WIRING_SETTINGS),
949
+ )
950
+
831
951
  /** Lists the linted workspace roots that ignore patterns must not reach. */
832
952
  export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
833
953
  'src',
834
954
  'app',
835
955
  'tests',
836
956
  'configs',
957
+ '.agents',
837
958
  ])
838
959
 
960
+ /** Matches a character that makes a path segment a glob rather than a literal name. */
961
+ export const POLICY_GLOB_CHARACTER = /[*?[\]{}]/u
962
+
839
963
  /** Matches either lint suppression token or the formatter directive the text sweep refuses. */
840
964
  export const POLICY_SUPPRESSION_PATTERN = new RegExp(
841
965
  [['eslint', '-disable'].join(''), POLICY_SUPPRESSION_DIRECTIVE, POLICY_FORMATTER_DIRECTIVE].join(
@@ -844,6 +968,24 @@ export const POLICY_SUPPRESSION_PATTERN = new RegExp(
844
968
  'u',
845
969
  )
846
970
 
971
+ /**
972
+ * Holds the decoder the text sweep reads bytes through.
973
+ *
974
+ * @remarks
975
+ * `fatal` refuses a byte sequence UTF-8 does not carry, and `ignoreBOM` keeps a leading byte-order
976
+ * mark in the decoded text, where the default would drop it before the sweep could see it.
977
+ */
978
+ export const POLICY_TEXT_DECODER = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
979
+
980
+ /** Lists the control code points a text file may carry: tab, line feed, and carriage return. */
981
+ export const POLICY_TEXT_CONTROLS: readonly number[] = Object.freeze([0x09, 0x0a, 0x0d])
982
+
983
+ /** Holds the code point of the replacement character a failed decode leaves behind. */
984
+ export const POLICY_TEXT_REPLACEMENT = 0xfffd
985
+
986
+ /** Holds the code point of the byte-order mark. */
987
+ export const POLICY_TEXT_MARK = 0xfeff
988
+
847
989
  /**
848
990
  * Matches the workspace-authored path population inspected for a name a Windows checkout cannot
849
991
  * hold.
@@ -887,6 +1029,19 @@ export const POLICY_RESERVED_PATTERN = /[<>:"|?*]/u
887
1029
  /** Matches a shell script named as a complete path token inside a manifest script. */
888
1030
  export const POLICY_SHELL_PATTERN = /\.sh\b/u
889
1031
 
1032
+ /** Lists the extensions of the shell, PowerShell, Python, and batch files a workspace keeps under one directory. */
1033
+ export const POLICY_SHELL_EXTENSIONS: readonly string[] = Object.freeze([
1034
+ '.sh',
1035
+ '.bash',
1036
+ '.ps1',
1037
+ '.py',
1038
+ '.bat',
1039
+ '.cmd',
1040
+ ])
1041
+
1042
+ /** Names the one directory whose files are Cloud setup hooks, the shell files a workspace admits. */
1043
+ export const POLICY_SHELL_ROOT = 'scripts'
1044
+
890
1045
  /** Names the directory whose direct Markdown files form the complete rule family. */
891
1046
  export const POLICY_RULE_ROOT = '.claude/rules'
892
1047
 
@@ -1190,6 +1345,165 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
1190
1345
  )
1191
1346
  }
1192
1347
 
1348
+ /**
1349
+ * Decodes file bytes as UTF-8 without throwing.
1350
+ *
1351
+ * @param bytes - The raw file bytes.
1352
+ * @returns The decoded text with any leading byte-order mark kept, or `undefined` when the bytes
1353
+ * are not valid UTF-8.
1354
+ */
1355
+ export function parsePolicyText(bytes: Uint8Array): string | undefined {
1356
+ try {
1357
+ return POLICY_TEXT_DECODER.decode(bytes)
1358
+ } catch {
1359
+ return undefined
1360
+ }
1361
+ }
1362
+
1363
+ /**
1364
+ * Reports whether one code point is a character the text sweep refuses.
1365
+ *
1366
+ * @remarks
1367
+ * The sweep refuses the replacement character, the byte-order mark, and every C0 or C1 control
1368
+ * character other than the three {@link POLICY_TEXT_CONTROLS} lists.
1369
+ *
1370
+ * @param code - The code point to judge.
1371
+ * @returns True if the code point is refused; false otherwise.
1372
+ */
1373
+ export function matchesPolicyCharacter(code: number): boolean {
1374
+ if (code === POLICY_TEXT_REPLACEMENT || code === POLICY_TEXT_MARK) return true
1375
+ if (code < 0x20) return !POLICY_TEXT_CONTROLS.includes(code)
1376
+ return code >= 0x7f && code <= 0x9f
1377
+ }
1378
+
1379
+ /**
1380
+ * Renders one code point as the `\u` escape a violation names it by.
1381
+ *
1382
+ * @param code - The code point to render.
1383
+ * @returns The escape, four upper-case hexadecimal digits long.
1384
+ */
1385
+ export function renderPolicyCharacter(code: number): string {
1386
+ return `\\u${code.toString(16).toUpperCase().padStart(4, '0')}`
1387
+ }
1388
+
1389
+ /**
1390
+ * Inspects one file's bytes for text that is not clean UTF-8.
1391
+ *
1392
+ * @remarks
1393
+ * Bytes that do not decode report once for the file. Decoded text reports each refused character
1394
+ * once per line, so a line that repeats a character names it once.
1395
+ *
1396
+ * @param path - The workspace-relative path the bytes belong to.
1397
+ * @param bytes - The raw file bytes.
1398
+ * @returns Every text violation in line order.
1399
+ */
1400
+ export function inspectPolicyBytes(path: string, bytes: Uint8Array): readonly PolicyViolation[] {
1401
+ const text = parsePolicyText(bytes)
1402
+ if (text === undefined) return [createPolicyViolation('text', path, 'text decodes as UTF-8')]
1403
+ return text
1404
+ .split(/\r\n|\n/u)
1405
+ .flatMap((line, index) =>
1406
+ [...new Set([...line].map((character) => character.codePointAt(0) ?? 0))]
1407
+ .filter(matchesPolicyCharacter)
1408
+ .map((code) =>
1409
+ createPolicyViolation(
1410
+ 'text',
1411
+ path,
1412
+ `text carries no replacement, byte-order, or control character: ${renderPolicyCharacter(code)}`,
1413
+ index + 1,
1414
+ ),
1415
+ ),
1416
+ )
1417
+ }
1418
+
1419
+ /**
1420
+ * Collects the workspace-relative paths of the authored and source text the sweep reads.
1421
+ *
1422
+ * @param root - The workspace root to read.
1423
+ * @returns The authored Markdown population joined with the source population the suppression
1424
+ * sweep reads and the instruction, configuration, and root text population
1425
+ * {@link POLICY_TEXT_GLOB} names, sorted by path. The three populations share no extension, so no
1426
+ * path repeats.
1427
+ */
1428
+ export function collectPolicyTextPaths(root: string): readonly string[] {
1429
+ return [
1430
+ ...new Set([
1431
+ ...readPolicyProse(root),
1432
+ ...globSync(POLICY_SUPPRESSION_GLOB, { cwd: root }).map(normalizePolicyPath),
1433
+ ...globSync(POLICY_TEXT_GLOB, { cwd: root }).map(normalizePolicyPath),
1434
+ ]),
1435
+ ].sort()
1436
+ }
1437
+
1438
+ /**
1439
+ * Inspects every authored and source text file for text that is not clean UTF-8.
1440
+ *
1441
+ * @param root - The workspace root to inspect.
1442
+ * @returns Every text violation in path and line order.
1443
+ */
1444
+ export function inspectPolicyText(root: string): readonly PolicyViolation[] {
1445
+ return collectPolicyTextPaths(root).flatMap((path) =>
1446
+ inspectPolicyBytes(path, readFileSync(join(root, path))),
1447
+ )
1448
+ }
1449
+
1450
+ /**
1451
+ * Collects the literal path segments the files globs of a configuration's overrides route through.
1452
+ *
1453
+ * @remarks
1454
+ * A segment that carries a glob character is not literal, and neither is an empty or dot segment.
1455
+ *
1456
+ * @param overrides - The parsed `overrides` value of an Oxlint configuration.
1457
+ * @returns Each literal segment, in the order the globs declare them.
1458
+ */
1459
+ export function collectPolicyOverrideSegments(overrides: unknown): readonly string[] {
1460
+ return (Array.isArray(overrides) ? overrides : []).flatMap((override: unknown) => {
1461
+ const files: unknown = isPolicyRecord(override)
1462
+ ? Object.getOwnPropertyDescriptor(override, 'files')?.value
1463
+ : undefined
1464
+ return (Array.isArray(files) ? files : []).flatMap((glob: unknown) =>
1465
+ typeof glob === 'string'
1466
+ ? normalizePolicyPath(glob)
1467
+ .split('/')
1468
+ .filter(
1469
+ (segment) =>
1470
+ segment !== '' &&
1471
+ segment !== '.' &&
1472
+ segment !== '..' &&
1473
+ !POLICY_GLOB_CHARACTER.test(segment),
1474
+ )
1475
+ : [],
1476
+ )
1477
+ })
1478
+ }
1479
+
1480
+ /**
1481
+ * Checks whether an ignore pattern can silence files inside a linted workspace root.
1482
+ *
1483
+ * @remarks
1484
+ * Oxlint reads an ignore pattern with gitignore semantics. A pattern with a slash before its last
1485
+ * character is anchored to the configuration directory, so only its first segment can reach a
1486
+ * root. A pattern without one matches a name at any depth, so it reaches a root when it carries a
1487
+ * glob character or names a segment the overrides route through. A negation reaches nothing.
1488
+ *
1489
+ * @param pattern - One entry of the configuration's `ignorePatterns`.
1490
+ * @param overrides - The parsed `overrides` value of the same configuration.
1491
+ * @returns Whether the pattern can silence a guarded file.
1492
+ */
1493
+ export function isPolicyIgnoreReaching(pattern: string, overrides: unknown): boolean {
1494
+ if (pattern.startsWith('!')) return false
1495
+ const normalized = normalizePolicyPath(pattern)
1496
+ const [first = ''] = normalized.replace(/^\.\//u, '').replace(/^\//u, '').split('/')
1497
+ if (
1498
+ POLICY_WIRING_ROOTS.some((root) => first === root || (first !== '' && matchesGlob(root, first)))
1499
+ ) {
1500
+ return true
1501
+ }
1502
+ const name = normalized.replace(/\/$/u, '')
1503
+ if (name.includes('/')) return false
1504
+ return POLICY_GLOB_CHARACTER.test(name) || collectPolicyOverrideSegments(overrides).includes(name)
1505
+ }
1506
+
1193
1507
  /**
1194
1508
  * Inspects the lint configuration that keeps policy rules active across the workspace.
1195
1509
  *
@@ -1208,29 +1522,23 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
1208
1522
  )?.value
1209
1523
  const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
1210
1524
  return [
1211
- ...POLICY_WIRING_RULES.flatMap((rule) => {
1525
+ ...Object.entries(POLICY_WIRING_SETTINGS).flatMap(([rule, declared]) => {
1212
1526
  const setting = isPolicyRecord(rules)
1213
1527
  ? Object.getOwnPropertyDescriptor(rules, rule)?.value
1214
1528
  : undefined
1215
- const severity = Array.isArray(setting) ? setting[0] : setting
1216
- return severity === 'error' ? [] : [`${rule} must have top-level error severity`]
1529
+ return isDeepStrictEqual(setting, declared)
1530
+ ? []
1531
+ : [`${rule} must have the top-level setting ${JSON.stringify(declared)}`]
1217
1532
  }),
1218
1533
  ...(ignorePatterns !== undefined && !Array.isArray(ignorePatterns)
1219
1534
  ? ['ignorePatterns must be an array when declared']
1220
1535
  : // The mapper runs on each pattern as the iterator yields it, as the loop it replaced did.
1221
1536
  Iterator.from(Array.isArray(ignorePatterns) ? ignorePatterns : [])
1222
- .flatMap((pattern) => {
1223
- if (typeof pattern !== 'string' || pattern.startsWith('!')) return []
1224
- const normalized = normalizePolicyPath(pattern)
1225
- .replace(/^\.\//u, '')
1226
- .replace(/^\//u, '')
1227
- const [first = ''] = normalized.split('/')
1228
- return POLICY_WIRING_ROOTS.some(
1229
- (root) => first === root || (first !== '' && matchesGlob(root, first)),
1230
- )
1537
+ .flatMap((pattern) =>
1538
+ typeof pattern === 'string' && isPolicyIgnoreReaching(pattern, overrides)
1231
1539
  ? [`ignorePatterns must not reach ${pattern}`]
1232
- : []
1233
- })
1540
+ : [],
1541
+ )
1234
1542
  .toArray()),
1235
1543
  ...(overrides !== undefined && !Array.isArray(overrides)
1236
1544
  ? ['overrides must be an array when declared']
@@ -1281,9 +1589,15 @@ export function inspectPolicyWiring(
1281
1589
  return [{ record, declared: Array.isArray(files) ? [files.join(' ')] : [] }]
1282
1590
  })
1283
1591
  .toArray()
1592
+ // A name enables its rule only at error severity: the string, or the first member of an array.
1284
1593
  const enabled = new Set(
1285
1594
  [topRules, ...entries.map(({ record }) => record)].flatMap((record) =>
1286
- isPolicyRecord(record) ? Object.getOwnPropertyNames(record) : [],
1595
+ isPolicyRecord(record)
1596
+ ? Object.getOwnPropertyNames(record).filter((name) => {
1597
+ const setting = Object.getOwnPropertyDescriptor(record, name)?.value
1598
+ return (Array.isArray(setting) ? setting[0] : setting) === 'error'
1599
+ })
1600
+ : [],
1287
1601
  ),
1288
1602
  )
1289
1603
  const declaredPopulations = entries.flatMap(({ declared }) => declared)
@@ -1638,8 +1952,8 @@ export function inspectSkillTemplateTODOs(
1638
1952
  ? { fence: undefined, indexes: state.indexes }
1639
1953
  : state
1640
1954
  }
1641
- const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1642
- if (opening !== undefined) return { fence: opening, indexes: state.indexes }
1955
+ const fence = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1956
+ if (fence !== undefined) return { fence: fence, indexes: state.indexes }
1643
1957
  return { fence: undefined, indexes: [...state.indexes, index] }
1644
1958
  },
1645
1959
  { fence: undefined, indexes: [] },
@@ -2099,6 +2413,74 @@ export function inspectSkillImports(
2099
2413
  })
2100
2414
  }
2101
2415
 
2416
+ /**
2417
+ * Collects every module specifier a syntax tree names, in source order.
2418
+ *
2419
+ * @remarks
2420
+ * A specifier is the literal source of an `import` or `export … from` declaration, or of an
2421
+ * `import()` expression anywhere in the tree. An `import()` whose source is no literal names none.
2422
+ *
2423
+ * @param node - The syntax tree, or any node or list of nodes within it.
2424
+ * @returns Each literal specifier the tree names.
2425
+ */
2426
+ export function collectPolicySpecifiers(node: unknown): readonly string[] {
2427
+ if (Array.isArray(node)) return node.flatMap((child: unknown) => collectPolicySpecifiers(child))
2428
+ if (!isPolicyRecord(node)) return []
2429
+ const type: unknown = Object.getOwnPropertyDescriptor(node, 'type')?.value
2430
+ const source: unknown = Object.getOwnPropertyDescriptor(node, 'source')?.value
2431
+ const value: unknown = isPolicyRecord(source)
2432
+ ? Object.getOwnPropertyDescriptor(source, 'value')?.value
2433
+ : undefined
2434
+ return [
2435
+ ...(typeof value === 'string' &&
2436
+ (type === 'ImportDeclaration' ||
2437
+ type === 'ExportAllDeclaration' ||
2438
+ type === 'ExportNamedDeclaration' ||
2439
+ type === 'ImportExpression')
2440
+ ? [value]
2441
+ : []),
2442
+ ...Object.values(node).flatMap((child: unknown) => collectPolicySpecifiers(child)),
2443
+ ]
2444
+ }
2445
+
2446
+ /**
2447
+ * Inspects one skill script proof for an import of a skill file other than the script it proves.
2448
+ *
2449
+ * @remarks
2450
+ * Each relative `import`, `export … from`, and literal `import()` specifier resolves against the
2451
+ * proof's directory. A script is self-contained and no file but its own proof imports it, so a
2452
+ * proof that reaches another script, or any other file beneath the skill family, shares a helper
2453
+ * the script law places in the test setup modules.
2454
+ *
2455
+ * @param root - The workspace root to inspect.
2456
+ * @param proof - The workspace-relative path of the proof.
2457
+ * @param script - The workspace-relative path of the script the proof mirrors.
2458
+ * @returns One violation per import of another skill file, in source order.
2459
+ */
2460
+ export function inspectSkillProof(
2461
+ root: string,
2462
+ proof: string,
2463
+ script: string,
2464
+ ): readonly PolicyViolation[] {
2465
+ if (!isPolicyFile(root, proof)) return []
2466
+ const directory = dirname(join(root, proof))
2467
+ return collectPolicySpecifiers(parseAst(readFileSync(join(root, proof), 'utf8'), { lang: 'ts' }))
2468
+ .filter((specifier) => specifier.startsWith('.'))
2469
+ .flatMap((specifier) => {
2470
+ const imported = normalizePolicyPath(relativePath(root, resolve(directory, specifier)))
2471
+ return imported.startsWith(`${SKILL_FAMILY_ROOT}/`) &&
2472
+ imported.replace(/\.js$/u, '.ts') !== script
2473
+ ? [
2474
+ createPolicyViolation(
2475
+ 'skill',
2476
+ proof,
2477
+ `skill proof imports no skill file but its own script: ${imported}`,
2478
+ ),
2479
+ ]
2480
+ : []
2481
+ })
2482
+ }
2483
+
2102
2484
  /**
2103
2485
  * Inspects one discovered skill's required files, metadata, token, references, and fenced imports.
2104
2486
  *
@@ -2247,6 +2629,7 @@ export function inspectSkill(
2247
2629
  ...(isPolicyFile(root, proof)
2248
2630
  ? []
2249
2631
  : [createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`)]),
2632
+ ...inspectSkillProof(root, proof, `${base}/${script}`),
2250
2633
  ]
2251
2634
  })
2252
2635
  const foreign = (content === undefined ? [] : extractForeignSkillScripts(content, name)).flatMap(
@@ -2642,6 +3025,53 @@ export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
2642
3025
  )
2643
3026
  }
2644
3027
 
3028
+ /**
3029
+ * Inspects a file path population for a shell, PowerShell, Python, or batch file outside the scripts
3030
+ * directory, and for any file inside it other than a `.sh` file.
3031
+ *
3032
+ * @param paths - The workspace-relative file paths to inspect.
3033
+ * @returns One violation per refused path, in path order.
3034
+ */
3035
+ export function inspectPolicyShellPaths(paths: readonly string[]): readonly PolicyViolation[] {
3036
+ return paths.flatMap((path) => {
3037
+ const lower = normalizePolicyPath(path).toLowerCase()
3038
+ const inside = lower.startsWith(`${POLICY_SHELL_ROOT}/`)
3039
+ const shell = POLICY_SHELL_EXTENSIONS.some((extension) => lower.endsWith(extension))
3040
+ if (inside ? lower.endsWith('.sh') : !shell) return []
3041
+ return [
3042
+ createPolicyViolation(
3043
+ 'portability',
3044
+ path,
3045
+ inside
3046
+ ? `${POLICY_SHELL_ROOT} holds .sh files and nothing else`
3047
+ : `shell, PowerShell, Python, and batch files sit only in ${POLICY_SHELL_ROOT}`,
3048
+ ),
3049
+ ]
3050
+ })
3051
+ }
3052
+
3053
+ /**
3054
+ * Inspects the workspace-authored file population for a script no Windows host runs.
3055
+ *
3056
+ * @remarks
3057
+ * Only a path that could be refused is checked against the file system: a directory named like a
3058
+ * shell file, or a directory inside the scripts directory, is no file and is left to the other
3059
+ * sweeps.
3060
+ *
3061
+ * @param root - The workspace root to inspect.
3062
+ * @returns Every shell-file violation in path order.
3063
+ */
3064
+ export function inspectPolicyShells(root: string): readonly PolicyViolation[] {
3065
+ const candidates = readPolicyPaths(root).filter((path) => {
3066
+ const lower = path.toLowerCase()
3067
+ return (
3068
+ lower.startsWith(`${POLICY_SHELL_ROOT}/`) ||
3069
+ POLICY_SHELL_EXTENSIONS.some((extension) => lower.endsWith(extension))
3070
+ )
3071
+ })
3072
+ return inspectPolicyShellPaths(candidates.filter((path) => isPolicyFile(root, path)))
3073
+ }
3074
+
2645
3075
  /**
2646
3076
  * Reads every authored Markdown path in one workspace, sorted by path.
2647
3077
  *
@@ -2830,12 +3260,13 @@ export function readPolicyTerms(content: string): readonly string[] {
2830
3260
  * Inspects every host portability rule across one workspace.
2831
3261
  *
2832
3262
  * @param root - The workspace root to inspect.
2833
- * @returns Every rule-map, filename, and manifest-script violation.
3263
+ * @returns Every rule-map, filename, shell-file, and manifest-script violation.
2834
3264
  */
2835
3265
  export function inspectPolicyPortability(root: string): readonly PolicyViolation[] {
2836
3266
  return [
2837
3267
  ...inspectPolicyRuleMap(root),
2838
3268
  ...inspectPolicyFilenames(root),
3269
+ ...inspectPolicyShells(root),
2839
3270
  ...inspectPolicyScripts(root),
2840
3271
  ]
2841
3272
  }
@@ -3010,7 +3441,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3010
3441
  : [createPolicyViolation('surface', path, POLICY_SURFACE_STYLES_MESSAGE, line)]
3011
3442
  })
3012
3443
  // Each barrel statement answers one violation, one star-export target, or neither.
3013
- const readings = barrels.flatMap(
3444
+ const results = barrels.flatMap(
3014
3445
  (path): ReadonlyArray<{ readonly violation?: PolicyViolation; readonly target?: string }> => {
3015
3446
  const text = files[path]
3016
3447
  if (text === undefined) return []
@@ -3067,7 +3498,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3067
3498
  })
3068
3499
  },
3069
3500
  )
3070
- const targets = new Set(readings.flatMap(({ target }) => (target === undefined ? [] : [target])))
3501
+ const targets = new Set(results.flatMap(({ target }) => (target === undefined ? [] : [target])))
3071
3502
  const reads = [...targets].flatMap((path) => {
3072
3503
  const text = files[path]
3073
3504
  return text === undefined ? [] : [collectPolicyDeclarations(root, path, text)]
@@ -3076,7 +3507,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3076
3507
  declarations: reads.flatMap((read) => read.declarations),
3077
3508
  violations: [
3078
3509
  ...entryViolations,
3079
- ...readings.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3510
+ ...results.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3080
3511
  ...reads.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
3081
3512
  ],
3082
3513
  }
@@ -3209,12 +3640,14 @@ export function inspectPolicySurface(root: string): readonly PolicyViolation[] {
3209
3640
  * Inspects every policy rule across one workspace.
3210
3641
  *
3211
3642
  * @param root - The workspace root to inspect.
3212
- * @returns Every mirror, suppression, skill, bridge, portability, prose, and surface violation.
3643
+ * @returns Every mirror, suppression, text, skill, bridge, portability, prose, and surface
3644
+ * violation.
3213
3645
  */
3214
3646
  export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
3215
3647
  return [
3216
3648
  ...inspectPolicyMirrors(root),
3217
3649
  ...inspectPolicySuppressions(root),
3650
+ ...inspectPolicyText(root),
3218
3651
  ...inspectSkillFamily(root),
3219
3652
  ...inspectSkillBridges(root),
3220
3653
  ...inspectPolicyPortability(root),
@@ -3265,6 +3698,17 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
3265
3698
  },
3266
3699
  ],
3267
3700
  },
3701
+ {
3702
+ label: 'rejects a suppression directive in a skill script',
3703
+ membership: 'skill scripts in the suppression population',
3704
+ rule: 'suppression',
3705
+ files: [
3706
+ {
3707
+ path: '.agents/skills/sample/scripts/control.ts',
3708
+ content: `// ${POLICY_SUPPRESSION_DIRECTIVE}\ndebugger\n`,
3709
+ },
3710
+ ],
3711
+ },
3268
3712
  {
3269
3713
  label: 'rejects a suppression directive in a root TSX file',
3270
3714
  membership: 'root code files in the suppression population',
@@ -3617,6 +4061,61 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
3617
4061
  },
3618
4062
  ],
3619
4063
  },
4064
+ {
4065
+ label: 'rejects a skill proof that imports a skill file other than its own script',
4066
+ membership: 'relative imports of skill proofs against the script each proof mirrors',
4067
+ rule: 'skill',
4068
+ files: [
4069
+ {
4070
+ path: '.agents/skills/sample/SKILL.md',
4071
+ content: SKILL_POLICY_TEXT + '\nRun `scripts/probe.ts`.\n',
4072
+ },
4073
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
4074
+ {
4075
+ path: '.agents/skills/sample/scripts/probe.ts',
4076
+ content: '// Usage: node .agents/skills/sample/scripts/probe.ts\n',
4077
+ },
4078
+ {
4079
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4080
+ content:
4081
+ "import { read } from '../../../../../.agents/skills/other/scripts/helpers.ts'\nvoid read\n",
4082
+ },
4083
+ ],
4084
+ violations: [
4085
+ {
4086
+ rule: 'skill',
4087
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4088
+ message:
4089
+ 'skill proof imports no skill file but its own script: .agents/skills/other/scripts/helpers.ts',
4090
+ },
4091
+ ],
4092
+ },
4093
+ {
4094
+ label: 'accepts a skill proof that imports its own script and a test setup module',
4095
+ membership: 'relative imports of skill proofs against the script each proof mirrors',
4096
+ rule: 'skill',
4097
+ files: [
4098
+ {
4099
+ path: '.agents/skills/sample/SKILL.md',
4100
+ content: SKILL_POLICY_TEXT + '\nRun `scripts/probe.ts`.\n',
4101
+ },
4102
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
4103
+ {
4104
+ path: '.agents/skills/sample/scripts/probe.ts',
4105
+ content: '// Usage: node .agents/skills/sample/scripts/probe.ts\n',
4106
+ },
4107
+ {
4108
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4109
+ content: [
4110
+ "import { read } from '../../../../../.agents/skills/sample/scripts/probe.ts'",
4111
+ "import { runSkillScript } from '../../../../setupServer.js'",
4112
+ 'void read',
4113
+ 'void runSkillScript',
4114
+ ].join('\n'),
4115
+ },
4116
+ ],
4117
+ violations: [],
4118
+ },
3620
4119
  {
3621
4120
  label: 'rejects an unexported value binding in a skill fence',
3622
4121
  membership: 'named value imports in canonical skill fences',
@@ -4496,6 +4995,379 @@ export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.free
4496
4995
  },
4497
4996
  ],
4498
4997
  },
4998
+ ...[
4999
+ ['a shell file outside scripts', 'src/worker/run.sh'],
5000
+ ['a bash file outside scripts', 'configs/setup.bash'],
5001
+ ['a PowerShell file outside scripts', 'tests/probe.ps1'],
5002
+ ['a Python file outside scripts', 'guides/build.py'],
5003
+ ['a batch file outside scripts', 'build.bat'],
5004
+ ['a command file outside scripts', '.claude/hook.cmd'],
5005
+ ].map(([label, path]): PolicyControl => ({
5006
+ label: `rejects ${label}`,
5007
+ membership: 'shell, PowerShell, Python, and batch extensions in the file path population',
5008
+ rule: 'portability',
5009
+ message: 'shell, PowerShell, Python, and batch files sit only in scripts',
5010
+ files: [{ path: path ?? '', content: '' }],
5011
+ })),
5012
+ {
5013
+ label: 'rejects a non-shell file inside scripts',
5014
+ membership: 'every file beneath the scripts directory',
5015
+ rule: 'portability',
5016
+ message: 'scripts holds .sh files and nothing else',
5017
+ files: [{ path: 'scripts/probe.ts', content: '' }],
5018
+ },
5019
+ ])
5020
+
5021
+ /**
5022
+ * Holds the message prefix a text violation carries, written out so the controls read the
5023
+ * production message from a second copy.
5024
+ */
5025
+ export const TEXT_POLICY_MESSAGE = 'text carries no replacement, byte-order, or control character'
5026
+
5027
+ /**
5028
+ * Lists the physical controls for every text-integrity refusal and admission the workspace route
5029
+ * reaches.
5030
+ *
5031
+ * @remarks
5032
+ * Every character a control plants is written as a `\u` escape here, so this file carries none of
5033
+ * the characters it tests for. Each excluded-directory control also writes the arrival file, whose
5034
+ * refusal proves the sweep ran over the control workspace rather than reporting nothing because it
5035
+ * found nothing. A file whose bytes are not UTF-8 cannot be written through a text fixture, so the
5036
+ * proofs of that refusal write the bytes themselves.
5037
+ */
5038
+ export const TEXT_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
5039
+ {
5040
+ label: 'rejects a bell control character in a source file',
5041
+ membership: 'source files in the suppression population, C0 controls beyond the three allowed',
5042
+ rule: 'text',
5043
+ files: [{ path: 'src/control.ts', content: "const bell = 'a\u0007b'\n" }],
5044
+ violations: [
5045
+ {
5046
+ rule: 'text',
5047
+ path: 'src/control.ts',
5048
+ line: 1,
5049
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5050
+ },
5051
+ ],
5052
+ },
5053
+ {
5054
+ label: 'rejects an escape control character in authored Markdown',
5055
+ membership: 'authored Markdown outside the excluded directories, C0 controls',
5056
+ rule: 'text',
5057
+ files: [{ path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' }],
5058
+ violations: [
5059
+ {
5060
+ rule: 'text',
5061
+ path: 'README.md',
5062
+ line: 3,
5063
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5064
+ },
5065
+ ],
5066
+ },
5067
+ {
5068
+ label: 'rejects a C1 control character',
5069
+ membership: 'source files in the suppression population, C1 controls',
5070
+ rule: 'text',
5071
+ files: [{ path: 'src/control.ts', content: "const a = 1\nconst next = 'a\u0085b'\n" }],
5072
+ violations: [
5073
+ {
5074
+ rule: 'text',
5075
+ path: 'src/control.ts',
5076
+ line: 2,
5077
+ message: `${TEXT_POLICY_MESSAGE}: \\u0085`,
5078
+ },
5079
+ ],
5080
+ },
5081
+ {
5082
+ label: 'rejects the delete control character',
5083
+ membership: 'source files in the suppression population, the control above the ASCII range',
5084
+ rule: 'text',
5085
+ files: [{ path: 'src/control.ts', content: "const del = 'a\u007Fb'\n" }],
5086
+ violations: [
5087
+ {
5088
+ rule: 'text',
5089
+ path: 'src/control.ts',
5090
+ line: 1,
5091
+ message: `${TEXT_POLICY_MESSAGE}: \\u007F`,
5092
+ },
5093
+ ],
5094
+ },
5095
+ {
5096
+ label: 'rejects a replacement character',
5097
+ membership: 'authored Markdown outside the excluded directories, decoded text',
5098
+ rule: 'text',
5099
+ files: [{ path: 'README.md', content: '# Front page\n\nbroken \uFFFD text\n' }],
5100
+ violations: [
5101
+ {
5102
+ rule: 'text',
5103
+ path: 'README.md',
5104
+ line: 3,
5105
+ message: `${TEXT_POLICY_MESSAGE}: \\uFFFD`,
5106
+ },
5107
+ ],
5108
+ },
5109
+ {
5110
+ label: 'rejects a byte-order mark that opens a file',
5111
+ membership: 'source files in the suppression population, the first character of a file',
5112
+ rule: 'text',
5113
+ files: [{ path: 'src/control.ts', content: '\uFEFFexport {}\n' }],
5114
+ violations: [
5115
+ {
5116
+ rule: 'text',
5117
+ path: 'src/control.ts',
5118
+ line: 1,
5119
+ message: `${TEXT_POLICY_MESSAGE}: \\uFEFF`,
5120
+ },
5121
+ ],
5122
+ },
5123
+ {
5124
+ label: 'rejects a byte-order mark inside a literal',
5125
+ membership: 'source files in the suppression population, a character within a line',
5126
+ rule: 'text',
5127
+ files: [{ path: 'src/control.ts', content: "const path = '\uFEFFleading.txt'\n" }],
5128
+ violations: [
5129
+ {
5130
+ rule: 'text',
5131
+ path: 'src/control.ts',
5132
+ line: 1,
5133
+ message: `${TEXT_POLICY_MESSAGE}: \\uFEFF`,
5134
+ },
5135
+ ],
5136
+ },
5137
+ {
5138
+ label: 'names each refused character once on a line that repeats it',
5139
+ membership: 'refused characters grouped by line, in order of first appearance',
5140
+ rule: 'text',
5141
+ files: [{ path: 'src/control.ts', content: "const mixed = 'a\u0007b\u0007c\u001B'\n" }],
5142
+ violations: [
5143
+ {
5144
+ rule: 'text',
5145
+ path: 'src/control.ts',
5146
+ line: 1,
5147
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5148
+ },
5149
+ {
5150
+ rule: 'text',
5151
+ path: 'src/control.ts',
5152
+ line: 1,
5153
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5154
+ },
5155
+ ],
5156
+ },
5157
+ {
5158
+ label: 'accepts tab, line feed, carriage return, and intentional Unicode',
5159
+ membership: 'the characters a text file may carry, composed and decomposed forms included',
5160
+ rule: 'text',
5161
+ files: [
5162
+ {
5163
+ path: 'src/control.ts',
5164
+ content: "const note = 'café\tcafé — \u{1F600}'\r\n\t\n",
5165
+ },
5166
+ { path: 'README.md', content: '# Front page\r\n\r\nplain é — text\n' },
5167
+ ],
5168
+ violations: [],
5169
+ },
5170
+ {
5171
+ label: 'accepts a control character below an excluded directory',
5172
+ membership: 'Markdown below a directory name the sweep never descends into',
5173
+ rule: 'text',
5174
+ files: [
5175
+ { path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' },
5176
+ { path: 'node_modules/sample/README.md', content: '# Installed\n\nbell \u0007\n' },
5177
+ ],
5178
+ violations: [
5179
+ {
5180
+ rule: 'text',
5181
+ path: 'README.md',
5182
+ line: 3,
5183
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5184
+ },
5185
+ ],
5186
+ },
5187
+ ...[
5188
+ {
5189
+ label: 'rejects a bell control character in a skill script',
5190
+ membership: 'skill scripts in the suppression population, C0 controls',
5191
+ path: '.agents/skills/sample/scripts/control.ts',
5192
+ content: "const bell = 'a\u0007b'\n",
5193
+ },
5194
+ {
5195
+ label: 'rejects a bell control character in a root JSON file',
5196
+ membership: 'root JSON files in the text population, C0 controls',
5197
+ path: 'tsconfig.json',
5198
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5199
+ },
5200
+ {
5201
+ label: 'rejects a bell control character in a root dot JSON file',
5202
+ membership: 'root dot-prefixed JSON files in the text population, C0 controls',
5203
+ path: '.oxlintrc.json',
5204
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5205
+ },
5206
+ {
5207
+ label: 'rejects a bell control character in a JSON file below an instruction folder',
5208
+ membership: 'JSON files below .agents, .claude, .codex, and .cursor, C0 controls',
5209
+ path: '.claude/settings.json',
5210
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5211
+ },
5212
+ {
5213
+ label: 'rejects a bell control character in a TOML role file',
5214
+ membership: 'TOML files below .agents, .claude, .codex, and .cursor, C0 controls',
5215
+ path: '.codex/agents/sample.toml',
5216
+ content: 'name = "sample"\nnote = "a\u0007b"\n',
5217
+ },
5218
+ {
5219
+ label: 'rejects a bell control character in a skill metadata file',
5220
+ membership: 'YAML files below .agents, .claude, .codex, and .cursor, C0 controls',
5221
+ path: '.agents/skills/sample/agents/openai.yaml',
5222
+ content: "interface:\n display_name: 'a\u0007b'\n",
5223
+ },
5224
+ {
5225
+ label: 'rejects a bell control character in a Cursor rule file',
5226
+ membership: 'MDC files below .agents, .claude, .codex, and .cursor, C0 controls',
5227
+ path: '.cursor/rules/sample.mdc',
5228
+ content: '---\nalwaysApply: true\n---\na\u0007b\n',
5229
+ },
5230
+ {
5231
+ label: 'rejects a bell control character in the attributes file',
5232
+ membership: 'the root .gitattributes file, C0 controls',
5233
+ path: '.gitattributes',
5234
+ content: '* text=auto eol=lf\n# a\u0007b\n',
5235
+ },
5236
+ {
5237
+ label: 'rejects a bell control character in the formatter ignore file',
5238
+ membership: 'the root .prettierignore file, C0 controls',
5239
+ path: '.prettierignore',
5240
+ content: 'dist/\n# a\u0007b\n',
5241
+ },
5242
+ ].map(({ label, membership, path, content }): PolicyControl => ({
5243
+ label,
5244
+ membership,
5245
+ rule: 'text',
5246
+ files: [{ path, content }],
5247
+ violations: [
5248
+ {
5249
+ rule: 'text',
5250
+ path,
5251
+ line: content.split('\n').findIndex((line) => line.includes('\u0007')) + 1,
5252
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5253
+ },
5254
+ ],
5255
+ })),
5256
+ {
5257
+ label: 'accepts a control character in JSON outside the instruction folders and the root',
5258
+ membership: 'JSON files below src and tmp, which the text population does not reach',
5259
+ rule: 'text',
5260
+ files: [
5261
+ { path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' },
5262
+ { path: 'src/data.json', content: '{\n\t"note": "a\u0007b"\n}\n' },
5263
+ { path: 'tmp/probe.json', content: '{\n\t"note": "a\u0007b"\n}\n' },
5264
+ ],
5265
+ violations: [
5266
+ {
5267
+ rule: 'text',
5268
+ path: 'README.md',
5269
+ line: 3,
5270
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5271
+ },
5272
+ ],
5273
+ },
5274
+ ])
5275
+
5276
+ /** Pairs a setup-module import fence with the specifiers it refuses and the ones it admits. */
5277
+ export interface PolicyImportCase {
5278
+ /** Names the text of the fence's message the lint configuration carries. */
5279
+ readonly marker: string
5280
+ /** Lists each module specifier with whether the fence refuses it. */
5281
+ readonly cases: ReadonlyArray<readonly [string, boolean]>
5282
+ /** States whether a relative specifier outside the fence is also a case that must be admitted. */
5283
+ readonly controlled: boolean
5284
+ }
5285
+
5286
+ /** Pairs a setup-module named-import fence with the import clauses it refuses and the ones it admits. */
5287
+ export interface PolicyNameCase {
5288
+ /** Names the text of the fence's message the lint configuration carries. */
5289
+ readonly marker: string
5290
+ /** Lists the import clauses the fence refuses, each written as the text between the braces. */
5291
+ readonly refused: readonly string[]
5292
+ /** Lists the import clauses the fence admits, each written as the text between the braces. */
5293
+ readonly admitted: readonly string[]
5294
+ }
5295
+
5296
+ /**
5297
+ * Lists the refused and admitted specifiers of every setup-module `no-restricted-imports` pattern.
5298
+ *
5299
+ * @remarks
5300
+ * The isolated-pattern proof reads a case from here when a pattern's message carries the marker, and
5301
+ * refuses a configuration that carries no pattern for a marker, so a deleted fence cannot pass by
5302
+ * leaving nothing to test.
5303
+ */
5304
+ export const POLICY_IMPORT_CASES: readonly PolicyImportCase[] = Object.freeze([
5305
+ {
5306
+ marker: 'tests/setup.ts must stay host-independent: import no node:',
5307
+ controlled: true,
5308
+ cases: [
5309
+ ['node:fs', true],
5310
+ ['node:fs/promises', true],
5311
+ ['node:path', true],
5312
+ ['nodes:fs', false],
5313
+ ['fs', false],
5314
+ ['vitest', false],
5315
+ ['./setupServer.js', false],
5316
+ ],
5317
+ },
5318
+ {
5319
+ marker: 'tests/setup.ts must stay host-independent: import no Vue',
5320
+ controlled: true,
5321
+ cases: [
5322
+ ['vue', true],
5323
+ ['vue/server-renderer', true],
5324
+ ['vue?raw', true],
5325
+ ['@vue/test-utils', true],
5326
+ ['vue-router', false],
5327
+ ['vuex', false],
5328
+ ['node:fs', false],
5329
+ ['./vue.js', false],
5330
+ ],
5331
+ },
5332
+ {
5333
+ marker: 'the browser and styles setup modules hold no Node',
5334
+ controlled: true,
5335
+ cases: [
5336
+ ['node:fs', true],
5337
+ ['node:child_process', true],
5338
+ ['nodes:fs', false],
5339
+ ['vue', false],
5340
+ ['playwright', false],
5341
+ ['./setup.js', false],
5342
+ ],
5343
+ },
5344
+ {
5345
+ marker: 'configs/policy.ts imports nothing',
5346
+ controlled: false,
5347
+ cases: [
5348
+ ['node:fs', true],
5349
+ ['./helpers.js', true],
5350
+ ['vitest', true],
5351
+ ['@orkestrel/test', true],
5352
+ ],
5353
+ },
5354
+ ])
5355
+
5356
+ /** Lists the refused and admitted named imports of every setup-module `no-restricted-imports` path. */
5357
+ export const POLICY_NAME_CASES: readonly PolicyNameCase[] = Object.freeze([
5358
+ {
5359
+ marker: 'nothing an assertion is',
5360
+ refused: [
5361
+ 'describe',
5362
+ 'it',
5363
+ 'test',
5364
+ 'expect',
5365
+ 'describe as suite',
5366
+ 'expect, vi',
5367
+ 'type TestContext, it',
5368
+ ],
5369
+ admitted: ['vi', 'TestRunner', 'afterEach', 'beforeAll', 'bench'],
5370
+ },
4499
5371
  ])
4500
5372
 
4501
5373
  /**
@@ -4656,16 +5528,468 @@ export const COMPUTED_IMPORT_PATHS: readonly string[] = Object.freeze([
4656
5528
  '.orkestrel/sample/instruments/harness/load.mts',
4657
5529
  ])
4658
5530
 
5531
+ /** Lists nested same-face imports carrying a native collection sentinel in each source. */
5532
+ export const POLICY_VUE_RELATIVE_SOURCES: readonly PolicySource[] = Object.freeze([
5533
+ {
5534
+ path: 'app/vue/components/Panel.vue',
5535
+ content: [
5536
+ '<script lang="ts">',
5537
+ "import * as boundary from '../composables/useTheme.js'",
5538
+ 'void boundary',
5539
+ 'debugger',
5540
+ '</script>',
5541
+ '',
5542
+ ].join('\n'),
5543
+ },
5544
+ {
5545
+ path: 'app/vue/components/dialogs/Panel.vue',
5546
+ content: [
5547
+ '<script setup lang="ts">',
5548
+ "import * as boundary from '../../composables/useTheme.js'",
5549
+ 'void boundary',
5550
+ 'debugger',
5551
+ '</script>',
5552
+ '',
5553
+ ].join('\n'),
5554
+ },
5555
+ {
5556
+ path: 'app/vue/features/editor/dialogs/helpers.ts',
5557
+ content: [
5558
+ "import * as boundary from '../../../components/Toolbar.vue'",
5559
+ 'void boundary',
5560
+ 'debugger',
5561
+ '',
5562
+ ].join('\n'),
5563
+ },
5564
+ {
5565
+ path: 'app/vue/features/helpers.ts',
5566
+ content: [
5567
+ "import * as boundary from '../print/index.js'",
5568
+ 'void boundary',
5569
+ 'debugger',
5570
+ '',
5571
+ ].join('\n'),
5572
+ },
5573
+ {
5574
+ path: 'src/vue/features/helpers.ts',
5575
+ content: [
5576
+ "import * as boundary from '../print/index.js'",
5577
+ 'void boundary',
5578
+ 'debugger',
5579
+ '',
5580
+ ].join('\n'),
5581
+ },
5582
+ {
5583
+ path: 'src/vue/features/editor/helpers.ts',
5584
+ content: [
5585
+ "import * as boundary from '../../components/Toolbar.js'",
5586
+ 'void boundary',
5587
+ 'debugger',
5588
+ '',
5589
+ ].join('\n'),
5590
+ },
5591
+ ])
5592
+
5593
+ /** Supplies a valid direct Vue composable with its collection sentinel inside the exported function. */
5594
+ export const POLICY_VUE_COMPOSABLE_SOURCE: PolicySource = Object.freeze({
5595
+ path: 'app/vue/composables/useTheme.ts',
5596
+ content: [
5597
+ '/** Reads the selected theme. */',
5598
+ 'export function useTheme(): string {',
5599
+ '\tdebugger',
5600
+ "\treturn 'light'",
5601
+ '}',
5602
+ '',
5603
+ ].join('\n'),
5604
+ })
5605
+
5606
+ /** Lists lexical destinations independently of the host running the test. */
5607
+ export const POLICY_VUE_TARGET_CASES: ReadonlyArray<
5608
+ readonly [string, string, string, string | undefined]
5609
+ > = Object.freeze([
5610
+ [
5611
+ '/work/app/vue/components/Panel.vue',
5612
+ '/work',
5613
+ '../composables/useTheme.js',
5614
+ 'app/vue/composables/useTheme.js',
5615
+ ],
5616
+ [
5617
+ '/work/app/vue/components/dialogs/Panel.vue',
5618
+ '/work/',
5619
+ '../../composables/useTheme.js',
5620
+ 'app/vue/composables/useTheme.js',
5621
+ ],
5622
+ ['/work/src/vue/features/editor/helpers.ts', '/work', '../../types.js', 'src/vue/types.js'],
5623
+ [
5624
+ '/work/app/vue/features/editor/dialogs/Panel.vue',
5625
+ '/work',
5626
+ '../../../components/Toolbar.vue',
5627
+ 'app/vue/components/Toolbar.vue',
5628
+ ],
5629
+ ['/work/app/vue/helpers.ts', '/work', '../print/index.js', 'app/print/index.js'],
5630
+ ['/work/app/vue/features/helpers.ts', '/work', '../print/index.js', 'app/vue/print/index.js'],
5631
+ ['/work/src/vue/features/helpers.ts', '/work', '../print/index.js', 'src/vue/print/index.js'],
5632
+ ['/work/app/vue/helpers.ts', '/work', '../browser?x', 'app/browser'],
5633
+ ['/work/app/vue/helpers.ts', '/work', '../browser/#fragment', 'app/browser'],
5634
+ ['/work/app/vue/helpers.ts', '/work', './entry.js?raw#fragment', 'app/vue/entry.js'],
5635
+ ['/work/app/vue/helpers.ts', '/work', './%2e%2e/entry.js', 'app/vue/%2e%2e/entry.js'],
5636
+ ['/work/app/vue/helpers.ts', '/work', '../../src/vue/helpers.js', 'src/vue/helpers.js'],
5637
+ [
5638
+ '/work/src/vue/nested/app/vue/helpers.ts',
5639
+ '/work',
5640
+ '../print/index.js',
5641
+ 'src/vue/nested/app/print/index.js',
5642
+ ],
5643
+ ['/app/vue/helpers.ts', '/', './entry.js', 'app/vue/entry.js'],
5644
+ ['C:/work/app/vue/features/helpers.ts', 'C:/work/', '../types.js', 'app/vue/types.js'],
5645
+ ['C:\\work\\app\\vue\\features\\helpers.ts', 'C:\\work\\', '../types.js', 'app/vue/types.js'],
5646
+ ['C:\\app\\vue\\helpers.ts', 'C:\\', './entry.js', 'app/vue/entry.js'],
5647
+ [
5648
+ '\\\\host\\share\\work\\app\\vue\\features\\helpers.ts',
5649
+ '\\\\host\\share\\work\\',
5650
+ '../types.js',
5651
+ 'app/vue/types.js',
5652
+ ],
5653
+ ['//host/share/app/vue/helpers.ts', '//host/share', './entry.js', 'app/vue/entry.js'],
5654
+ ['/work-other/app/vue/helpers.ts', '/work', './entry.js', undefined],
5655
+ ['C:/work-other/app/vue/helpers.ts', 'C:/work', './entry.js', undefined],
5656
+ ['c:/work/app/vue/helpers.ts', 'C:/work', './entry.js', undefined],
5657
+ ['D:/work/app/vue/helpers.ts', 'C:/work', './entry.js', undefined],
5658
+ ['//host/other/app/vue/helpers.ts', '//host/share', './entry.js', undefined],
5659
+ ['/work/app/vuex/helpers.ts', '/work', './entry.js', undefined],
5660
+ ['/work/other/app/vue/helpers.ts', '/work', './entry.js', undefined],
5661
+ ['app/vue/helpers.ts', '/work', './entry.js', undefined],
5662
+ ['/work/app/vue/helpers.ts', 'work', './entry.js', undefined],
5663
+ ['/work/../other/app/vue/helpers.ts', '/work/../other', './entry.js', undefined],
5664
+ ['/work/app/vue/helpers.ts', '/work', '../../../print/index.js', undefined],
5665
+ ['/work/app/vue/helpers.ts', '/work', './../types.js', undefined],
5666
+ ['/work/app/vue/helpers.ts', '/work', '.././types.js', undefined],
5667
+ ['/work/app/vue/helpers.ts', '/work', './name/../types.js', undefined],
5668
+ ['/work/app/vue/helpers.ts', '/work', './name/./types.js', undefined],
5669
+ ['/work/app/vue/helpers.ts', '/work', './name//types.js', undefined],
5670
+ ['/work/app/vue/helpers.ts', '/work', '..\\types.js', undefined],
5671
+ ['/work/app/vue/helpers.ts', '/work', '/machine/entry.js', undefined],
5672
+ ['/work/app/vue/helpers.ts', '/work', '@src/vue', undefined],
5673
+ ['/work/app/vue/helpers.ts', '/work', '././entry.js', undefined],
5674
+ ['/work/app/vue/helpers.ts', '/work', './.', undefined],
5675
+ ['/work/app/vue/helpers.ts', '/work', '././entry.js?raw#fragment', undefined],
5676
+ ['/work/app/vue/helpers.ts', '/work', './entry.js', 'app/vue/entry.js'],
5677
+ ['/work/app/vue/features/helpers.ts', '/work', '../entry.js', 'app/vue/entry.js'],
5678
+ ['/work/app/vue/helpers.ts', '/work', '.', 'app/vue'],
5679
+ ['/work/app/vue/helpers.ts', '/work', './', 'app/vue'],
5680
+ ])
5681
+
5682
+ /** Lists the two existing override owners without inferring faces from a filename suffix. */
5683
+ export const POLICY_VUE_FACES = Object.freeze(['src/vue', 'app/vue'] as const)
5684
+
5685
+ /** Lists importer locations that a directly invoked Vue rule must refuse to classify. */
5686
+ export const POLICY_VUE_UNCLASSIFIED_FILES = Object.freeze([
5687
+ '../outside/app/vue/helpers.ts',
5688
+ 'other/app/vue/helpers.ts',
5689
+ 'app/vuex/helpers.ts',
5690
+ ])
5691
+
5692
+ /** Lists native source-bearing syntax, distinguishing forms that need TypeScript parsing. */
5693
+ export const POLICY_VUE_IMPORT_FORMS = Object.freeze([
5694
+ { name: 'value', before: 'import * as boundary from ', after: '\nvoid boundary', typed: false },
5695
+ { name: 'type', before: 'import type { Boundary } from ', after: '', typed: true },
5696
+ { name: 'named', before: 'export { boundary } from ', after: '', typed: false },
5697
+ { name: 'star', before: 'export * from ', after: '', typed: false },
5698
+ { name: 'namedType', before: 'export type { Boundary } from ', after: '', typed: true },
5699
+ { name: 'starType', before: 'export type * from ', after: '', typed: true },
5700
+ { name: 'dynamic', before: 'void import(', after: ')', typed: false },
5701
+ ])
5702
+
5703
+ /** Lists every configured extension and both native Vue script extraction forms. */
5704
+ export const POLICY_VUE_POPULATIONS = Object.freeze([
5705
+ { name: 'cjs', extension: 'cjs', before: '', after: '', typed: false },
5706
+ { name: 'cts', extension: 'cts', before: '', after: '', typed: true },
5707
+ { name: 'js', extension: 'js', before: '', after: '', typed: false },
5708
+ { name: 'jsx', extension: 'jsx', before: '', after: '', typed: false },
5709
+ { name: 'mjs', extension: 'mjs', before: '', after: '', typed: false },
5710
+ { name: 'mts', extension: 'mts', before: '', after: '', typed: true },
5711
+ { name: 'ts', extension: 'ts', before: '', after: '', typed: true },
5712
+ { name: 'tsx', extension: 'tsx', before: '', after: '', typed: true },
5713
+ {
5714
+ name: 'script',
5715
+ extension: 'vue',
5716
+ before: '<script lang="ts">\n',
5717
+ after: '</script>\n',
5718
+ typed: true,
5719
+ },
5720
+ {
5721
+ name: 'setup',
5722
+ extension: 'vue',
5723
+ before: '<script setup lang="ts">\n',
5724
+ after: '</script>\n',
5725
+ typed: true,
5726
+ },
5727
+ ])
5728
+
5729
+ /** Crosses admitted nested imports and refused sibling imports with the actual syntax populations. */
5730
+ export const POLICY_VUE_IMPORT_CASES: readonly PolicyVueImportCase[] = Object.freeze(
5731
+ POLICY_VUE_FACES.flatMap((face) =>
5732
+ POLICY_VUE_POPULATIONS.flatMap((population) =>
5733
+ POLICY_VUE_IMPORT_FORMS.filter((form) => !form.typed || population.typed).flatMap((form) =>
5734
+ [false, true].map((boundary) => ({
5735
+ path: `${face}/${boundary ? '' : 'features/editor/'}${population.name}${form.name}.${population.extension}`,
5736
+ content:
5737
+ population.before +
5738
+ form.before +
5739
+ JSON.stringify(boundary ? '../print/index.js' : '../../components/Toolbar.vue') +
5740
+ form.after +
5741
+ '\ndebugger\n' +
5742
+ population.after,
5743
+ boundary,
5744
+ restricted: false,
5745
+ })),
5746
+ ),
5747
+ ),
5748
+ ),
5749
+ )
5750
+
5751
+ /** Lists relative destinations and independent raw-spelling bans under the complete configuration. */
5752
+ export const POLICY_VUE_BOUNDARY_ROWS: ReadonlyArray<readonly [string, string, boolean, boolean]> =
5753
+ Object.freeze([
5754
+ ['helpers.ts', '../print/index.js', true, false],
5755
+ ['helpers.ts', '../src/print/index.js', true, false],
5756
+ ['helpers.ts', '../print.ext/index.js', true, false],
5757
+ ['helpers.ts', '../vuex/index.js', true, false],
5758
+ ['helpers.ts', '../corex/index.js', true, false],
5759
+ ['helpers.ts', '../browserx/index.js', true, false],
5760
+ ['helpers.ts', '../print?raw', true, false],
5761
+ ['helpers.ts', '../print#fragment', true, false],
5762
+ ['features/helpers.ts', '../print/index.js', false, false],
5763
+ ['features/helpers.ts', '../print.ext/index.js', false, false],
5764
+ ['features/editor/dialogs/helpers.ts', '../../../components/Toolbar.vue', false, false],
5765
+ ['features/editor/helpers.ts', '../../../print/index.js', true, false],
5766
+ ['features/editor/helpers.ts', '../../../../src/print/index.js', true, false],
5767
+ ['features/helpers.ts', '../types.js?raw#fragment', false, false],
5768
+ ['helpers.ts', './index.js', false, false],
5769
+ ['helpers.ts', '../../../outside/index.js', true, false],
5770
+ ['helpers.ts', '@src/core', false, false],
5771
+ ['helpers.ts', '@src/browser', false, false],
5772
+ ['helpers.ts', '@src/vue', false, false],
5773
+ ['helpers.ts', '@src/print', false, true],
5774
+ ['helpers.ts', '@src/corex', false, true],
5775
+ ['helpers.ts', '@src/', false, true],
5776
+ ['helpers.ts', '@src/server', false, true],
5777
+ ['helpers.ts', '@orkestrel/sample/server', false, true],
5778
+ ['helpers.ts', 'node:fs', false, true],
5779
+ ['helpers.ts', 'fs/promises', false, true],
5780
+ ['helpers.ts', '/machine/entry.js', false, true],
5781
+ ['helpers.ts', 'C:/machine/entry.js', false, true],
5782
+ ['helpers.ts', 'C:\\machine\\entry.js', false, true],
5783
+ ['helpers.ts', 'file:///machine/entry.js', false, true],
5784
+ ['helpers.ts', 'https://host/entry.js', false, true],
5785
+ ['helpers.ts', 'data:text/javascript,export{}', false, true],
5786
+ ['helpers.ts', '..\\print\\index.js', false, true],
5787
+ ['helpers.ts', './../types.js', true, true],
5788
+ ['helpers.ts', '.././types.js', true, true],
5789
+ ['helpers.ts', './name/../types.js', true, true],
5790
+ ['helpers.ts', './name/./types.js', true, true],
5791
+ ['helpers.ts', './name//types.js', true, true],
5792
+ ['helpers.ts', 'typescript', false, true],
5793
+ ['helpers.ts', '../server/index.js', true, true],
5794
+ ['features/helpers.ts', '../server/index.js', false, true],
5795
+ ])
5796
+
5797
+ /** Supplies face-specific alias requirements and preserves existing application-relative admissions. */
5798
+ export const POLICY_VUE_OWNER_ROWS: Readonly<
5799
+ Record<string, ReadonlyArray<readonly [string, string, boolean, boolean]>>
5800
+ > = Object.freeze({
5801
+ 'src/vue': [
5802
+ ['helpers.ts', '../browser?x', false, true],
5803
+ ['helpers.ts', '../browser#fragment', false, true],
5804
+ ['helpers.ts', '@app/core', false, true],
5805
+ ['features/helpers.ts', '../../../app/core/types.js', true, true],
5806
+ ['features/helpers.ts', '../../core/helpers.js', false, true],
5807
+ ['features/helpers.ts', '../../browser/helpers.js', false, true],
5808
+ ['features/helpers.ts', '../core/helpers.js', false, true],
5809
+ ['features/helpers.ts', '../browser/helpers.js', false, true],
5810
+ ['nested/app/vue/helpers.ts', '../print/index.js', false, false],
5811
+ ],
5812
+ 'app/vue': [
5813
+ ['helpers.ts', '../browser?x', false, false],
5814
+ ['helpers.ts', '../browser#fragment', false, false],
5815
+ ['helpers.ts', '../browser/index.js', false, false],
5816
+ ['features/helpers.ts', '../../browser/helpers.js', false, false],
5817
+ ['helpers.ts', '../core/index.js', false, false],
5818
+ ['helpers.ts', '../../src/core/index.js', false, false],
5819
+ ['helpers.ts', '../../src/browser/index.js', false, false],
5820
+ ['helpers.ts', '../../src/vue/index.js', false, false],
5821
+ ['helpers.ts', '@app/core', false, false],
5822
+ ['helpers.ts', '@app/browser', false, false],
5823
+ ['helpers.ts', '@app/vue', false, false],
5824
+ ['helpers.ts', '@app/print', false, true],
5825
+ ['helpers.ts', '@app/server', false, true],
5826
+ ['features/helpers.ts', '../../server/index.js', true, true],
5827
+ ['helpers.ts', '../../src/server/index.js', true, true],
5828
+ ['nested/src/vue/helpers.ts', '../print/index.js', false, false],
5829
+ ],
5830
+ })
5831
+
5832
+ /** Materializes the boundary rows as separate collected modules without resolving their targets. */
5833
+ export const POLICY_VUE_BOUNDARY_CASES: readonly PolicyVueImportCase[] = Object.freeze([
5834
+ ...POLICY_VUE_FACES.flatMap((face) =>
5835
+ [...POLICY_VUE_BOUNDARY_ROWS, ...(POLICY_VUE_OWNER_ROWS[face] ?? [])].map(
5836
+ ([file, source, boundary, restricted], index) => ({
5837
+ path: `${face}/${file.replace('helpers.ts', `boundary${index}.ts`)}`,
5838
+ content:
5839
+ 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
5840
+ boundary,
5841
+ restricted,
5842
+ }),
5843
+ ),
5844
+ ),
5845
+ ...POLICY_VUE_FACES.flatMap((face) => [
5846
+ {
5847
+ path: `${face}/leadingdotentry.ts`,
5848
+ content: 'import * as boundary from "././entry.js"\nvoid boundary\ndebugger\n',
5849
+ boundary: true,
5850
+ restricted: false,
5851
+ },
5852
+ {
5853
+ path: `${face}/leadingdotonly.ts`,
5854
+ content: 'import * as boundary from "./."\nvoid boundary\ndebugger\n',
5855
+ boundary: true,
5856
+ restricted: false,
5857
+ },
5858
+ {
5859
+ path: `${face}/leadingdotquery.ts`,
5860
+ content: 'import * as boundary from "././entry.js?raw#fragment"\nvoid boundary\ndebugger\n',
5861
+ boundary: true,
5862
+ restricted: false,
5863
+ },
5864
+ ]),
5865
+ ])
5866
+
5867
+ /** Lists nonliteral sources and source-less exports the relative rule leaves to their existing owners. */
5868
+ export const POLICY_VUE_COMPUTED_FORMS = Object.freeze([
5869
+ 'const target = "../print/index.js"; void import(target)',
5870
+ 'void import("../print/" + "index.js")',
5871
+ 'void import(`../print/index.js`)',
5872
+ 'const boundary = 1; export { boundary }',
5873
+ ])
5874
+
5875
+ /** Supplies collection sentinels for nonliteral and source-less forms in every configured population. */
5876
+ export const POLICY_VUE_COMPUTED_CASES: readonly PolicyVueImportCase[] = Object.freeze(
5877
+ POLICY_VUE_FACES.flatMap((face) =>
5878
+ POLICY_VUE_POPULATIONS.flatMap((population) =>
5879
+ POLICY_VUE_COMPUTED_FORMS.map((content, index) => ({
5880
+ path: `${face}/computed${population.name}${index}.${population.extension}`,
5881
+ content: population.before + content + '\ndebugger\n' + population.after,
5882
+ boundary: false,
5883
+ restricted: false,
5884
+ })),
5885
+ ),
5886
+ ),
5887
+ )
5888
+
5889
+ /** Lists the measured CommonJS parser refusals by original relative filename, token, and offset. */
5890
+ export const POLICY_VUE_PARSER_ROWS: ReadonlyArray<readonly [string, string, number]> =
5891
+ Object.freeze([
5892
+ ['cjsvalue.cjs', 'import', 0],
5893
+ ['features/editor/cjsvalue.cjs', 'import', 0],
5894
+ ['cjsnamed.cjs', 'export', 0],
5895
+ ['features/editor/cjsnamed.cjs', 'export', 0],
5896
+ ['cjsstar.cjs', 'export', 0],
5897
+ ['features/editor/cjsstar.cjs', 'export', 0],
5898
+ ['computedcjs3.cjs', 'export', 20],
5899
+ ])
5900
+
5901
+ /** Retains each original parser-refused input on both faces with its exact expected native location. */
5902
+ export const POLICY_VUE_PARSER_CASES = Object.freeze(
5903
+ POLICY_VUE_FACES.flatMap((face) =>
5904
+ POLICY_VUE_PARSER_ROWS.map(([file, keyword, offset]) => {
5905
+ const path = `${face}/${file}`
5906
+ const source = [...POLICY_VUE_IMPORT_CASES, ...POLICY_VUE_COMPUTED_CASES].find(
5907
+ (candidate) => candidate.path === path,
5908
+ )
5909
+ if (source === undefined) throw new Error(`Missing original Vue parser fixture ${path}`)
5910
+ return {
5911
+ ...source,
5912
+ keyword,
5913
+ message: `Cannot use ${keyword} statement outside a module`,
5914
+ span: { offset, length: keyword.length, line: 1, column: offset + 1 },
5915
+ }
5916
+ }),
5917
+ ),
5918
+ )
5919
+
5920
+ /** Retains all other original inputs under strict rule-code and debugger-sentinel validation. */
5921
+ export const POLICY_VUE_PARSED_CASES: readonly PolicyVueImportCase[] = Object.freeze(
5922
+ [...POLICY_VUE_IMPORT_CASES, ...POLICY_VUE_COMPUTED_CASES].filter(
5923
+ (source) => !POLICY_VUE_PARSER_CASES.some((refused) => refused.path === source.path),
5924
+ ),
5925
+ )
5926
+
5927
+ /** Preserves the two removed relative alternatives solely for scratch regression controls. */
5928
+ export const POLICY_VUE_ORIGINAL_RELATIVES: Readonly<Record<string, string>> = Object.freeze({
5929
+ 'src/vue': String.raw`(?:\.\./)+(?:src/)?(?:[^/.scbv][^/.]*|s|s[^/.r][^/.]*|sr|sr[^/.c][^/.]*|src[^/.][^/.]*|c|c[^/.o][^/.]*|co|co[^/.r][^/.]*|cor|cor[^/.e][^/.]*|core[^/.][^/.]*|b|b[^/.r][^/.]*|br|br[^/.o][^/.]*|bro|bro[^/.w][^/.]*|brow|brow[^/.s][^/.]*|brows|brows[^/.e][^/.]*|browse|browse[^/.r][^/.]*|browser[^/.][^/.]*|v|v[^/.u][^/.]*|vu|vu[^/.e][^/.]*|vue[^/.][^/.]*)(?:/|$)`,
5930
+ 'app/vue': String.raw`(?:\.\./)+(?:src/|app/)?(?:[^/.sacbv][^/.]*|s|s[^/.r][^/.]*|sr|sr[^/.c][^/.]*|src[^/.][^/.]*|a|a[^/.p][^/.]*|ap|ap[^/.p][^/.]*|app[^/.][^/.]*|c|c[^/.o][^/.]*|co|co[^/.r][^/.]*|cor|cor[^/.e][^/.]*|core[^/.][^/.]*|b|b[^/.r][^/.]*|br|br[^/.o][^/.]*|bro|bro[^/.w][^/.]*|brow|brow[^/.s][^/.]*|brows|brows[^/.e][^/.]*|browse|browse[^/.r][^/.]*|browser[^/.][^/.]*|v|v[^/.u][^/.]*|vu|vu[^/.e][^/.]*|vue[^/.][^/.]*)(?:/|$)`,
5931
+ })
5932
+
5933
+ /** Lists direct domain failures and look-alike folders that must remain unregistered. */
5934
+ export const POLICY_VUE_DOMAIN_CASES: readonly PolicyVueDomainCase[] = Object.freeze([
5935
+ { ...POLICY_VUE_COMPOSABLE_SOURCE, misplaced: false, malformed: false },
5936
+ {
5937
+ path: 'app/vue/composables/useWrong.ts',
5938
+ content: 'export function useMode(): void { debugger }\n',
5939
+ misplaced: false,
5940
+ malformed: true,
5941
+ },
5942
+ {
5943
+ path: 'app/vue/composables/useHidden.ts',
5944
+ content: 'function useHidden(): void { debugger }\n',
5945
+ misplaced: false,
5946
+ malformed: true,
5947
+ },
5948
+ {
5949
+ path: 'app/vue/composables/useMany.ts',
5950
+ content: 'export function useMany(): void { debugger }\nexport function useOther(): void {}\n',
5951
+ misplaced: false,
5952
+ malformed: true,
5953
+ },
5954
+ {
5955
+ path: 'app/vue/composables/useExtra.ts',
5956
+ content: 'const extra = 1\nexport function useExtra(): void { debugger; void extra }\n',
5957
+ misplaced: false,
5958
+ malformed: true,
5959
+ },
5960
+ ...[
5961
+ 'app/vue/composables/nested/useTheme.ts',
5962
+ 'app/vue/composables/nested/app/vue/composables/useTheme.ts',
5963
+ 'app/vue/composablesExtra/useTheme.ts',
5964
+ 'app/vue/other/useTheme.ts',
5965
+ 'src/vue/composables/useTheme.ts',
5966
+ 'app/vue/composables/index.ts',
5967
+ 'app/vue/composables/main.ts',
5968
+ 'app/vue/composables/UseTheme.ts',
5969
+ ].map((path) => ({
5970
+ path,
5971
+ content: POLICY_VUE_COMPOSABLE_SOURCE.content,
5972
+ misplaced: true,
5973
+ malformed: false,
5974
+ })),
5975
+ {
5976
+ path: 'app/vue/composables/helpers.ts',
5977
+ content: POLICY_VUE_COMPOSABLE_SOURCE.content,
5978
+ misplaced: false,
5979
+ malformed: false,
5980
+ },
5981
+ ])
5982
+
4659
5983
  /**
4660
- * Reads import restrictions and collection sentinels from the installed linter.
5984
+ * Reads native diagnostic records through the installed linter without assigning rule codes.
4661
5985
  * @param directory - The fixture workspace containing the lint configuration.
4662
- * @param paths - The fixtures carrying debugger collection sentinels.
4663
- * @returns Diagnostic codes paired with workspace-relative paths.
5986
+ * @param paths - The fixtures expected to produce native diagnostics.
5987
+ * @returns Readonly native records for the caller's rule or parser validation contract.
4664
5988
  */
4665
- export function readImportDiagnostics(
5989
+ export function readNativeDiagnostics(
4666
5990
  directory: string,
4667
5991
  paths: readonly string[],
4668
- ): ReadonlySet<string> {
5992
+ ): ReadonlyArray<Readonly<Record<string, unknown>>> {
4669
5993
  const manifestPath = createRequire(import.meta.url).resolve('oxlint/package.json')
4670
5994
  const manifest = readConfigRecord(JSON.parse(readFileSync(manifestPath, 'utf8')))
4671
5995
  const entry =
@@ -4691,11 +6015,37 @@ export function readImportDiagnostics(
4691
6015
  )
4692
6016
  const diagnostics = readConfigRecord(JSON.parse(result.stdout)).diagnostics
4693
6017
  if (!Array.isArray(diagnostics)) throw new Error('Missing lint diagnostics')
6018
+ return diagnostics.map(readConfigRecord)
6019
+ }
6020
+
6021
+ /**
6022
+ * Reads import restrictions and collection sentinels from the installed linter.
6023
+ * @param directory - The fixture workspace containing the lint configuration.
6024
+ * @param paths - The fixtures carrying debugger collection sentinels.
6025
+ * @returns Diagnostic codes paired with workspace-relative paths.
6026
+ */
6027
+ export function readImportDiagnostics(
6028
+ directory: string,
6029
+ paths: readonly string[],
6030
+ ): ReadonlySet<string> {
6031
+ const diagnostics = readNativeDiagnostics(directory, paths)
4694
6032
  const codes = new Set(
4695
6033
  diagnostics.map((value: unknown) => {
4696
6034
  const diagnostic = readConfigRecord(value)
4697
6035
  if (typeof diagnostic.code !== 'string' || typeof diagnostic.filename !== 'string')
4698
- throw new Error('Missing diagnostic location')
6036
+ throw new Error(
6037
+ 'Missing diagnostic location: ' +
6038
+ JSON.stringify({
6039
+ offending: value,
6040
+ invalidDiagnostics: diagnostics.filter((candidate: unknown) => {
6041
+ const candidateRecord = readConfigRecord(candidate)
6042
+ return (
6043
+ typeof candidateRecord.code !== 'string' ||
6044
+ typeof candidateRecord.filename !== 'string'
6045
+ )
6046
+ }),
6047
+ }),
6048
+ )
4699
6049
  return (
4700
6050
  diagnostic.code +
4701
6051
  ' ' +