@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
@@ -2,13 +2,7 @@
2
2
  // P2: Required items are checked strictly; extra items are ignored before their shape is read.
3
3
 
4
4
  import type { InlineConfig, UserConfig } from 'vite'
5
- import type {
6
- PolicyComment,
7
- PolicyContext,
8
- PolicyDiagnostic,
9
- PolicyExpression,
10
- PolicySourceCode,
11
- } from '../configs/policy.js'
5
+ import type { PolicyComment, PolicyDiagnostic, PolicyExpression } from '../configs/policy.js'
12
6
  import type { PolicyProject } from './setupPolicy.js'
13
7
  import { spawnSync } from 'node:child_process'
14
8
  import {
@@ -42,21 +36,28 @@ import policyPlugin, {
42
36
  FUNCTION_RULE,
43
37
  FUNCTION_SOURCE_FILES,
44
38
  HIDDEN_RULE,
39
+ LET_RULE,
45
40
  MOCKING_RULE,
41
+ MUTABLE_RULE,
46
42
  NESTED_RULE,
47
43
  PARSER_RULE,
48
44
  PLUGIN_RULE,
49
45
  POLICY_BANNED_TERMS,
50
46
  POLICY_ENDING_GLOBS,
51
47
  POLICY_JUDGED_TERMS,
48
+ POLICY_NESTED_GLOBS,
52
49
  POLICY_PLACEMENT_GLOBS,
50
+ POLICY_SCRIPT_GLOBS,
53
51
  POLICY_VOICE_STOPWORDS,
54
52
  PRIVACY_RULE,
53
+ SCRIPT_RULE,
55
54
  TERM_RULE,
56
55
  TYPE_RULE,
57
56
  VOICE_RULE,
57
+ VUE_IMPORT_RULE,
58
58
  blankPolicyText,
59
59
  commentToPolicyParagraph,
60
+ computePolicyTarget,
60
61
  isPolicyVoiced,
61
62
  nodeToPolicyOuter,
62
63
  paragraphToPolicyOpener,
@@ -82,16 +83,42 @@ import {
82
83
  SHEET_POLICY_BARREL_PATTERN,
83
84
  SHEET_POLICY_ORDER_PATTERN,
84
85
  readImportDiagnostics,
86
+ readNativeDiagnostics,
87
+ createPolicyComment,
88
+ createPolicyContext,
89
+ createPolicyDeclarator,
90
+ createPolicyExport,
85
91
  createPolicyLazyCases,
92
+ createPolicyRefused,
86
93
  createPolicyScratch,
94
+ createPolicySource,
87
95
  createPolicySparse,
88
96
  inspectPolicyConfiguration,
89
97
  inspectPolicyWiring,
90
98
  normalizePolicyFilename,
91
99
  POLICY_EXTRACTOR,
100
+ POLICY_IMPORT_CASES,
92
101
  POLICY_LAZY_SHAPES,
102
+ POLICY_NAME_CASES,
93
103
  POLICY_PUBLISHES,
94
104
  POLICY_ROOT,
105
+ POLICY_WIRING_SETTINGS,
106
+ POLICY_VUE_COMPOSABLE_SOURCE,
107
+ POLICY_VUE_RELATIVE_SOURCES,
108
+ POLICY_VUE_TARGET_CASES,
109
+ POLICY_VUE_FACES,
110
+ POLICY_VUE_UNCLASSIFIED_FILES,
111
+ POLICY_VUE_IMPORT_FORMS,
112
+ POLICY_VUE_POPULATIONS,
113
+ POLICY_VUE_IMPORT_CASES,
114
+ POLICY_VUE_BOUNDARY_CASES,
115
+ POLICY_VUE_COMPUTED_CASES,
116
+ POLICY_VUE_COMPUTED_FORMS,
117
+ POLICY_VUE_PARSER_ROWS,
118
+ POLICY_VUE_PARSER_CASES,
119
+ POLICY_VUE_PARSED_CASES,
120
+ POLICY_VUE_ORIGINAL_RELATIVES,
121
+ POLICY_VUE_DOMAIN_CASES,
95
122
  } from './setupPolicy.js'
96
123
  import { describe, expect, it } from 'vitest'
97
124
 
@@ -225,7 +252,7 @@ describe('selected faces', () => {
225
252
  const sheets = collectSheets(POLICY_ROOT)
226
253
  const frameworks = collectFrameworks(POLICY_ROOT)
227
254
  const wrappers = collectFaceWrappers(POLICY_ROOT)
228
- const readings: Array<{
255
+ const configurations: Array<{
229
256
  readonly axis: string
230
257
  readonly face: string
231
258
  readonly wrapper: string
@@ -260,10 +287,10 @@ describe('selected faces', () => {
260
287
  expect(
261
288
  Object.getOwnPropertyDescriptor(tsconfig.compilerOptions.paths, `@${axis}/${face}`)?.value,
262
289
  ).toEqual([`./${axis}/${face}/index.ts`])
263
- readings.push({ axis, face, wrapper, project })
290
+ configurations.push({ axis, face, wrapper, project })
264
291
  }
265
- for (const { face, wrapper, project } of readings.filter((reading) =>
266
- sheets.includes(reading.face),
292
+ for (const { face, wrapper, project } of configurations.filter((faceConfiguration) =>
293
+ sheets.includes(faceConfiguration.face),
267
294
  )) {
268
295
  expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
269
296
  expect(inspectSheetConfiguration(project)).toEqual([])
@@ -277,16 +304,18 @@ describe('selected faces', () => {
277
304
  )
278
305
  expect(readConfigScript(scripts, 'build:src')).toContain(`npm run build:src:${face}`)
279
306
  }
280
- for (const { project } of readings.filter((reading) =>
281
- frameworks.includes(`${reading.axis}/${reading.face}`),
307
+ for (const { project } of configurations.filter((faceConfiguration) =>
308
+ frameworks.includes(`${faceConfiguration.axis}/${faceConfiguration.face}`),
282
309
  )) {
283
310
  expect(project.optimizeDeps?.include).toContain('vue')
284
311
  expect(project.test?.setupFiles).toEqual(
285
312
  expect.arrayContaining(['./tests/setup.ts', './tests/setupBrowser.ts']),
286
313
  )
287
314
  }
288
- for (const { wrapper } of readings.filter(
289
- (reading) => reading.axis === 'src' && frameworks.includes(`${reading.axis}/${reading.face}`),
315
+ for (const { wrapper } of configurations.filter(
316
+ (faceConfiguration) =>
317
+ faceConfiguration.axis === 'src' &&
318
+ frameworks.includes(`${faceConfiguration.axis}/${faceConfiguration.face}`),
290
319
  )) {
291
320
  const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
292
321
  expect(text).toContain('rewriteCoreSpecifier')
@@ -785,7 +814,7 @@ describe('root configuration', () => {
785
814
  [
786
815
  'probe',
787
816
  {
788
- benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
817
+ benchmark: ['tmp/probes/**/*.bench.ts', 'tests/**/*.bench.ts'],
789
818
  include: 'tmp/probes/**/*.test.ts',
790
819
  parallel: false,
791
820
  pool: 'threads',
@@ -975,8 +1004,8 @@ describe('root configuration', () => {
975
1004
  sentinel: true,
976
1005
  }
977
1006
  const test = { name: { label: 'control' }, include: ['tests/control.test.ts'] }
978
- const ignoring = Object.defineProperty(() => ({ test }), 'name', { value: 'ignoring' })
979
- const spreading = Object.defineProperty((record: object) => ({ ...record, test }), 'name', {
1007
+ const ignoreInput = Object.defineProperty(() => ({ test }), 'name', { value: 'ignoring' })
1008
+ const spreadInput = Object.defineProperty((record: object) => ({ ...record, test }), 'name', {
980
1009
  value: 'spreading',
981
1010
  })
982
1011
  const wrappers = projects.filter((entry) => typeof entry === 'string')
@@ -1008,8 +1037,8 @@ describe('root configuration', () => {
1008
1037
  ).toThrow('expected')
1009
1038
  }
1010
1039
  const factories = projects.filter((entry) => typeof entry !== 'string')
1011
- const entries: readonly unknown[] = [...factories, ignoring, spreading, { test }]
1012
- const readings = entries.map((entry) => {
1040
+ const entries: readonly unknown[] = [...factories, ignoreInput, spreadInput, { test }]
1041
+ const outcomes = entries.map((entry) => {
1013
1042
  if (typeof entry !== 'function') {
1014
1043
  return { name: undefined, callable: false, mode: undefined, leaked: [] }
1015
1044
  }
@@ -1027,13 +1056,13 @@ describe('root configuration', () => {
1027
1056
  }
1028
1057
  })
1029
1058
  const forwarded = { callable: true, mode: 'sentinel-mode', leaked: [] }
1030
- expect(readings.slice(0, factories.length)).toStrictEqual(
1059
+ expect(outcomes.slice(0, factories.length)).toStrictEqual(
1031
1060
  factories.map((entry) => ({
1032
1061
  name: typeof entry === 'function' ? entry.name : undefined,
1033
1062
  ...forwarded,
1034
1063
  })),
1035
1064
  )
1036
- expect(readings.slice(factories.length)).toStrictEqual([
1065
+ expect(outcomes.slice(factories.length)).toStrictEqual([
1037
1066
  { name: 'ignoring', callable: true, mode: undefined, leaked: [] },
1038
1067
  {
1039
1068
  name: 'spreading',
@@ -1043,8 +1072,8 @@ describe('root configuration', () => {
1043
1072
  },
1044
1073
  { name: undefined, callable: false, mode: undefined, leaked: [] },
1045
1074
  ])
1046
- for (const reading of readings.slice(factories.length)) {
1047
- expect({ ...reading, name: undefined }).not.toStrictEqual({ name: undefined, ...forwarded })
1075
+ for (const outcome of outcomes.slice(factories.length)) {
1076
+ expect({ ...outcome, name: undefined }).not.toStrictEqual({ name: undefined, ...forwarded })
1048
1077
  }
1049
1078
 
1050
1079
  // A record that carries no string `mode` is refused rather than forwarded as a project
@@ -1282,6 +1311,85 @@ describe('root configuration', () => {
1282
1311
  ).toThrow(/expected/u)
1283
1312
  })
1284
1313
 
1314
+ // A target with a server application builds it to one `main.cjs`, so a
1315
+ // single-executable embed has one file to take. The real wrapper's configuration
1316
+ // builds a scratch entry that dynamically imports a scratch module, once as the
1317
+ // wrapper declares it and once with splitting forced on. The second build is the
1318
+ // control: it shows the single chunk comes from the wrapper's `codeSplitting` option
1319
+ // and not from the entry's shape. Evaluation order of the inlined module is proved
1320
+ // once, by executing the emitted file, in the template proof that stages a server
1321
+ // root. A workspace without an `app/server` has no wrapper to build, so this case
1322
+ // reports nothing about the server application of a target that has one: the
1323
+ // assertion below only classifies the absence, and each target with a server
1324
+ // application establishes this proof when its own configuration test runs.
1325
+ it('builds the application server wrapper to one entry and splits it under the control', async () => {
1326
+ const wrapper = resolve(POLICY_ROOT, 'configs/app/vite.server.config.ts')
1327
+ const present = existsSync(wrapper)
1328
+ expect(present || !existsSync(resolve(POLICY_ROOT, 'app/server'))).toBe(true)
1329
+ if (!present) return
1330
+ const scratch = createPolicyScratch({
1331
+ parent: resolve(POLICY_ROOT, 'app/server'),
1332
+ prefix: 'config-server-entry-',
1333
+ })
1334
+ try {
1335
+ scratch.write(
1336
+ 'index.ts',
1337
+ "process.stdout.write('entry\\n')\nvoid import('./lazy.js').then(({ lazy }) => process.stdout.write(lazy()))\nprocess.stdout.write('after\\n')\n",
1338
+ )
1339
+ scratch.write(
1340
+ 'lazy.ts',
1341
+ "process.stdout.write('lazy\\n')\nexport function lazy(): string {\n\treturn String(process.argv.length)\n}\n",
1342
+ )
1343
+ const rosters: Array<readonly string[]> = []
1344
+ for (const chunked of [false, true]) {
1345
+ const loaded = await loadConfigFromFile(
1346
+ { command: 'build', mode: 'production', isSsrBuild: false, isPreview: false },
1347
+ wrapper,
1348
+ POLICY_ROOT,
1349
+ 'silent',
1350
+ )
1351
+ if (loaded === null) throw new Error('The server wrapper did not load')
1352
+ const rolldown = loaded.config.build?.rolldownOptions
1353
+ const output: unknown = rolldown?.output
1354
+ if (typeof output !== 'object' || output === null || Array.isArray(output)) {
1355
+ throw new Error('The server wrapper declares no output options')
1356
+ }
1357
+ expect(Object.getOwnPropertyDescriptor(output, 'codeSplitting')?.value).toBe(false)
1358
+ const built = await build({
1359
+ ...loaded.config,
1360
+ root: POLICY_ROOT,
1361
+ configFile: false,
1362
+ logLevel: 'silent',
1363
+ build: {
1364
+ ...loaded.config.build,
1365
+ write: false,
1366
+ lib: {
1367
+ entry: resolve(scratch.path, 'index.ts'),
1368
+ formats: ['cjs'],
1369
+ fileName: () => 'main.cjs',
1370
+ },
1371
+ rolldownOptions: chunked
1372
+ ? { ...rolldown, output: { codeSplitting: true } }
1373
+ : { ...rolldown },
1374
+ },
1375
+ })
1376
+ // A library build returns one result per format, an array even for one format.
1377
+ const results = Array.isArray(built) ? built : [built]
1378
+ expect(results).toHaveLength(1)
1379
+ for (const result of results) {
1380
+ if (!('output' in result)) throw new Error('The server build returned no output')
1381
+ rosters.push(result.output.map(({ fileName }) => fileName).toSorted())
1382
+ }
1383
+ }
1384
+ const [single, split] = rosters
1385
+ expect(single).toStrictEqual(['main.cjs'])
1386
+ expect(split).toContain('main.cjs')
1387
+ expect(split?.length).toBeGreaterThan(1)
1388
+ } finally {
1389
+ scratch.destroy()
1390
+ }
1391
+ })
1392
+
1285
1393
  it('registers proof scripts in the correct gate', () => {
1286
1394
  const manifest: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, 'package.json'), 'utf8'))
1287
1395
  if (typeof manifest !== 'object' || manifest === null) {
@@ -1549,6 +1657,49 @@ describe('root configuration', () => {
1549
1657
  expect(inspectPolicyConfiguration(controlled)).toEqual([
1550
1658
  'overrides must not configure policy/no-mocking',
1551
1659
  ])
1660
+ // The skill scripts sit under .agents, so an ignore pattern that reaches the folder defeats the
1661
+ // script rules as an ignore of a source root defeats the placement rules.
1662
+ const ignored = structuredClone(parsed)
1663
+ Object.defineProperty(ignored, 'ignorePatterns', {
1664
+ value: ['.orkestrel/', '.agents/skills/'],
1665
+ enumerable: true,
1666
+ configurable: true,
1667
+ writable: true,
1668
+ })
1669
+ expect(inspectPolicyConfiguration(ignored)).toEqual([
1670
+ 'ignorePatterns must not reach .agents/skills/',
1671
+ ])
1672
+ // An entry without an inner slash matches at any depth, so a glob or a directory name the
1673
+ // overrides route through silences guarded files while its first segment names no root.
1674
+ for (const pattern of ['*.ts', 'scripts', 'scripts/', 'policy.ts']) {
1675
+ expect(
1676
+ inspectPolicyConfiguration({ ...readConfigRecord(parsed), ignorePatterns: [pattern] }),
1677
+ ).toEqual([`ignorePatterns must not reach ${pattern}`])
1678
+ }
1679
+ for (const patterns of [['.orkestrel/'], ['.orkestrel/', '!src/keep.ts'], ['./scripts']]) {
1680
+ expect(
1681
+ inspectPolicyConfiguration({ ...readConfigRecord(parsed), ignorePatterns: patterns }),
1682
+ ).toEqual([])
1683
+ }
1684
+ })
1685
+
1686
+ it('refuses a wiring rule whose options weaken it beneath an error severity', () => {
1687
+ const parsed: unknown = JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
1688
+ const rules = readConfigRecord(readConfigRecord(parsed).rules)
1689
+ for (const [rule, setting] of [
1690
+ ['typescript/explicit-member-accessibility', ['error', { accessibility: 'off' }]],
1691
+ ['typescript/parameter-properties', ['error', { allow: ['private readonly'] }]],
1692
+ ['no-var', ['error', { ignore: true }]],
1693
+ ] as const) {
1694
+ expect(
1695
+ inspectPolicyConfiguration({
1696
+ ...readConfigRecord(parsed),
1697
+ rules: { ...rules, [rule]: setting },
1698
+ }),
1699
+ ).toEqual([
1700
+ `${rule} must have the top-level setting ${JSON.stringify(POLICY_WIRING_SETTINGS[rule])}`,
1701
+ ])
1702
+ }
1552
1703
  })
1553
1704
 
1554
1705
  it('omits the audit-confirmed dead policy type exports', () => {
@@ -1562,6 +1713,35 @@ describe('policy plugin', () => {
1562
1713
  RuleTester.it = it
1563
1714
 
1564
1715
  const tester = new RuleTester({ languageOptions: { parserOptions: { lang: 'ts' } } })
1716
+ tester.run('no-cross-vue-face', VUE_IMPORT_RULE, {
1717
+ valid: [...POLICY_VUE_IMPORT_CASES, ...POLICY_VUE_BOUNDARY_CASES, ...POLICY_VUE_COMPUTED_CASES]
1718
+ .filter((source) => source.path.endsWith('.ts') && !source.boundary)
1719
+ .map((source) => ({
1720
+ name: `accepts the native relative boundary in ${source.path}`,
1721
+ filename: resolve(POLICY_ROOT, source.path),
1722
+ cwd: POLICY_ROOT,
1723
+ code: source.content,
1724
+ })),
1725
+ invalid: [
1726
+ ...POLICY_VUE_IMPORT_CASES,
1727
+ ...POLICY_VUE_BOUNDARY_CASES,
1728
+ ...POLICY_VUE_UNCLASSIFIED_FILES.map((path) => ({
1729
+ path,
1730
+ content: "import './entry.js'\ndebugger\n",
1731
+ boundary: true,
1732
+ restricted: false,
1733
+ })),
1734
+ ]
1735
+ .filter((source) => source.path.endsWith('.ts') && source.boundary)
1736
+ .map((source) => ({
1737
+ name: `rejects the native relative boundary in ${source.path} [membership: literal relative import or export outside permitted Vue destinations]`,
1738
+ filename: resolve(POLICY_ROOT, source.path),
1739
+ cwd: POLICY_ROOT,
1740
+ code: source.content,
1741
+ errors: [{ messageId: 'vue' }],
1742
+ })),
1743
+ })
1744
+
1565
1745
  tester.run('no-mocking', MOCKING_RULE, {
1566
1746
  valid: [
1567
1747
  { name: 'accepts recorders', code: 'createRecorder()' },
@@ -1634,6 +1814,80 @@ describe('policy plugin', () => {
1634
1814
  ],
1635
1815
  })
1636
1816
 
1817
+ tester.run('no-let', LET_RULE, {
1818
+ valid: [
1819
+ { name: 'leaves an unwritten let to prefer-const', code: 'let value = 1\nread(value)' },
1820
+ {
1821
+ name: 'leaves an unwritten for-of head to prefer-const',
1822
+ code: 'for (let entry of entries) read(entry)',
1823
+ },
1824
+ {
1825
+ name: 'leaves an unwritten for-in head to prefer-const',
1826
+ code: 'for (let key in record) read(key)',
1827
+ },
1828
+ { name: 'accepts const', code: 'const value = 1\nread(value)' },
1829
+ { name: 'leaves var to no-var', code: 'var value\nvalue = 1' },
1830
+ { name: 'accepts an ambient let', code: 'declare let value: number' },
1831
+ {
1832
+ name: 'accepts a let in a global augmentation',
1833
+ code: 'declare global { let ambient: number }',
1834
+ },
1835
+ {
1836
+ name: 'accepts an exported let in a declared module',
1837
+ code: "declare module 'm' { export let ambient: string }",
1838
+ },
1839
+ {
1840
+ name: 'accepts a let in a declared namespace',
1841
+ code: 'declare namespace Ambient { let value: number }',
1842
+ },
1843
+ {
1844
+ name: 'accepts a let in a namespace nested in a declared module',
1845
+ code: "declare module 'm' { namespace Inner { let value: number } }",
1846
+ },
1847
+ ],
1848
+ invalid: [
1849
+ ...[
1850
+ { form: 'assignment', code: 'let count = 0\ncount = 1' },
1851
+ { form: 'compound assignment', code: 'let count = 0\ncount += 1' },
1852
+ { form: 'logical assignment', code: 'let count: number | undefined = 0\ncount ??= 1' },
1853
+ { form: 'update', code: 'let count = 0\ncount++' },
1854
+ { form: 'counter loop', code: 'for (let index = 0; index < 3; index++) read(index)' },
1855
+ { form: 'array destructuring target', code: 'let [first] = [1]\n;[first] = [2]' },
1856
+ { form: 'object destructuring target', code: 'let { a } = record\n;({ a } = other)' },
1857
+ {
1858
+ form: 'for-of head written in the body',
1859
+ code: 'for (let entry of entries) { entry = 1 }',
1860
+ },
1861
+ { form: 'closure write', code: 'let ready = false\nlisten(() => { ready = true })' },
1862
+ { form: 'later declarator', code: 'let a = 1, b = 2\nb = 3\nread(a, b)' },
1863
+ { form: 'declaration without a value', code: 'let value\nread(value)' },
1864
+ { form: 'value assigned later', code: 'let value: number\nvalue = 1\nread(value)' },
1865
+ ].map(({ form, code }) => ({
1866
+ name: `rejects a let by ${form} [membership: let that prefer-const leaves]`,
1867
+ code,
1868
+ errors: [{ messageId: 'let' }],
1869
+ })),
1870
+ ...[
1871
+ {
1872
+ form: 'a bare let beside a global augmentation',
1873
+ code: 'declare global { let ambient: number }\nlet value\nread(value)',
1874
+ },
1875
+ {
1876
+ form: 'a reassigned let beside a declared module',
1877
+ code: "declare module 'm' { export let ambient: string }\nlet count = 0\ncount++",
1878
+ },
1879
+ {
1880
+ form: 'a bare let in a namespace that is not declared',
1881
+ code: 'namespace Local { let value: number }',
1882
+ },
1883
+ ].map(({ form, code }) => ({
1884
+ name: `rejects ${form} [membership: let outside an ambient block]`,
1885
+ code,
1886
+ errors: [{ messageId: 'let' }],
1887
+ })),
1888
+ ],
1889
+ })
1890
+
1637
1891
  tester.run('no-nested-functions', NESTED_RULE, {
1638
1892
  valid: [
1639
1893
  {
@@ -1931,6 +2185,12 @@ createSomething({
1931
2185
 
1932
2186
  tester.run('no-misplaced-function', FUNCTION_RULE, {
1933
2187
  valid: [
2188
+ ...POLICY_VUE_DOMAIN_CASES.filter((source) => !source.misplaced).map((source) => ({
2189
+ name: `accepts Vue function placement in ${source.path}`,
2190
+ filename: resolve(POLICY_ROOT, source.path),
2191
+ cwd: POLICY_ROOT,
2192
+ code: source.content,
2193
+ })),
1934
2194
  {
1935
2195
  name: 'accepts a function in a function-kind file',
1936
2196
  filename: 'src/worker/helpers.ts',
@@ -1979,6 +2239,13 @@ createSomething({
1979
2239
  code: 'export function runTask(): void {}',
1980
2240
  errors: [{ messageId: 'function' }],
1981
2241
  },
2242
+ ...POLICY_VUE_DOMAIN_CASES.filter((source) => source.misplaced).map((source) => ({
2243
+ name: `rejects Vue function placement in ${source.path} [membership: modules outside the exact direct function register]`,
2244
+ filename: resolve(POLICY_ROOT, source.path),
2245
+ cwd: POLICY_ROOT,
2246
+ code: source.content,
2247
+ errors: [{ messageId: 'function' }],
2248
+ })),
1982
2249
  {
1983
2250
  name: 'rejects a property-held arrow in a route table [membership: module function syntax whose file is absent from the function register]',
1984
2251
  filename: 'src/worker/routes.ts',
@@ -2281,6 +2548,12 @@ createSomething({
2281
2548
 
2282
2549
  tester.run('no-malformed-domain', DOMAIN_RULE, {
2283
2550
  valid: [
2551
+ ...POLICY_VUE_DOMAIN_CASES.filter((source) => !source.malformed).map((source) => ({
2552
+ name: `accepts Vue domain shape in ${source.path}`,
2553
+ filename: resolve(POLICY_ROOT, source.path),
2554
+ cwd: POLICY_ROOT,
2555
+ code: source.content,
2556
+ })),
2284
2557
  {
2285
2558
  name: 'accepts a registered function module carrying imports and one named export',
2286
2559
  filename: 'app/browser/composables/useTheme.ts',
@@ -2306,6 +2579,13 @@ createSomething({
2306
2579
  code: 'export function useMode(): void {}',
2307
2580
  errors: [{ messageId: 'module' }],
2308
2581
  },
2582
+ ...POLICY_VUE_DOMAIN_CASES.filter((source) => source.malformed).map((source) => ({
2583
+ name: `rejects Vue domain shape in ${source.path} [membership: direct camelCase modules in the Vue function domain]`,
2584
+ filename: resolve(POLICY_ROOT, source.path),
2585
+ cwd: POLICY_ROOT,
2586
+ code: source.content,
2587
+ errors: [{ messageId: 'module' }],
2588
+ })),
2309
2589
  {
2310
2590
  name: 'rejects a hidden domain function [membership: direct camelCase modules in a registered function-domain folder]',
2311
2591
  filename: 'app/browser/composables/useTheme.ts',
@@ -2367,6 +2647,253 @@ createSomething({
2367
2647
  ],
2368
2648
  })
2369
2649
 
2650
+ const SCRIPT_GUARD = [
2651
+ 'if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {',
2652
+ '\tprocess.exitCode = main()',
2653
+ '}',
2654
+ ].join('\n')
2655
+
2656
+ tester.run('no-malformed-script', SCRIPT_RULE, {
2657
+ valid: [
2658
+ {
2659
+ name: 'accepts imports, documented exports, and the one guarded entry',
2660
+ filename: '.agents/skills/sample/scripts/tool.ts',
2661
+ code: [
2662
+ "import { realpathSync } from 'node:fs'",
2663
+ '',
2664
+ '/** Reads the one value. */',
2665
+ 'export function read(): number {',
2666
+ '\treturn 1',
2667
+ '}',
2668
+ '',
2669
+ '/** Runs the script. */',
2670
+ 'export function main(): number {',
2671
+ '\treturn read()',
2672
+ '}',
2673
+ '',
2674
+ SCRIPT_GUARD,
2675
+ ].join('\n'),
2676
+ },
2677
+ {
2678
+ name: 'accepts a documented exported type, interface, and constant',
2679
+ filename: '.agents/skills/sample/scripts/tool.ts',
2680
+ code: [
2681
+ '/** Names the one value. */',
2682
+ 'export type Value = number',
2683
+ '',
2684
+ '/** Describes the one record. */',
2685
+ 'export interface Entry {',
2686
+ '\treadonly value: Value',
2687
+ '}',
2688
+ '',
2689
+ '/** Holds the one constant. */',
2690
+ 'export const VALUE = 1',
2691
+ ].join('\n'),
2692
+ },
2693
+ {
2694
+ name: 'accepts a guard the formatter wrapped over lines',
2695
+ filename: '.agents/skills/sample/scripts/tool.ts',
2696
+ code: [
2697
+ 'if (',
2698
+ '\tprocess.argv[1] !== undefined &&',
2699
+ '\trealpathSync(process.argv[1]) === import.meta.filename',
2700
+ ') {',
2701
+ '\tmain()',
2702
+ '}',
2703
+ ].join('\n'),
2704
+ },
2705
+ {
2706
+ name: 'accepts a doc block separated from its export by a blank line',
2707
+ filename: '.agents/skills/sample/scripts/tool.ts',
2708
+ code: ['/** Holds the one constant. */', '', 'export const VALUE = 1'].join('\n'),
2709
+ },
2710
+ {
2711
+ name: 'accepts a module outside the skill scripts',
2712
+ filename: 'src/worker/helpers.ts',
2713
+ code: 'function hidden(): void {}\nhidden()',
2714
+ },
2715
+ {
2716
+ name: 'accepts a file beside the skill scripts',
2717
+ filename: '.agents/skills/sample/references/tool.ts',
2718
+ code: 'function hidden(): void {}\nhidden()',
2719
+ },
2720
+ ],
2721
+ invalid: [
2722
+ {
2723
+ name: 'rejects an unexported function [membership: top-level declarations of a skill script]',
2724
+ filename: '.agents/skills/sample/scripts/tool.ts',
2725
+ code: 'function read(): number { return 1 }',
2726
+ errors: [{ messageId: 'hidden' }],
2727
+ },
2728
+ {
2729
+ name: 'rejects an unexported constant [membership: top-level declarations of a skill script]',
2730
+ filename: '.agents/skills/sample/scripts/tool.ts',
2731
+ code: 'const VALUE = 1',
2732
+ errors: [{ messageId: 'hidden' }],
2733
+ },
2734
+ {
2735
+ name: 'rejects an unexported interface even with a doc block [membership: top-level declarations of a skill script]',
2736
+ filename: '.agents/skills/sample/scripts/tool.ts',
2737
+ code: [
2738
+ '/** Describes the one record. */',
2739
+ 'interface Value { readonly value: number }',
2740
+ ].join('\n'),
2741
+ errors: [{ messageId: 'hidden' }],
2742
+ },
2743
+ {
2744
+ name: 'rejects an export with no comment above it [membership: exported declarations of a skill script]',
2745
+ filename: '.agents/skills/sample/scripts/tool.ts',
2746
+ code: 'export function read(): number { return 1 }',
2747
+ errors: [{ messageId: 'doc' }],
2748
+ },
2749
+ {
2750
+ name: 'rejects an export under a line comment [membership: exported declarations of a skill script]',
2751
+ filename: '.agents/skills/sample/scripts/tool.ts',
2752
+ code: ['// Reads the one value.', 'export function read(): number { return 1 }'].join('\n'),
2753
+ errors: [{ messageId: 'doc' }],
2754
+ },
2755
+ {
2756
+ name: 'rejects an export under a plain block comment [membership: exported declarations of a skill script]',
2757
+ filename: '.agents/skills/sample/scripts/tool.ts',
2758
+ code: ['/* Reads the one value. */', 'export const VALUE = 1'].join('\n'),
2759
+ errors: [{ messageId: 'doc' }],
2760
+ },
2761
+ {
2762
+ name: 'rejects a doc block separated from its export by code [membership: exported declarations of a skill script]',
2763
+ filename: '.agents/skills/sample/scripts/tool.ts',
2764
+ code: ['/** Holds the one constant. */', 'const FIRST = 1', 'export const SECOND = 2'].join(
2765
+ '\n',
2766
+ ),
2767
+ errors: [{ messageId: 'hidden' }, { messageId: 'doc' }],
2768
+ },
2769
+ {
2770
+ name: 'rejects a bare call [membership: top-level statements of a skill script]',
2771
+ filename: '.agents/skills/sample/scripts/tool.ts',
2772
+ code: 'main()',
2773
+ errors: [{ messageId: 'entry' }],
2774
+ },
2775
+ {
2776
+ name: 'rejects an entry under another condition [membership: top-level statements of a skill script]',
2777
+ filename: '.agents/skills/sample/scripts/tool.ts',
2778
+ code: ['if (process.argv[1] !== undefined) {', '\tmain()', '}'].join('\n'),
2779
+ errors: [{ messageId: 'entry' }],
2780
+ },
2781
+ {
2782
+ name: 'rejects the guard when it carries an else branch [membership: top-level statements of a skill script]',
2783
+ filename: '.agents/skills/sample/scripts/tool.ts',
2784
+ code: [SCRIPT_GUARD.replace(/\}$/u, '} else {'), '\tmain()', '}'].join('\n'),
2785
+ errors: [{ messageId: 'entry' }],
2786
+ },
2787
+ {
2788
+ name: 'rejects a second guard [membership: top-level statements of a skill script]',
2789
+ filename: '.agents/skills/sample/scripts/tool.ts',
2790
+ code: [SCRIPT_GUARD, SCRIPT_GUARD].join('\n'),
2791
+ errors: [{ messageId: 'entry' }],
2792
+ },
2793
+ {
2794
+ name: 'rejects an export list [membership: top-level statements of a skill script]',
2795
+ filename: '.agents/skills/sample/scripts/tool.ts',
2796
+ code: ['/** Holds the one constant. */', 'const VALUE = 1', 'export { VALUE }'].join('\n'),
2797
+ errors: [{ messageId: 'hidden' }, { messageId: 'entry' }],
2798
+ },
2799
+ {
2800
+ name: 'rejects a default export [membership: top-level statements of a skill script]',
2801
+ filename: '.agents/skills/sample/scripts/tool.ts',
2802
+ code: 'export default {}',
2803
+ errors: [{ messageId: 'entry' }],
2804
+ },
2805
+ ],
2806
+ })
2807
+
2808
+ tester.run('no-mutable-property', MUTABLE_RULE, {
2809
+ valid: [
2810
+ {
2811
+ name: 'accepts a readonly interface property',
2812
+ filename: 'src/core/types.ts',
2813
+ code: 'export interface Value { readonly id: string }',
2814
+ },
2815
+ {
2816
+ name: 'accepts a readonly property of a type alias object',
2817
+ filename: 'src/core/types.ts',
2818
+ code: 'export type Value = { readonly id: string; readonly name?: string }',
2819
+ },
2820
+ {
2821
+ name: 'accepts a method signature',
2822
+ filename: 'src/core/types.ts',
2823
+ code: 'export interface Value { read(): string }',
2824
+ },
2825
+ {
2826
+ name: 'accepts an inline parameter type',
2827
+ filename: 'src/core/helpers.ts',
2828
+ code: 'export function read(options: { id: string }): string { return options.id }',
2829
+ },
2830
+ {
2831
+ name: 'accepts an inline parameter type of an interface method',
2832
+ filename: 'src/core/types.ts',
2833
+ code: 'export interface Value { read(options: { id: string }): string }',
2834
+ },
2835
+ {
2836
+ name: 'accepts an inline parameter type of a function typed property',
2837
+ filename: 'src/core/types.ts',
2838
+ code: 'export interface Value { readonly read: (options: { id: string }) => string }',
2839
+ },
2840
+ {
2841
+ name: 'accepts an object type annotating a binding',
2842
+ filename: 'src/core/constants.ts',
2843
+ code: "export const VALUE: { id: string } = { id: 'a' }",
2844
+ },
2845
+ {
2846
+ name: 'accepts a mutable property of an ambient declaration file',
2847
+ filename: 'src/core/types.d.ts',
2848
+ code: 'export interface Value { id: string }',
2849
+ },
2850
+ ],
2851
+ invalid: [
2852
+ {
2853
+ name: 'rejects a mutable interface property [membership: property signatures of an interface]',
2854
+ filename: 'src/core/types.ts',
2855
+ code: 'export interface Value { id: string }',
2856
+ errors: [{ messageId: 'mutable' }],
2857
+ },
2858
+ {
2859
+ name: 'rejects a mutable optional interface property [membership: property signatures of an interface]',
2860
+ filename: 'src/core/types.ts',
2861
+ code: 'export interface Value { readonly id: string; name?: string }',
2862
+ errors: [{ messageId: 'mutable' }],
2863
+ },
2864
+ {
2865
+ name: 'rejects a mutable property of a type alias object [membership: property signatures of a type alias object type]',
2866
+ filename: 'src/core/types.ts',
2867
+ code: 'export type Value = { id: string }',
2868
+ errors: [{ messageId: 'mutable' }],
2869
+ },
2870
+ {
2871
+ name: 'rejects a mutable property of a union member [membership: property signatures of a type alias object type]',
2872
+ filename: 'src/core/types.ts',
2873
+ code: 'export type Value = { readonly id: string } | { name: string }',
2874
+ errors: [{ messageId: 'mutable' }],
2875
+ },
2876
+ {
2877
+ name: 'rejects a mutable property of a nested object type [membership: property signatures of a type alias object type]',
2878
+ filename: 'src/core/types.ts',
2879
+ code: 'export interface Value { readonly inner: { id: string } }',
2880
+ errors: [{ messageId: 'mutable' }],
2881
+ },
2882
+ {
2883
+ name: 'rejects a mutable property of a generic argument [membership: property signatures of a type alias object type]',
2884
+ filename: 'src/core/types.ts',
2885
+ code: 'export type Value = ReadonlyArray<{ id: string }>',
2886
+ errors: [{ messageId: 'mutable' }],
2887
+ },
2888
+ {
2889
+ name: 'rejects a mutable function typed property [membership: property signatures of an interface]',
2890
+ filename: 'src/core/types.ts',
2891
+ code: 'export interface Value { read: () => string }',
2892
+ errors: [{ messageId: 'mutable' }],
2893
+ },
2894
+ ],
2895
+ })
2896
+
2370
2897
  tester.run('no-malformed-summary', VOICE_RULE, {
2371
2898
  valid: [
2372
2899
  {
@@ -2734,7 +3261,7 @@ createSomething({
2734
3261
  range: [47, 63],
2735
3262
  declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
2736
3263
  }
2737
- const source: PolicySourceCode = { text: '', getAllComments: () => [] }
3264
+ const source = createPolicySource('')
2738
3265
 
2739
3266
  it('refuses a hole in the declarators of a statement as the declarator it reads as', () => {
2740
3267
  const declarator: PolicyExpression = {
@@ -2782,14 +3309,7 @@ createSomething({
2782
3309
 
2783
3310
  it('refuses a hole in the body of a function-domain module as the statement it reads as', () => {
2784
3311
  const reports: PolicyDiagnostic[] = []
2785
- const context: PolicyContext = {
2786
- filename: '/ws/src/server/execution/run.ts',
2787
- cwd: '/ws',
2788
- sourceCode: source,
2789
- report: (diagnostic) => {
2790
- reports.push(diagnostic)
2791
- },
2792
- }
3312
+ const context = createPolicyContext('/ws/src/server/execution/run.ts', '/ws', reports)
2793
3313
  const dense: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
2794
3314
  const sparse: PolicyExpression = {
2795
3315
  type: 'Program',
@@ -2855,12 +3375,12 @@ createSomething({
2855
3375
  { esmodule: false },
2856
3376
  ])
2857
3377
  if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
2858
- const touching = configured.overrides
3378
+ const overrides = configured.overrides
2859
3379
  .map(readConfigRecord)
2860
3380
  .filter(
2861
3381
  (override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
2862
3382
  )
2863
- expect(touching).toEqual([])
3383
+ expect(overrides).toEqual([])
2864
3384
  })
2865
3385
 
2866
3386
  it('permits a computed import() and refuses a computed require at every module path', () => {
@@ -2923,107 +3443,519 @@ createSomething({
2923
3443
  }
2924
3444
  })
2925
3445
 
2926
- it('matches every isolated import pattern with refused and admitted fixtures', () => {
2927
- const fixture = createPolicyScratch({ prefix: 'propagation-patterns-' })
2928
- const scratch = fixture.path
3446
+ it('computes Vue targets from lexical importer and workspace boundaries', () => {
3447
+ expect(POLICY_VUE_TARGET_CASES.length).toBeGreaterThan(30)
3448
+ for (const [filename, cwd, source, expected] of POLICY_VUE_TARGET_CASES) {
3449
+ expect({ filename, cwd, source, target: computePolicyTarget(filename, cwd, source) }).toEqual(
3450
+ {
3451
+ filename,
3452
+ cwd,
3453
+ source,
3454
+ target: expected,
3455
+ },
3456
+ )
3457
+ }
3458
+ })
3459
+
3460
+ it('partitions every Vue syntax input between parsed and native parser-refused coverage', () => {
3461
+ const originals = [...POLICY_VUE_IMPORT_CASES, ...POLICY_VUE_COMPUTED_CASES]
3462
+ const partition = [...POLICY_VUE_PARSED_CASES, ...POLICY_VUE_PARSER_CASES]
3463
+ expect(originals).toHaveLength(312)
3464
+ expect(new Set(originals.map(({ path }) => path)).size).toBe(originals.length)
3465
+ expect(POLICY_VUE_PARSED_CASES).toHaveLength(298)
3466
+ expect(POLICY_VUE_PARSER_CASES).toHaveLength(14)
3467
+ expect(new Set(partition.map(({ path }) => path)).size).toBe(partition.length)
3468
+ expect(partition.map(({ path }) => path).toSorted()).toEqual(
3469
+ originals.map(({ path }) => path).toSorted(),
3470
+ )
3471
+ for (const original of originals) {
3472
+ expect(partition.find((source) => source.path === original.path)).toMatchObject(original)
3473
+ }
3474
+ expect(POLICY_VUE_PARSER_CASES.map(({ path }) => path).toSorted()).toEqual(
3475
+ POLICY_VUE_FACES.flatMap((face) =>
3476
+ POLICY_VUE_PARSER_ROWS.map(([file]) => `${face}/${file}`),
3477
+ ).toSorted(),
3478
+ )
3479
+ expect(POLICY_VUE_PARSER_CASES.every(({ path }) => path.endsWith('.cjs'))).toBe(true)
3480
+ expect([...new Set(POLICY_VUE_POPULATIONS.map(({ extension }) => extension))]).toEqual([
3481
+ 'cjs',
3482
+ 'cts',
3483
+ 'js',
3484
+ 'jsx',
3485
+ 'mjs',
3486
+ 'mts',
3487
+ 'ts',
3488
+ 'tsx',
3489
+ 'vue',
3490
+ ])
3491
+ for (const face of POLICY_VUE_FACES) {
3492
+ const sources = originals.filter((source) => source.path.startsWith(face + '/'))
3493
+ expect(sources).toHaveLength(156)
3494
+ for (const population of POLICY_VUE_POPULATIONS) {
3495
+ for (const form of POLICY_VUE_IMPORT_FORMS.filter(
3496
+ (item) => !item.typed || population.typed,
3497
+ )) {
3498
+ expect(sources).toContainEqual(
3499
+ expect.objectContaining({
3500
+ path: `${face}/${population.name}${form.name}.${population.extension}`,
3501
+ boundary: true,
3502
+ }),
3503
+ )
3504
+ expect(sources).toContainEqual(
3505
+ expect.objectContaining({
3506
+ path: `${face}/features/editor/${population.name}${form.name}.${population.extension}`,
3507
+ boundary: false,
3508
+ }),
3509
+ )
3510
+ }
3511
+ for (const index of POLICY_VUE_COMPUTED_FORMS.keys()) {
3512
+ expect(sources).toContainEqual(
3513
+ expect.objectContaining({
3514
+ path: `${face}/computed${population.name}${index}.${population.extension}`,
3515
+ boundary: false,
3516
+ }),
3517
+ )
3518
+ }
3519
+ }
3520
+ }
3521
+ })
3522
+
3523
+ it('checks Vue import syntax and populations through the canonical policy', () => {
3524
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-syntax-' })
2929
3525
  try {
2930
- const configured = readConfigRecord(
2931
- JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
3526
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3527
+ fixture.write(
3528
+ 'configs/policy.ts',
3529
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
2932
3530
  )
2933
- if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
2934
- const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
2935
- const block = readConfigRecord(value)
2936
- const rule = readConfigRecord(block.rules)['no-restricted-imports']
2937
- if (!Array.isArray(rule)) return []
2938
- const patterns = readConfigRecord(rule[1]).patterns
2939
- if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
2940
- return patterns.map((patternValue: unknown, patternIndex: number) => {
2941
- const pattern = readConfigRecord(patternValue)
2942
- const message = pattern.message
2943
- if (typeof message !== 'string') throw new Error('Missing pattern message')
2944
- const alias = /@src\/(core|browser|server) alias/u.exec(message)?.[1]
2945
- const listed: ReadonlyArray<readonly [string, boolean]> =
2946
- alias !== undefined
2947
- ? [
2948
- [`../${alias}/index.js`, true],
2949
- [`../../${alias}/types.js`, true],
2950
- [`../../src/${alias}/index.js`, true],
2951
- [`../${alias}`, true],
2952
- [`../${alias}/`, true],
2953
- [`../${alias}?raw`, true],
2954
- [`../${alias}#fragment`, true],
2955
- [`@src/${alias}`, false],
2956
- [`./${alias}/index.js`, false],
2957
- [`../${alias}x/index.js`, false],
2958
- [`../${alias}.ext/index.js`, false],
2959
- ]
2960
- : message.includes('URL schemes')
2961
- ? [
2962
- ['https://host/x', true],
2963
- ['data:text/plain,x', true],
2964
- ['node:fs', false],
2965
- ['n:entry', true],
2966
- ['no:entry', true],
2967
- ['nod:entry', true],
2968
- ['nodea:entry', true],
2969
- ['1bad:entry', false],
2970
- ['./data:entry', false],
3531
+ for (const face of POLICY_VUE_FACES) {
3532
+ const sources = POLICY_VUE_PARSED_CASES.filter((source) =>
3533
+ source.path.startsWith(face + '/'),
3534
+ )
3535
+ expect(sources).toHaveLength(149)
3536
+ expect(sources).toContainEqual(
3537
+ expect.objectContaining({ path: `${face}/cjsdynamic.cjs`, boundary: true }),
3538
+ )
3539
+ expect(sources).toContainEqual(
3540
+ expect.objectContaining({
3541
+ path: `${face}/features/editor/cjsdynamic.cjs`,
3542
+ boundary: false,
3543
+ }),
3544
+ )
3545
+ for (const source of sources) fixture.write(source.path, source.content)
3546
+ const codes = readImportDiagnostics(
3547
+ fixture.path,
3548
+ sources.map(({ path }) => path),
3549
+ )
3550
+ expect(
3551
+ sources.map(({ path }) => ({
3552
+ path,
3553
+ boundary: codes.has('policy(no-cross-vue-face) ' + path),
3554
+ restricted: codes.has('eslint(no-restricted-imports) ' + path),
3555
+ })),
3556
+ ).toEqual(sources.map(({ path, boundary, restricted }) => ({ path, boundary, restricted })))
3557
+ }
3558
+ } finally {
3559
+ fixture.destroy()
3560
+ }
3561
+ })
3562
+
3563
+ it('retains exact native parser refusals for CommonJS Vue inputs on both faces', () => {
3564
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-parser-' })
3565
+ try {
3566
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3567
+ fixture.write(
3568
+ 'configs/policy.ts',
3569
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3570
+ )
3571
+ const sources = POLICY_VUE_PARSER_CASES
3572
+ const paths = sources.map(({ path }) => path)
3573
+ expect(paths).toHaveLength(14)
3574
+ for (const source of sources) fixture.write(source.path, source.content)
3575
+ const diagnostics = readNativeDiagnostics(fixture.path, paths)
3576
+ expect(diagnostics).toHaveLength(paths.length)
3577
+ const comparisonRoot = realpathSync.native(fixture.path)
3578
+ const observed = diagnostics.map((diagnostic) => {
3579
+ if (typeof diagnostic.filename !== 'string')
3580
+ throw new Error('Missing parser diagnostic filename: ' + JSON.stringify(diagnostic))
3581
+ const path = normalizePolicyFilename(comparisonRoot, diagnostic.filename)
3582
+ const source = sources.find((candidate) => candidate.path === path)
3583
+ if (source === undefined)
3584
+ throw new Error('Unexpected parser diagnostic: ' + JSON.stringify(diagnostic))
3585
+ expect(diagnostic).toEqual({
3586
+ message: source.message,
3587
+ severity: 'error',
3588
+ filename: diagnostic.filename,
3589
+ labels: [{ span: source.span }],
3590
+ })
3591
+ expect(
3592
+ source.content.slice(source.span.offset, source.span.offset + source.span.length),
3593
+ ).toBe(source.keyword)
3594
+ return path
3595
+ })
3596
+ expect(observed.toSorted()).toEqual(paths.toSorted())
3597
+ } finally {
3598
+ fixture.destroy()
3599
+ }
3600
+ })
3601
+
3602
+ it('retains Vue independent restrictions beside exact relative diagnostics', () => {
3603
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-boundaries-' })
3604
+ try {
3605
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3606
+ fixture.write(
3607
+ 'configs/policy.ts',
3608
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3609
+ )
3610
+ const sources = POLICY_VUE_BOUNDARY_CASES
3611
+ expect(sources.length).toBeGreaterThan(100)
3612
+ for (const source of sources) fixture.write(source.path, source.content)
3613
+ const codes = readImportDiagnostics(
3614
+ fixture.path,
3615
+ sources.map(({ path }) => path),
3616
+ )
3617
+ expect(
3618
+ sources.map(({ path }) => ({
3619
+ path,
3620
+ boundary: codes.has('policy(no-cross-vue-face) ' + path),
3621
+ restricted: codes.has('eslint(no-restricted-imports) ' + path),
3622
+ })),
3623
+ ).toEqual(sources.map(({ path, boundary, restricted }) => ({ path, boundary, restricted })))
3624
+ } finally {
3625
+ fixture.destroy()
3626
+ }
3627
+ })
3628
+
3629
+ it('detects suppressed Vue reporting in a scratch policy without weakening independent bans', () => {
3630
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-report-control-' })
3631
+ try {
3632
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3633
+ const plugin = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
3634
+ const marker = "context.report({ node, messageId: 'vue' })"
3635
+ expect(plugin.split(marker)).toHaveLength(2)
3636
+ fixture.write('configs/policy.ts', plugin)
3637
+ const sources = POLICY_VUE_BOUNDARY_CASES
3638
+ for (const source of sources) fixture.write(source.path, source.content)
3639
+ const paths = sources.map(({ path }) => path)
3640
+ const enabled = readImportDiagnostics(fixture.path, paths)
3641
+ const expected = sources.filter(({ boundary }) => boundary).map(({ path }) => path)
3642
+ expect(expected).toContain('src/vue/boundary0.ts')
3643
+ expect(expected).toContain('app/vue/boundary0.ts')
3644
+ expect(paths.filter((path) => enabled.has('policy(no-cross-vue-face) ' + path))).toEqual(
3645
+ expected,
3646
+ )
3647
+ fixture.write('configs/policy.ts', plugin.replace(marker, 'void node'))
3648
+ const suppressed = readImportDiagnostics(fixture.path, paths)
3649
+ const actual = paths.filter((path) => suppressed.has('policy(no-cross-vue-face) ' + path))
3650
+ expect(actual).toEqual([])
3651
+ expect(() => expect(actual).toEqual(expected)).toThrow('deeply equal')
3652
+ for (const { path, restricted } of sources) {
3653
+ expect({ path, enabled: enabled.has('eslint(no-restricted-imports) ' + path) }).toEqual({
3654
+ path,
3655
+ enabled: restricted,
3656
+ })
3657
+ expect({
3658
+ path,
3659
+ suppressed: suppressed.has('eslint(no-restricted-imports) ' + path),
3660
+ }).toEqual({ path, suppressed: restricted })
3661
+ }
3662
+ } finally {
3663
+ fixture.destroy()
3664
+ }
3665
+ })
3666
+
3667
+ it('detects restored Vue relative regexes against nested canonical imports', () => {
3668
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-regex-control-' })
3669
+ try {
3670
+ const text = readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')
3671
+ fixture.write('.oxlintrc.json', text)
3672
+ fixture.write(
3673
+ 'configs/policy.ts',
3674
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3675
+ )
3676
+ for (const source of POLICY_VUE_RELATIVE_SOURCES) fixture.write(source.path, source.content)
3677
+ const paths = POLICY_VUE_RELATIVE_SOURCES.map(({ path }) => path)
3678
+ const enabled = readImportDiagnostics(fixture.path, paths)
3679
+ expect(paths.filter((path) => enabled.has('eslint(no-restricted-imports) ' + path))).toEqual(
3680
+ [],
3681
+ )
3682
+ expect(paths.filter((path) => enabled.has('policy(no-cross-vue-face) ' + path))).toEqual([])
3683
+ const restoredConfiguration = readConfigRecord(JSON.parse(text))
3684
+ if (!Array.isArray(restoredConfiguration.overrides)) throw new Error('Missing lint overrides')
3685
+ const restored = restoredConfiguration.overrides.map((value: unknown) => {
3686
+ const block = readConfigRecord(value)
3687
+ const files = block.files
3688
+ const owner = POLICY_VUE_FACES.find(
3689
+ (face) =>
3690
+ Array.isArray(files) &&
3691
+ files.some((file: unknown) => typeof file === 'string' && file.startsWith(face + '/')),
3692
+ )
3693
+ if (owner === undefined) return block
3694
+ const original = POLICY_VUE_ORIGINAL_RELATIVES[owner]
3695
+ if (original === undefined) throw new Error('Missing original Vue relative pattern')
3696
+ const rules = readConfigRecord(block.rules)
3697
+ const rule = rules['no-restricted-imports']
3698
+ if (!Array.isArray(rule)) throw new Error('Missing Vue import restrictions')
3699
+ const options = readConfigRecord(rule[1])
3700
+ if (!Array.isArray(options.patterns)) throw new Error('Missing Vue import patterns')
3701
+ const patterns = options.patterns.map((patternValue: unknown): Record<string, unknown> => {
3702
+ const pattern = readConfigRecord(patternValue)
3703
+ if (pattern.message !== 'Vue faces must not depend on sibling sheet faces') return pattern
3704
+ if (typeof pattern.regex !== 'string' || !pattern.regex.endsWith(')'))
3705
+ throw new Error('Missing Vue sibling alias pattern')
3706
+ return { ...pattern, regex: pattern.regex.slice(0, -1) + '|' + original + ')' }
3707
+ })
3708
+ expect(
3709
+ patterns.filter(
3710
+ (pattern) => pattern.message === 'Vue faces must not depend on sibling sheet faces',
3711
+ ),
3712
+ ).toHaveLength(1)
3713
+ return {
3714
+ ...block,
3715
+ rules: { ...rules, 'no-restricted-imports': [rule[0], { ...options, patterns }] },
3716
+ }
3717
+ })
3718
+ expect(
3719
+ restored.filter(
3720
+ (block) => readConfigRecord(block.rules)['policy/no-cross-vue-face'] === 'error',
3721
+ ),
3722
+ ).toHaveLength(2)
3723
+ fixture.write(
3724
+ '.oxlintrc.json',
3725
+ JSON.stringify({ ...restoredConfiguration, overrides: restored }),
3726
+ )
3727
+ const control = readImportDiagnostics(fixture.path, paths)
3728
+ const refused = paths.filter((path) => control.has('eslint(no-restricted-imports) ' + path))
3729
+ expect(refused).toEqual(paths)
3730
+ expect(() => expect(refused).toEqual([])).toThrow('deeply equal')
3731
+ expect(paths.filter((path) => control.has('policy(no-cross-vue-face) ' + path))).toEqual([])
3732
+ } finally {
3733
+ fixture.destroy()
3734
+ }
3735
+ })
3736
+
3737
+ it('checks exact Vue composable membership and malformed modules through the canonical policy', () => {
3738
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-domain-' })
3739
+ try {
3740
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3741
+ fixture.write(
3742
+ 'configs/policy.ts',
3743
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3744
+ )
3745
+ const sources = POLICY_VUE_DOMAIN_CASES
3746
+ expect(sources).toContainEqual({
3747
+ ...POLICY_VUE_COMPOSABLE_SOURCE,
3748
+ misplaced: false,
3749
+ malformed: false,
3750
+ })
3751
+ for (const source of sources) fixture.write(source.path, source.content)
3752
+ const codes = readImportDiagnostics(
3753
+ fixture.path,
3754
+ sources.map(({ path }) => path),
3755
+ )
3756
+ expect(
3757
+ sources.map(({ path }) => ({
3758
+ path,
3759
+ misplaced: codes.has('policy(no-misplaced-function) ' + path),
3760
+ malformed: codes.has('policy(no-malformed-domain) ' + path),
3761
+ })),
3762
+ ).toEqual(sources.map(({ path, misplaced, malformed }) => ({ path, misplaced, malformed })))
3763
+ } finally {
3764
+ fixture.destroy()
3765
+ }
3766
+ })
3767
+
3768
+ it('detects a removed Vue composable registration in a scratch policy', () => {
3769
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-domain-control-' })
3770
+ try {
3771
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3772
+ const plugin = readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8')
3773
+ const registration = /^\t'app\/vue\/composables',\r?\n/gmu
3774
+ expect(plugin.match(registration)).toHaveLength(1)
3775
+ fixture.write('configs/policy.ts', plugin)
3776
+ const source = POLICY_VUE_COMPOSABLE_SOURCE
3777
+ fixture.write(source.path, source.content)
3778
+ const enabled = readImportDiagnostics(fixture.path, [source.path])
3779
+ expect(enabled.has('policy(no-misplaced-function) ' + source.path)).toBe(false)
3780
+ expect(enabled.has('policy(no-malformed-domain) ' + source.path)).toBe(false)
3781
+ fixture.write('configs/policy.ts', plugin.replace(registration, ''))
3782
+ const control = readImportDiagnostics(fixture.path, [source.path])
3783
+ expect(control.has('policy(no-misplaced-function) ' + source.path)).toBe(true)
3784
+ expect(control.has('policy(no-malformed-domain) ' + source.path)).toBe(false)
3785
+ expect(() =>
3786
+ expect(control.has('policy(no-misplaced-function) ' + source.path)).toBe(false),
3787
+ ).toThrow('expected')
3788
+ } finally {
3789
+ fixture.destroy()
3790
+ }
3791
+ })
3792
+
3793
+ it('admits nested same-face Vue relative imports through the canonical policy', () => {
3794
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-relative-' })
3795
+ try {
3796
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3797
+ fixture.write(
3798
+ 'configs/policy.ts',
3799
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3800
+ )
3801
+ for (const source of POLICY_VUE_RELATIVE_SOURCES) {
3802
+ fixture.write(source.path, source.content)
3803
+ }
3804
+ const paths = POLICY_VUE_RELATIVE_SOURCES.map(({ path }) => path)
3805
+ expect(paths).toContain('app/vue/components/Panel.vue')
3806
+ expect(paths).toContain('src/vue/features/helpers.ts')
3807
+ const codes = readImportDiagnostics(fixture.path, paths)
3808
+ const refused = paths.filter(
3809
+ (path) =>
3810
+ codes.has('eslint(no-restricted-imports) ' + path) ||
3811
+ codes.has('policy(no-cross-vue-face) ' + path),
3812
+ )
3813
+ expect(refused).toEqual([])
3814
+ } finally {
3815
+ fixture.destroy()
3816
+ }
3817
+ })
3818
+
3819
+ it('admits a valid direct application Vue composable through the canonical policy', () => {
3820
+ const fixture = createPolicyScratch({ prefix: 'policy-vue-composable-' })
3821
+ try {
3822
+ fixture.write('.oxlintrc.json', readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8'))
3823
+ fixture.write(
3824
+ 'configs/policy.ts',
3825
+ readFileSync(resolve(POLICY_ROOT, 'configs/policy.ts'), 'utf8'),
3826
+ )
3827
+ const source = POLICY_VUE_COMPOSABLE_SOURCE
3828
+ expect(source.path).toBe('app/vue/composables/useTheme.ts')
3829
+ fixture.write(source.path, source.content)
3830
+ const codes = readImportDiagnostics(fixture.path, [source.path])
3831
+ expect({
3832
+ misplaced: codes.has('policy(no-misplaced-function) ' + source.path),
3833
+ malformed: codes.has('policy(no-malformed-domain) ' + source.path),
3834
+ }).toEqual({ misplaced: false, malformed: false })
3835
+ } finally {
3836
+ fixture.destroy()
3837
+ }
3838
+ })
3839
+
3840
+ it('matches every isolated import pattern with refused and admitted fixtures', () => {
3841
+ const fixture = createPolicyScratch({ prefix: 'propagation-patterns-' })
3842
+ const scratch = fixture.path
3843
+ try {
3844
+ const configured = readConfigRecord(
3845
+ JSON.parse(readFileSync(resolve(POLICY_ROOT, '.oxlintrc.json'), 'utf8')),
3846
+ )
3847
+ if (!Array.isArray(configured.overrides)) throw new Error('Missing overrides')
3848
+ const built = configured.overrides.flatMap((value: unknown, blockIndex: number) => {
3849
+ const block = readConfigRecord(value)
3850
+ const rule = readConfigRecord(block.rules)['no-restricted-imports']
3851
+ if (!Array.isArray(rule)) return []
3852
+ const patterns = readConfigRecord(rule[1]).patterns
3853
+ if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
3854
+ const fromPatterns = patterns.map((patternValue: unknown, patternIndex: number) => {
3855
+ const pattern = readConfigRecord(patternValue)
3856
+ const message = pattern.message
3857
+ if (typeof message !== 'string') throw new Error('Missing pattern message')
3858
+ const alias = /@src\/(core|browser|server) alias/u.exec(message)?.[1]
3859
+ const fence = POLICY_IMPORT_CASES.find(({ marker }) => message.includes(marker))
3860
+ const chained: ReadonlyArray<readonly [string, boolean]> =
3861
+ alias !== undefined
3862
+ ? [
3863
+ [`../${alias}/index.js`, true],
3864
+ [`../../${alias}/types.js`, true],
3865
+ [`../../src/${alias}/index.js`, true],
3866
+ [`../${alias}`, true],
3867
+ [`../${alias}/`, true],
3868
+ [`../${alias}?raw`, true],
3869
+ [`../${alias}#fragment`, true],
3870
+ [`@src/${alias}`, false],
3871
+ [`./${alias}/index.js`, false],
3872
+ [`../${alias}x/index.js`, false],
3873
+ [`../${alias}.ext/index.js`, false],
3874
+ ]
3875
+ : message.includes('self-contained')
3876
+ ? [
3877
+ ['./helpers.ts', true],
3878
+ ['../../orkestrel-dispatch/scripts/helpers.ts', true],
3879
+ ['@orkestrel/test', true],
3880
+ ['fs', true],
3881
+ ['', true],
3882
+ ['n', true],
3883
+ ['no', true],
3884
+ ['nod', true],
3885
+ ['node', true],
3886
+ ['nodes:fs', true],
3887
+ ['node:fs', false],
3888
+ ['node:child_process', false],
2971
3889
  ]
2972
- : message.includes('noncanonical dot')
3890
+ : message.includes('URL schemes')
2973
3891
  ? [
2974
- ['../core/../server/index.js', true],
2975
- ['../core/./index.js', true],
2976
- ['../core/index.js', false],
2977
- ['./index.js', false],
2978
- ['../../core/index.js', false],
2979
- ['.../../index.js', true],
2980
- ['.name/../index.js', true],
3892
+ ['https://host/x', true],
3893
+ ['data:text/plain,x', true],
3894
+ ['node:fs', false],
3895
+ ['n:entry', true],
3896
+ ['no:entry', true],
3897
+ ['nod:entry', true],
3898
+ ['nodea:entry', true],
3899
+ ['1bad:entry', false],
3900
+ ['./data:entry', false],
2981
3901
  ]
2982
- : message.includes('sibling sheet')
3902
+ : message.includes('noncanonical dot')
2983
3903
  ? [
2984
- ['@src/print', true],
2985
- ['../print/index.js', true],
2986
- ['@src/core', false],
2987
- ['@src/browser', false],
2988
- ['@src/vue', false],
2989
- ['@app/core', false],
2990
- ['@app/browser', false],
2991
- ['@src/corex', true],
2992
- ['@src/', true],
2993
- ['../src/print/index.js', true],
2994
- ['../browser?x', true],
2995
- ['../print.ext/index.js', false],
2996
- ...(Array.isArray(block.files) &&
2997
- block.files.some(
2998
- (file: unknown) =>
2999
- typeof file === 'string' && file.startsWith('app/vue/'),
3000
- )
3001
- ? [['@app/print', true] as const]
3002
- : []),
3003
- ]
3004
- : [
3005
- [
3006
- message.includes('absolute paths')
3007
- ? '/machine/module.js'
3008
- : message.includes('normalized traversal')
3009
- ? './../core/index.js'
3010
- : message.includes('forward slashes')
3011
- ? '..\\core\\index.js'
3012
- : message.includes('compiler API')
3013
- ? 'typescript'
3014
- : message.includes('private app')
3015
- ? '@app/core'
3016
- : message.includes('stylesheets')
3017
- ? './index.css'
3018
- : message.includes('host-independent') ||
3019
- message.includes('Node or server')
3020
- ? 'node:fs'
3021
- : message.includes('Vue extension')
3022
- ? '@src/vue'
3023
- : '@src/browser',
3024
- true,
3025
- ],
3904
+ ['../core/../server/index.js', true],
3905
+ ['../core/./index.js', true],
3906
+ ['../core/index.js', false],
3907
+ ['./index.js', false],
3908
+ ['../../core/index.js', false],
3909
+ ['.../../index.js', true],
3910
+ ['.name/../index.js', true],
3026
3911
  ]
3912
+ : message.includes('sibling sheet')
3913
+ ? [
3914
+ ['@src/print', true],
3915
+ // Relative refusals now belong to the canonical filename-based matrix.
3916
+ ['@src/core', false],
3917
+ ['@src/browser', false],
3918
+ ['@src/vue', false],
3919
+ ['@app/core', false],
3920
+ ['@app/browser', false],
3921
+ ['@src/corex', true],
3922
+ ['@src/', true],
3923
+ // The app browser query is admitted there; src retains its alias refusal.
3924
+ ['../browser?x', false],
3925
+ ['../print.ext/index.js', false],
3926
+ ...(Array.isArray(block.files) &&
3927
+ block.files.some(
3928
+ (file: unknown) =>
3929
+ typeof file === 'string' && file.startsWith('app/vue/'),
3930
+ )
3931
+ ? [['@app/print', true] as const]
3932
+ : []),
3933
+ ]
3934
+ : [
3935
+ [
3936
+ message.includes('absolute paths')
3937
+ ? '/machine/module.js'
3938
+ : message.includes('normalized traversal')
3939
+ ? './../core/index.js'
3940
+ : message.includes('forward slashes')
3941
+ ? '..\\core\\index.js'
3942
+ : message.includes('compiler API')
3943
+ ? 'typescript'
3944
+ : message.includes('private app')
3945
+ ? '@app/core'
3946
+ : message.includes('stylesheets')
3947
+ ? './index.css'
3948
+ : message.includes('host-independent') ||
3949
+ message.includes('Node or server')
3950
+ ? 'node:fs'
3951
+ : message.includes('Vue extension')
3952
+ ? '@src/vue'
3953
+ : '@src/browser',
3954
+ true,
3955
+ ],
3956
+ ]
3957
+ // A setup-module fence lists its own refused and admitted specifiers in the setup module.
3958
+ const listed = fence?.cases ?? chained
3027
3959
  const face =
3028
3960
  message.includes('another styles face') && typeof pattern.regex === 'string'
3029
3961
  ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
@@ -3044,24 +3976,31 @@ createSomething({
3044
3976
  [`@src/${face}x`, false],
3045
3977
  [`../${face}.ext/index.js`, false],
3046
3978
  ]
3979
+ // A per-file glob such as `.agents/skills/*/scripts/*.ts` or `tests/setup.ts` names a
3980
+ // directory once its file part is dropped and each wildcard segment takes a sample name.
3047
3981
  const owner =
3048
3982
  Array.isArray(block.files) && typeof block.files[0] === 'string'
3049
- ? block.files[0].split('/**')[0]
3983
+ ? block.files[0]
3984
+ .split('/**')[0]
3985
+ ?.replace(/\/[^/]*\.[a-z]+$/u, '')
3986
+ .replaceAll('*', 'sample')
3050
3987
  : undefined
3051
3988
  if (owner === undefined) throw new Error('Missing pattern owner')
3052
- const cases = [...sources, ['./index.js', false] as const].map(
3053
- ([source, refusal], index) => {
3054
- const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3055
- mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3056
- writeFileSync(
3057
- resolve(scratch, path),
3058
- 'import * as boundary from ' +
3059
- JSON.stringify(source) +
3060
- '\nvoid boundary\ndebugger\n',
3061
- )
3062
- return { path, refusal }
3063
- },
3064
- )
3989
+ // Every relative specifier is refused in a self-contained script and in the policy
3990
+ // plugin, which imports nothing, so their admitted controls are the rows above.
3991
+ const controls =
3992
+ message.includes('self-contained') || fence?.controlled === false
3993
+ ? []
3994
+ : [['./index.js', false] as const]
3995
+ const cases = [...sources, ...controls].map(([source, refusal], index) => {
3996
+ const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3997
+ mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3998
+ writeFileSync(
3999
+ resolve(scratch, path),
4000
+ 'import * as boundary from ' + JSON.stringify(source) + '\nvoid boundary\ndebugger\n',
4001
+ )
4002
+ return { path, refusal }
4003
+ })
3065
4004
  return {
3066
4005
  cases,
3067
4006
  override: {
@@ -3070,7 +4009,60 @@ createSomething({
3070
4009
  },
3071
4010
  }
3072
4011
  })
4012
+ // A path entry restricts named imports of one module, so each case imports one clause.
4013
+ const entries = readConfigRecord(rule[1]).paths
4014
+ const fromPaths = (Array.isArray(entries) ? entries : []).map(
4015
+ (entryValue: unknown, entryIndex: number) => {
4016
+ const entry = readConfigRecord(entryValue)
4017
+ const message = entry.message
4018
+ if (typeof message !== 'string' || typeof entry.name !== 'string') {
4019
+ throw new Error('Missing path entry message or module')
4020
+ }
4021
+ const named = POLICY_NAME_CASES.find(({ marker }) => message.includes(marker))
4022
+ if (named === undefined) throw new Error('Missing named import cases for a path entry')
4023
+ const owner =
4024
+ Array.isArray(block.files) && typeof block.files[0] === 'string'
4025
+ ? block.files[0].replace(/\/[^/]*\.[a-z]+$/u, '').replaceAll('*', 'sample')
4026
+ : undefined
4027
+ if (owner === undefined) throw new Error('Missing path owner')
4028
+ const cases = [
4029
+ ...named.refused.map((clause): [string, boolean] => [clause, true]),
4030
+ ...named.admitted.map((clause): [string, boolean] => [clause, false]),
4031
+ ].map(([clause, refusal], index) => {
4032
+ const path = `${owner}/path${blockIndex}_${entryIndex}_${index}.ts`
4033
+ mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
4034
+ writeFileSync(
4035
+ resolve(scratch, path),
4036
+ `import { ${clause} } from ${JSON.stringify(entry.name)}\ndebugger\n`,
4037
+ )
4038
+ return { path, refusal }
4039
+ })
4040
+ return {
4041
+ cases,
4042
+ override: {
4043
+ files: cases.map(({ path }) => path),
4044
+ rules: { 'no-restricted-imports': ['error', { paths: [entry] }] },
4045
+ },
4046
+ }
4047
+ },
4048
+ )
4049
+ return [...fromPatterns, ...fromPaths]
3073
4050
  })
4051
+ // Each fence keeps a pattern or path entry in the configuration, so deleting one fails here
4052
+ // rather than leaving the loop above with nothing to read.
4053
+ const messages = configured.overrides.flatMap((value: unknown) => {
4054
+ const rule = readConfigRecord(readConfigRecord(value).rules)['no-restricted-imports']
4055
+ if (!Array.isArray(rule)) return []
4056
+ const options = readConfigRecord(rule[1])
4057
+ return [options.patterns, options.paths]
4058
+ .flatMap((list: unknown) => (Array.isArray(list) ? list : []))
4059
+ .map((entry: unknown) => String(readConfigRecord(entry).message))
4060
+ })
4061
+ expect(
4062
+ [...POLICY_IMPORT_CASES, ...POLICY_NAME_CASES]
4063
+ .map(({ marker }) => marker)
4064
+ .filter((marker) => !messages.some((message) => message.includes(marker))),
4065
+ ).toEqual([])
3074
4066
  const overrides = built.map(({ override }) => override)
3075
4067
  const expected = new Map(
3076
4068
  built.flatMap(({ cases }) =>
@@ -3082,12 +4074,12 @@ createSomething({
3082
4074
  JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
3083
4075
  )
3084
4076
  const codes = readImportDiagnostics(scratch, [...expected.keys()])
3085
- const readings = [...expected].map(([path, refusal]) => ({
4077
+ const verdicts = [...expected].map(([path, refusal]) => ({
3086
4078
  path,
3087
4079
  expected: refusal,
3088
4080
  actual: codes.has('eslint(no-restricted-imports) ' + path),
3089
4081
  }))
3090
- expect(readings.filter((reading) => reading.expected !== reading.actual)).toEqual([])
4082
+ expect(verdicts.filter((verdict) => verdict.expected !== verdict.actual)).toEqual([])
3091
4083
  writeFileSync(
3092
4084
  resolve(scratch, '.oxlintrc.json'),
3093
4085
  JSON.stringify({ rules: { 'no-debugger': 'error', 'no-restricted-imports': 'off' } }),
@@ -3299,19 +4291,23 @@ createSomething({
3299
4291
  const [enabled, disabled] = reports
3300
4292
  if (enabled === undefined || disabled === undefined)
3301
4293
  throw new Error('Missing boundary readings')
3302
- const readings = [...expected].map(([path, refusal]) => ({
4294
+ const verdicts = [...expected].map(([path, refusal]) => ({
3303
4295
  path,
3304
4296
  expected: refusal,
3305
- actual: enabled.has('eslint(no-restricted-imports) ' + path),
4297
+ actual:
4298
+ enabled.has('eslint(no-restricted-imports) ' + path) ||
4299
+ enabled.has('policy(no-cross-vue-face) ' + path),
3306
4300
  }))
3307
- expect(readings.filter((reading) => reading.actual !== reading.expected)).toEqual([])
4301
+ expect(verdicts.filter((verdict) => verdict.actual !== verdict.expected)).toEqual([])
3308
4302
  const control = [...expected].map(([path, refusal]) => ({
3309
4303
  path,
3310
4304
  expected: refusal,
3311
- actual: disabled.has('eslint(no-restricted-imports) ' + path),
4305
+ actual:
4306
+ disabled.has('eslint(no-restricted-imports) ' + path) ||
4307
+ disabled.has('policy(no-cross-vue-face) ' + path),
3312
4308
  }))
3313
4309
  expect(() =>
3314
- expect(control.filter((reading) => reading.actual !== reading.expected)).toEqual([]),
4310
+ expect(control.filter((verdict) => verdict.actual !== verdict.expected)).toEqual([]),
3315
4311
  ).toThrow('deeply equal')
3316
4312
  expect(control).toContainEqual({ path: 'src/vue/refused3.ts', expected: true, actual: false })
3317
4313
  expect(control).toContainEqual({ path: 'src/vue/refused8.ts', expected: true, actual: false })
@@ -3329,13 +4325,53 @@ createSomething({
3329
4325
  const declared = Object.getOwnPropertyNames(rules)
3330
4326
  expect(declared).toContain('no-misplaced-function')
3331
4327
  expect(declared).toContain('no-host-line-endings')
4328
+ expect(declared).toContain('no-cross-vue-face')
4329
+ expect(declared).toContain('no-malformed-script')
4330
+ expect(declared).toContain('no-mutable-property')
3332
4331
  expect(
3333
4332
  inspectPolicyWiring(
3334
4333
  parsed,
3335
4334
  declared.map((name) => `policy/${name}`),
3336
- [POLICY_PLACEMENT_GLOBS, POLICY_ENDING_GLOBS],
4335
+ [POLICY_PLACEMENT_GLOBS, POLICY_ENDING_GLOBS, POLICY_SCRIPT_GLOBS, POLICY_NESTED_GLOBS],
3337
4336
  ),
3338
4337
  ).toEqual([])
4338
+ // The controls: a configuration with the skill-script override or the nested-function
4339
+ // override removed reports the population it no longer declares.
4340
+ const overrides: unknown = Object.getOwnPropertyDescriptor(parsed, 'overrides')?.value
4341
+ if (!Array.isArray(overrides)) throw new Error('The Oxlint configuration has no overrides')
4342
+ for (const population of [POLICY_SCRIPT_GLOBS, POLICY_NESTED_GLOBS]) {
4343
+ const trimmed = {
4344
+ ...readConfigRecord(parsed),
4345
+ overrides: overrides.filter((override: unknown) => {
4346
+ const files = readConfigRecord(override).files
4347
+ return !Array.isArray(files) || files.join(' ') !== population.join(' ')
4348
+ }),
4349
+ }
4350
+ expect(
4351
+ inspectPolicyWiring(
4352
+ trimmed,
4353
+ [],
4354
+ [POLICY_PLACEMENT_GLOBS, POLICY_ENDING_GLOBS, POLICY_SCRIPT_GLOBS, POLICY_NESTED_GLOBS],
4355
+ ),
4356
+ ).toEqual([`no override files list declares the population exactly: ${population.join(' ')}`])
4357
+ }
4358
+ // The control: the script override keeps its exact population while the rule it enables is
4359
+ // set to off, which names the rule as enabled by no record.
4360
+ const disabled = {
4361
+ ...readConfigRecord(parsed),
4362
+ overrides: overrides.map((override: unknown) => {
4363
+ const record = readConfigRecord(override)
4364
+ return Object.hasOwn(readConfigRecord(record.rules ?? {}), 'policy/no-malformed-script')
4365
+ ? {
4366
+ ...record,
4367
+ rules: { ...readConfigRecord(record.rules), 'policy/no-malformed-script': 'off' },
4368
+ }
4369
+ : record
4370
+ }),
4371
+ }
4372
+ expect(
4373
+ inspectPolicyWiring(disabled, ['policy/no-malformed-script'], [POLICY_SCRIPT_GLOBS]),
4374
+ ).toEqual(['policy/no-malformed-script is enabled by no top-level or override rules record'])
3339
4375
  })
3340
4376
 
3341
4377
  it('loads every configured policy rule through the real binary', () => {
@@ -3364,6 +4400,15 @@ createSomething({
3364
4400
  "export const STATUS = 'ready'",
3365
4401
  "export const lines = text.trim().split('\\n')",
3366
4402
  'export const ending = os.EOL + EOL',
4403
+ 'let counter = 0',
4404
+ 'counter++',
4405
+ 'let unchanged = 1',
4406
+ 'var legacy = 1',
4407
+ 'function shiftValue(value: number) { value += 1; return value }',
4408
+ 'try { void counter } catch (error) { error = undefined; void error }',
4409
+ 'void unchanged',
4410
+ 'void legacy',
4411
+ 'void shiftValue',
3367
4412
  'void PrivateMember',
3368
4413
  'void ParameterMember',
3369
4414
  'void PublicMember',
@@ -3380,6 +4425,181 @@ createSomething({
3380
4425
  scratch.write('src/violations/constants.ts', 'export const values = []\n')
3381
4426
  scratch.write('src/violations/composables.ts', "export const READY = 'yes'\n")
3382
4427
  scratch.write('app/browser/composables/useTheme.ts', 'export function useMode(): void {}\n')
4428
+ scratch.write('app/vue/helpers.ts', "export * from '../print/index.js'\ndebugger\n")
4429
+ scratch.write(
4430
+ '.agents/skills/sample/scripts/shared.ts',
4431
+ ["import { readOption } from './helpers.ts'", 'void readOption'].join('\n'),
4432
+ )
4433
+ // The fence reads a dynamic specifier as it reads a static one, and the empty specifier opens
4434
+ // with no `node:` as every other refused specifier does.
4435
+ scratch.write(
4436
+ '.agents/skills/sample/scripts/dynamic.ts',
4437
+ [
4438
+ '/** Loads a sibling. */',
4439
+ "export function load(): Promise<unknown> { return import('./other.ts') }",
4440
+ ].join('\n'),
4441
+ )
4442
+ scratch.write(
4443
+ '.agents/skills/sample/scripts/empty.ts',
4444
+ [
4445
+ '/** Loads nothing. */',
4446
+ "export function load(): Promise<unknown> { return import('') }",
4447
+ ].join('\n'),
4448
+ )
4449
+ scratch.write(
4450
+ '.agents/skills/sample/scripts/alone.ts',
4451
+ ["import { readFileSync } from 'node:fs'", 'void readFileSync'].join('\n'),
4452
+ )
4453
+ // A script that hides a declaration, exports one with no doc block, and runs at import draws
4454
+ // every message; the conforming script beside it draws none, and its `debugger` statement
4455
+ // sits inside a function, outside the top-level statements the script rule reads, so the
4456
+ // root `no-debugger` rule proves the file entered the run.
4457
+ scratch.write(
4458
+ '.agents/skills/sample/scripts/malformed.ts',
4459
+ [
4460
+ 'function hidden(): void {}',
4461
+ 'export function bare(): void {}',
4462
+ 'hidden()',
4463
+ 'bare()',
4464
+ ].join('\n'),
4465
+ )
4466
+ scratch.write(
4467
+ '.agents/skills/sample/scripts/conforming.ts',
4468
+ [
4469
+ "import { realpathSync } from 'node:fs'",
4470
+ '',
4471
+ '/** Reads the one value. */',
4472
+ 'export function read(): number {',
4473
+ '\tdebugger',
4474
+ '\treturn 1',
4475
+ '}',
4476
+ '',
4477
+ '/** Runs the script. */',
4478
+ 'export function main(): number {',
4479
+ '\treturn read()',
4480
+ '}',
4481
+ '',
4482
+ 'if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {',
4483
+ '\tprocess.exitCode = main()',
4484
+ '}',
4485
+ ].join('\n'),
4486
+ )
4487
+ // Every form of write to a parameter or a catch binding draws the built-in rule, one file per
4488
+ // form so that a form the binary misses names itself.
4489
+ const reassignments = [
4490
+ {
4491
+ file: 'param-update',
4492
+ code: 'no-param-reassign',
4493
+ source: 'export function f(a: number) { a++; return a }',
4494
+ },
4495
+ {
4496
+ file: 'param-array-target',
4497
+ code: 'no-param-reassign',
4498
+ source: 'export function f(b: number[]) { ;[b] = [[1]]; return b }',
4499
+ },
4500
+ {
4501
+ file: 'param-object-target',
4502
+ code: 'no-param-reassign',
4503
+ source: 'export function f(c: object) { ;({ c } = { c: {} }); return c }',
4504
+ },
4505
+ {
4506
+ file: 'param-for-in-head',
4507
+ code: 'no-param-reassign',
4508
+ source: 'export function f(e: string, c: object) { for (e in c) void e; return e }',
4509
+ },
4510
+ {
4511
+ file: 'param-for-of-head',
4512
+ code: 'no-param-reassign',
4513
+ source: "export function f(e: string) { for (e of ['x']) void e; return e }",
4514
+ },
4515
+ {
4516
+ file: 'catch-update',
4517
+ code: 'no-ex-assign',
4518
+ source: 'try { void 0 } catch (err) { err++ }',
4519
+ },
4520
+ {
4521
+ file: 'catch-destructured',
4522
+ code: 'no-ex-assign',
4523
+ source: "try { void 0 } catch ({ message }) { message = 'x' }",
4524
+ },
4525
+ {
4526
+ file: 'catch-for-of-head',
4527
+ code: 'no-ex-assign',
4528
+ source: 'try { void 0 } catch (err) { for (err of [1]) void err }',
4529
+ },
4530
+ ].map(({ file, code, source }) => ({
4531
+ code: `eslint(${code})`,
4532
+ filename: `src/violations/reassign/${file}.ts`,
4533
+ source,
4534
+ }))
4535
+ for (const { filename, source } of reassignments) scratch.write(filename, source)
4536
+ // An ambient block holds a `let` with no value and no `declare` flag of its own, and the
4537
+ // `debugger` statement proves the file entered the run the absence assertion reads.
4538
+ scratch.write(
4539
+ 'src/violations/ambient.ts',
4540
+ [
4541
+ 'declare global { let ambientGlobal: number }',
4542
+ "declare module 'ambient-module' { export let ambientExport: string }",
4543
+ 'debugger',
4544
+ 'export {}',
4545
+ ].join('\n'),
4546
+ )
4547
+ scratch.write(
4548
+ 'src/violations/types.ts',
4549
+ [
4550
+ '/** Describes a mutable record. */',
4551
+ 'export interface MutableRecord { id: string }',
4552
+ '/** Describes a frozen record. */',
4553
+ 'export interface FrozenRecord { readonly id: string }',
4554
+ '/** Aliases an object type. */',
4555
+ 'export type MutableAlias = { value: number }',
4556
+ '/** Reads one field through an inline parameter type. */',
4557
+ 'export type Reader = (options: { id: string }) => string',
4558
+ ].join('\n'),
4559
+ )
4560
+ // The setup-module fences replace the generic tests restriction for the files they name,
4561
+ // and the proofs beside them keep the generic one. The `debugger` statement proves each file
4562
+ // entered the run.
4563
+ scratch.write(
4564
+ 'tests/setup.ts',
4565
+ ["import { readFileSync } from 'node:fs'", "import { ref } from 'vue'", 'debugger'].join(
4566
+ '\n',
4567
+ ),
4568
+ )
4569
+ scratch.write(
4570
+ 'tests/setupBrowser.ts',
4571
+ [
4572
+ "import { describe, it } from 'vitest'",
4573
+ "import { readFileSync } from 'node:fs'",
4574
+ 'debugger',
4575
+ ].join('\n'),
4576
+ )
4577
+ scratch.write(
4578
+ 'tests/setupServer.ts',
4579
+ ["import { expect } from 'vitest'", 'debugger'].join('\n'),
4580
+ )
4581
+ scratch.write(
4582
+ 'tests/setupPolicy.ts',
4583
+ ["import { readFileSync } from 'node:fs'", "import { vi } from 'vitest'", 'debugger'].join(
4584
+ '\n',
4585
+ ),
4586
+ )
4587
+ scratch.write(
4588
+ 'tests/setupStyles.ts',
4589
+ ["import { TestRunner } from 'vitest'", 'debugger'].join('\n'),
4590
+ )
4591
+ scratch.write(
4592
+ 'tests/setup.test.ts',
4593
+ ["import { describe, expect, it } from 'vitest'", 'debugger'].join('\n'),
4594
+ )
4595
+ scratch.write(
4596
+ 'tests/titles.test.ts',
4597
+ [
4598
+ "import { expect, it } from 'vitest'",
4599
+ "it('reads one value', () => expect(1).toBe(1))",
4600
+ "it('reads one value', () => expect(2).toBe(2))",
4601
+ ].join('\n'),
4602
+ )
3383
4603
  // The line-ending population reaches src, app, and configs alone, so this module
3384
4604
  // outside them carries the same defect and must draw no diagnostic. The `debugger`
3385
4605
  // statement is the arrival control the root `no-debugger` rule reports, which proves
@@ -3432,7 +4652,18 @@ createSomething({
3432
4652
  const config = resolve(scratch.path, '.oxlintrc.json')
3433
4653
  const violations = spawnSync(
3434
4654
  process.execPath,
3435
- [binary, '--config', config, '--format', 'json', 'src/violations', 'app', 'scripts'],
4655
+ [
4656
+ binary,
4657
+ '--config',
4658
+ config,
4659
+ '--format',
4660
+ 'json',
4661
+ 'src/violations',
4662
+ 'app',
4663
+ 'scripts',
4664
+ 'tests',
4665
+ '.agents',
4666
+ ],
3436
4667
  { cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
3437
4668
  )
3438
4669
  const clean = spawnSync(
@@ -3470,7 +4701,33 @@ createSomething({
3470
4701
  for (const { code, filename } of [
3471
4702
  { code: 'policy(no-mocking)', filename: 'src/violations/fixture.ts' },
3472
4703
  { code: 'policy(no-keyword-privacy)', filename: 'src/violations/fixture.ts' },
4704
+ { code: 'policy(no-let)', filename: 'src/violations/fixture.ts' },
4705
+ { code: 'eslint(prefer-const)', filename: 'src/violations/fixture.ts' },
4706
+ { code: 'eslint(no-var)', filename: 'src/violations/fixture.ts' },
4707
+ { code: 'eslint(no-param-reassign)', filename: 'src/violations/fixture.ts' },
4708
+ { code: 'eslint(no-ex-assign)', filename: 'src/violations/fixture.ts' },
4709
+ { code: 'vitest(no-identical-title)', filename: 'tests/titles.test.ts' },
4710
+ {
4711
+ code: 'eslint(no-restricted-imports)',
4712
+ filename: '.agents/skills/sample/scripts/shared.ts',
4713
+ },
4714
+ {
4715
+ code: 'eslint(no-restricted-imports)',
4716
+ filename: '.agents/skills/sample/scripts/dynamic.ts',
4717
+ },
4718
+ {
4719
+ code: 'eslint(no-restricted-imports)',
4720
+ filename: '.agents/skills/sample/scripts/empty.ts',
4721
+ },
3473
4722
  { code: 'policy(no-nested-functions)', filename: 'src/violations/fixture.ts' },
4723
+ {
4724
+ code: 'policy(no-malformed-script)',
4725
+ filename: '.agents/skills/sample/scripts/malformed.ts',
4726
+ },
4727
+ { code: 'policy(no-mutable-property)', filename: 'src/violations/types.ts' },
4728
+ { code: 'eslint(no-restricted-imports)', filename: 'tests/setup.ts' },
4729
+ { code: 'eslint(no-restricted-imports)', filename: 'tests/setupBrowser.ts' },
4730
+ { code: 'eslint(no-restricted-imports)', filename: 'tests/setupServer.ts' },
3474
4731
  { code: 'policy(no-misplaced-type)', filename: 'src/violations/fixture.ts' },
3475
4732
  { code: 'policy(no-misplaced-data)', filename: 'src/violations/fixture.ts' },
3476
4733
  { code: 'policy(no-misplaced-function)', filename: 'src/violations/fixture.ts' },
@@ -3482,6 +4739,8 @@ createSomething({
3482
4739
  { code: 'policy(no-misnamed-plugin)', filename: 'src/violations/plugins.ts' },
3483
4740
  { code: 'policy(no-malformed-constant)', filename: 'src/violations/constants.ts' },
3484
4741
  { code: 'policy(no-malformed-domain)', filename: 'src/violations/composables.ts' },
4742
+ { code: 'policy(no-cross-vue-face)', filename: 'app/vue/helpers.ts' },
4743
+ { code: 'eslint(no-debugger)', filename: 'app/vue/helpers.ts' },
3485
4744
  {
3486
4745
  code: 'policy(no-malformed-domain)',
3487
4746
  filename: 'app/browser/composables/useTheme.ts',
@@ -3498,12 +4757,55 @@ createSomething({
3498
4757
  `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`,
3499
4758
  )
3500
4759
  }
4760
+ for (const { code, filename } of reassignments) {
4761
+ expect(violationCodes).toContain(
4762
+ `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`,
4763
+ )
4764
+ }
4765
+ expect(violationCodes).toContain(
4766
+ `eslint(no-debugger) ${normalizePolicyFilename(comparisonRoot, 'src/violations/ambient.ts')}`,
4767
+ )
4768
+ expect(violationCodes).not.toContain(
4769
+ `policy(no-let) ${normalizePolicyFilename(comparisonRoot, 'src/violations/ambient.ts')}`,
4770
+ )
3501
4771
  expect(violationCodes).toContain(
3502
4772
  `eslint(no-debugger) ${normalizePolicyFilename(comparisonRoot, 'scripts/read.ts')}`,
3503
4773
  )
3504
4774
  expect(violationCodes).not.toContain(
3505
4775
  `policy(no-host-line-endings) ${normalizePolicyFilename(comparisonRoot, 'scripts/read.ts')}`,
3506
4776
  )
4777
+ expect(violationCodes).not.toContain(
4778
+ `eslint(no-restricted-imports) ${normalizePolicyFilename(comparisonRoot, '.agents/skills/sample/scripts/alone.ts')}`,
4779
+ )
4780
+ // The conforming script arrives, through the `debugger` statement inside its function, and
4781
+ // draws no script diagnostic; the inline parameter type beside the two mutable properties
4782
+ // draws none either, so the types file draws exactly two.
4783
+ expect(violationCodes).toContain(
4784
+ `eslint(no-debugger) ${normalizePolicyFilename(comparisonRoot, '.agents/skills/sample/scripts/conforming.ts')}`,
4785
+ )
4786
+ expect(violationCodes).not.toContain(
4787
+ `policy(no-malformed-script) ${normalizePolicyFilename(comparisonRoot, '.agents/skills/sample/scripts/conforming.ts')}`,
4788
+ )
4789
+ expect(
4790
+ violationCodes.filter(
4791
+ (code) =>
4792
+ code ===
4793
+ `policy(no-mutable-property) ${normalizePolicyFilename(comparisonRoot, 'src/violations/types.ts')}`,
4794
+ ),
4795
+ ).toHaveLength(2)
4796
+ // The fences replace only the restriction of the files they name: a setup module that
4797
+ // imports a Node module or a vitest helper the law admits, a browser-enabled helper module
4798
+ // the law admits, and a setup proof that registers tests all draw the root arrival
4799
+ // diagnostic and no import restriction.
4800
+ for (const filename of [
4801
+ 'tests/setupPolicy.ts',
4802
+ 'tests/setupStyles.ts',
4803
+ 'tests/setup.test.ts',
4804
+ ]) {
4805
+ const normalized = normalizePolicyFilename(comparisonRoot, filename)
4806
+ expect(violationCodes).toContain(`eslint(no-debugger) ${normalized}`)
4807
+ expect(violationCodes).not.toContain(`eslint(no-restricted-imports) ${normalized}`)
4808
+ }
3507
4809
  expect(clean.status).toBe(0)
3508
4810
  expect(cleanCodes).toHaveLength(0)
3509
4811
  } finally {
@@ -3673,6 +4975,7 @@ describe('configuration helpers', () => {
3673
4975
  'readBoundedFile',
3674
4976
  'readHandleBytes',
3675
4977
  'readCompilerOutput',
4978
+ 'resolveCompiler',
3676
4979
  'rewriteCoreSpecifier',
3677
4980
  'sourceFallback',
3678
4981
  'workspacePath',
@@ -3780,7 +5083,7 @@ describe('configuration helpers', () => {
3780
5083
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
3781
5084
  try {
3782
5085
  // Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
3783
- const readings = [
5086
+ const results = [
3784
5087
  { text: 'abc', offset: 0, limit: 4 },
3785
5088
  { text: 'abcd', offset: 0, limit: 4 },
3786
5089
  { text: 'abcde', offset: 0, limit: 4 },
@@ -3800,8 +5103,8 @@ describe('configuration helpers', () => {
3800
5103
  })
3801
5104
  // A file at or under the limit answers its length; the shifted read adds the offset to the
3802
5105
  // length and fills the buffer past it; a file over the limit answers nothing.
3803
- expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
3804
- expect(readings.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
5106
+ expect(results.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
5107
+ expect(results.map(({ filled }) => filled)).toStrictEqual(['abc', 'abcd', '', '', 'abc', ''])
3805
5108
  } finally {
3806
5109
  scratch.destroy()
3807
5110
  }
@@ -3854,18 +5157,18 @@ describe('configuration helpers', () => {
3854
5157
  logLevel: 'silent',
3855
5158
  build: { outDir: 'out', lib: { entry, formats: ['es'] } },
3856
5159
  }
3857
- const building = new configHelpers.BuildState()
3858
- building.resolve(await resolveConfig(inline, 'build'))
3859
- expect(building.building).toBe(true)
3860
- expect(building.output).toBe(resolve(scratch.path, 'out'))
3861
- expect(building.root).toBe(realpathSync.native(scratch.path))
3862
- expect(building.source).toBe(entry)
5160
+ const buildState = new configHelpers.BuildState()
5161
+ buildState.resolve(await resolveConfig(inline, 'build'))
5162
+ expect(buildState.building).toBe(true)
5163
+ expect(buildState.output).toBe(resolve(scratch.path, 'out'))
5164
+ expect(buildState.root).toBe(realpathSync.native(scratch.path))
5165
+ expect(buildState.source).toBe(entry)
3863
5166
 
3864
5167
  // The control: a serve command is not a build, and a config with no library entry has no source.
3865
- const serving = new configHelpers.BuildState()
3866
- serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
3867
- expect(serving.building).toBe(false)
3868
- expect(serving.source).toBe('')
5168
+ const serveState = new configHelpers.BuildState()
5169
+ serveState.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
5170
+ expect(serveState.building).toBe(false)
5171
+ expect(serveState.source).toBe('')
3869
5172
  } finally {
3870
5173
  scratch.destroy()
3871
5174
  }
@@ -4060,6 +5363,65 @@ describe('configuration helpers', () => {
4060
5363
  }
4061
5364
  })
4062
5365
 
5366
+ it('resolves the installed compiler through bin.tsc when private package paths are unexported', () => {
5367
+ const compiler = configHelpers.resolveCompiler(import.meta.url)
5368
+ expect(configHelpers.readCompilerOutput(compiler, ['--version'])).toMatch(/^Version \d+\./u)
5369
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-compiler-exports-' })
5370
+ try {
5371
+ const root = join(scratch.path, 'node_modules/typescript')
5372
+ scratch.write('package.json', '{"type":"module"}')
5373
+ scratch.write(
5374
+ 'node_modules/typescript/package.json',
5375
+ JSON.stringify({
5376
+ name: 'typescript',
5377
+ exports: { './package.json': './package.json' },
5378
+ bin: { tsc: relative(root, compiler) },
5379
+ }),
5380
+ )
5381
+ const path = join(scratch.path, 'package.json')
5382
+ expect(() => createRequire(path).resolve('typescript/bin/tsc')).toThrow(
5383
+ 'not defined by "exports"',
5384
+ )
5385
+ expect(configHelpers.resolveCompiler(path)).toBe(compiler)
5386
+ expect(
5387
+ configHelpers.readCompilerOutput(configHelpers.resolveCompiler(path), ['--version']),
5388
+ ).toMatch(/^Version \d+\./u)
5389
+ scratch.write('consumer.ts', 'const value: string = 1\nconsole.log(value)\n')
5390
+ const refused = spawnSync(
5391
+ process.execPath,
5392
+ [compiler, '--noEmit', '--pretty', 'false', '--skipLibCheck', 'consumer.ts'],
5393
+ { cwd: scratch.path, encoding: 'utf8', windowsHide: true },
5394
+ )
5395
+ expect(refused.status).toBeGreaterThan(0)
5396
+ expect(refused.stdout).toContain('error TS2322')
5397
+ scratch.write('consumer.ts', "const value: string = 'accepted'\nconsole.log(value)\n")
5398
+ const accepted = spawnSync(
5399
+ process.execPath,
5400
+ [compiler, '--noEmit', '--pretty', 'false', '--skipLibCheck', 'consumer.ts'],
5401
+ { cwd: scratch.path, encoding: 'utf8', windowsHide: true },
5402
+ )
5403
+ expect(accepted.status, `${accepted.stdout}${accepted.stderr}`).toBe(0)
5404
+ } finally {
5405
+ scratch.destroy()
5406
+ }
5407
+ })
5408
+
5409
+ it.each([{}, { bin: null }, { bin: { tsc: 7 } }])(
5410
+ 'refuses a compiler manifest without a bin.tsc command: %s',
5411
+ (declared) => {
5412
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-compiler-manifest-' })
5413
+ try {
5414
+ scratch.write('package.json', '{"type":"module"}')
5415
+ scratch.write('node_modules/typescript/package.json', JSON.stringify(declared))
5416
+ expect(() => configHelpers.resolveCompiler(join(scratch.path, 'package.json'))).toThrow(
5417
+ 'The TypeScript package declares no bin.tsc command',
5418
+ )
5419
+ } finally {
5420
+ scratch.destroy()
5421
+ }
5422
+ },
5423
+ )
5424
+
4063
5425
  // The scope reading applies only where the workspace publishes source from `src`, which is what
4064
5426
  // `publishes` reads, and the name ends at that mechanism. An absent face project is the defect
4065
5427
  // this case exists to report rather than a second reason to excuse it, so a workspace holding
@@ -4071,7 +5433,7 @@ describe('configuration helpers', () => {
4071
5433
  it.skipIf(!POLICY_PUBLISHES)(
4072
5434
  'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
4073
5435
  () => {
4074
- const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
5436
+ const compiler = configHelpers.resolveCompiler(import.meta.url)
4075
5437
  // The order mirrors ENVIRONMENTS in src/core/constants.ts; a server-only workspace vendors
4076
5438
  // no core project, so this walks to the first face the workspace actually carries.
4077
5439
  const faces = ['core', 'browser', 'server']
@@ -4131,7 +5493,7 @@ describe('configuration helpers', () => {
4131
5493
  // reading a face project must not excuse it from the helpers behind the roll-up, which it
4132
5494
  // vendors whether or not it publishes.
4133
5495
  it('reads the refusals, guards, overrides, and rewrites a declaration roll-up requires from every workspace', () => {
4134
- const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
5496
+ const compiler = configHelpers.resolveCompiler(import.meta.url)
4135
5497
  // Any project path resolves these, because each reading is refused by the text before the
4136
5498
  // path is read. The root project is the one file every workspace carries.
4137
5499
  const project = resolve(POLICY_ROOT, 'tsconfig.json')
@@ -4295,8 +5657,15 @@ describe('configuration helpers', () => {
4295
5657
  const rewritten = join(workspace, 'rewritten')
4296
5658
  const kept = join(workspace, 'kept')
4297
5659
  const builds = [
4298
- { output: rewritten, rewrite: true },
4299
- { output: kept, rewrite: false },
5660
+ {
5661
+ output: rewritten,
5662
+ plugin: configHelpers.declarationRollup({
5663
+ project,
5664
+ rewrite: (text) =>
5665
+ configHelpers.rewriteBrowserSpecifier(configHelpers.rewriteCoreSpecifier(text)),
5666
+ }),
5667
+ },
5668
+ { output: kept, plugin: configHelpers.declarationRollup({ project }) },
4300
5669
  ]
4301
5670
  for (const face of builds) {
4302
5671
  await build({
@@ -4304,19 +5673,7 @@ describe('configuration helpers', () => {
4304
5673
  configFile: false,
4305
5674
  logLevel: 'silent',
4306
5675
  publicDir: false,
4307
- plugins: [
4308
- configHelpers.declarationRollup(
4309
- face.rewrite
4310
- ? {
4311
- project,
4312
- rewrite: (text) =>
4313
- configHelpers.rewriteBrowserSpecifier(
4314
- configHelpers.rewriteCoreSpecifier(text),
4315
- ),
4316
- }
4317
- : { project },
4318
- ),
4319
- ],
5676
+ plugins: [face.plugin],
4320
5677
  build: {
4321
5678
  write: true,
4322
5679
  outDir: face.output,
@@ -4331,12 +5688,12 @@ describe('configuration helpers', () => {
4331
5688
  }
4332
5689
 
4333
5690
  const idle = join(workspace, 'idle')
4334
- const serving = configHelpers.declarationRollup({
5691
+ const plugins = configHelpers.declarationRollup({
4335
5692
  project,
4336
5693
  rewrite: configHelpers.rewriteCoreSpecifier,
4337
5694
  })
4338
- const configure = serving.configResolved
4339
- const close = serving.closeBundle
5695
+ const configure = plugins.configResolved
5696
+ const close = plugins.closeBundle
4340
5697
  if (typeof configure !== 'function' || typeof close !== 'function') {
4341
5698
  throw new Error('The declaration roll-up exposes no build hooks')
4342
5699
  }
@@ -4383,28 +5740,7 @@ describe('configuration helpers', () => {
4383
5740
  // list reads every entry first. Each case runs a policy reader over a recorded list and pins the
4384
5741
  // outcome and the entries it read, as the loop the reader replaced gave them.
4385
5742
  describe('policy readers consume a caller-supplied list one entry at a time', () => {
4386
- const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
4387
- const declarator = (): PolicyExpression => ({
4388
- type: 'VariableDeclarator',
4389
- range: [0, 0],
4390
- id: name,
4391
- init: { type: 'ArrowFunctionExpression', range: [0, 0] },
4392
- })
4393
- const exported = (): PolicyExpression => ({
4394
- type: 'ExportNamedDeclaration',
4395
- range: [47, 63],
4396
- declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
4397
- })
4398
- const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
4399
- // A revoked proxy refuses every read, which fails the reader once it holds the entry.
4400
- const refused =
4401
- <T extends object>(entry: () => T) =>
4402
- (): T => {
4403
- const { proxy, revoke } = Proxy.revocable(entry(), {})
4404
- revoke()
4405
- return proxy
4406
- }
4407
- const throwing = [
5743
+ const failureCases = [
4408
5744
  [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
4409
5745
  [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
4410
5746
  [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
@@ -4420,11 +5756,11 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4420
5756
  statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
4421
5757
  (binding) => binding.name,
4422
5758
  ),
4423
- { valid: declarator, refusal: refused(declarator) },
5759
+ { valid: createPolicyDeclarator, refusal: createPolicyRefused(createPolicyDeclarator) },
4424
5760
  [
4425
5761
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
4426
5762
  [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4427
- ...throwing,
5763
+ ...failureCases,
4428
5764
  ],
4429
5765
  ),
4430
5766
  ...createPolicyLazyCases(
@@ -4435,7 +5771,7 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4435
5771
  {
4436
5772
  filename: '/ws/src/server/execution/run.ts',
4437
5773
  cwd: '/ws',
4438
- sourceCode: { text: '', getAllComments: () => [] },
5774
+ sourceCode: { text: '', getAllComments: () => [], getDeclaredVariables: () => [] },
4439
5775
  report: (diagnostic) => {
4440
5776
  reports.push(diagnostic.messageId)
4441
5777
  },
@@ -4444,11 +5780,11 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4444
5780
  )
4445
5781
  return reports
4446
5782
  },
4447
- { valid: exported, refusal: refused(exported) },
5783
+ { valid: createPolicyExport, refusal: createPolicyRefused(createPolicyExport) },
4448
5784
  [
4449
5785
  [POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
4450
5786
  [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4451
- ...throwing,
5787
+ ...failureCases,
4452
5788
  ],
4453
5789
  ),
4454
5790
  ...createPolicyLazyCases(
@@ -4456,27 +5792,30 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4456
5792
  (body: PolicyExpression[]) =>
4457
5793
  programToPolicyDocs(
4458
5794
  { type: 'Program', range: [0, 0], body },
4459
- { text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
5795
+ {
5796
+ text: '/** One. */\nexport function run() {}',
5797
+ getAllComments: () => [createPolicyComment()],
5798
+ },
4460
5799
  ).map((doc) => doc.name),
4461
- { valid: exported, refusal: refused(exported) },
5800
+ { valid: createPolicyExport, refusal: createPolicyRefused(createPolicyExport) },
4462
5801
  [
4463
5802
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
4464
5803
  [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
4465
- ...throwing,
5804
+ ...failureCases,
4466
5805
  ],
4467
5806
  ),
4468
5807
  ...createPolicyLazyCases(
4469
5808
  'programToPolicyDocs comments',
4470
5809
  (comments: PolicyComment[]) =>
4471
5810
  programToPolicyDocs(
4472
- { type: 'Program', range: [0, 0], body: [exported()] },
5811
+ { type: 'Program', range: [0, 0], body: [createPolicyExport()] },
4473
5812
  { text: '/** One. */\n', getAllComments: () => comments },
4474
5813
  ).map((doc) => doc.name),
4475
- { valid: comment, refusal: refused(comment) },
5814
+ { valid: createPolicyComment, refusal: createPolicyRefused(createPolicyComment) },
4476
5815
  [
4477
5816
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
4478
5817
  [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4479
- ...throwing,
5818
+ ...failureCases,
4480
5819
  ],
4481
5820
  ),
4482
5821
  ...createPolicyLazyCases(
@@ -4486,7 +5825,7 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4486
5825
  { type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
4487
5826
  { text: 'import x', getAllComments: () => comments },
4488
5827
  ).map((doc) => doc.name),
4489
- { valid: comment, refusal: refused(comment) },
5828
+ { valid: createPolicyComment, refusal: createPolicyRefused(createPolicyComment) },
4490
5829
  [
4491
5830
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
4492
5831
  [POLICY_LAZY_SHAPES.dense, 'return []', []],