@orkestrel/scaffold 0.0.108 → 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 (157) 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 +290 -69
  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 +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. 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,16 +36,21 @@ 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,
@@ -85,16 +84,25 @@ import {
85
84
  SHEET_POLICY_ORDER_PATTERN,
86
85
  readImportDiagnostics,
87
86
  readNativeDiagnostics,
87
+ createPolicyComment,
88
+ createPolicyContext,
89
+ createPolicyDeclarator,
90
+ createPolicyExport,
88
91
  createPolicyLazyCases,
92
+ createPolicyRefused,
89
93
  createPolicyScratch,
94
+ createPolicySource,
90
95
  createPolicySparse,
91
96
  inspectPolicyConfiguration,
92
97
  inspectPolicyWiring,
93
98
  normalizePolicyFilename,
94
99
  POLICY_EXTRACTOR,
100
+ POLICY_IMPORT_CASES,
95
101
  POLICY_LAZY_SHAPES,
102
+ POLICY_NAME_CASES,
96
103
  POLICY_PUBLISHES,
97
104
  POLICY_ROOT,
105
+ POLICY_WIRING_SETTINGS,
98
106
  POLICY_VUE_COMPOSABLE_SOURCE,
99
107
  POLICY_VUE_RELATIVE_SOURCES,
100
108
  POLICY_VUE_TARGET_CASES,
@@ -244,7 +252,7 @@ describe('selected faces', () => {
244
252
  const sheets = collectSheets(POLICY_ROOT)
245
253
  const frameworks = collectFrameworks(POLICY_ROOT)
246
254
  const wrappers = collectFaceWrappers(POLICY_ROOT)
247
- const readings: Array<{
255
+ const configurations: Array<{
248
256
  readonly axis: string
249
257
  readonly face: string
250
258
  readonly wrapper: string
@@ -279,10 +287,10 @@ describe('selected faces', () => {
279
287
  expect(
280
288
  Object.getOwnPropertyDescriptor(tsconfig.compilerOptions.paths, `@${axis}/${face}`)?.value,
281
289
  ).toEqual([`./${axis}/${face}/index.ts`])
282
- readings.push({ axis, face, wrapper, project })
290
+ configurations.push({ axis, face, wrapper, project })
283
291
  }
284
- for (const { face, wrapper, project } of readings.filter((reading) =>
285
- sheets.includes(reading.face),
292
+ for (const { face, wrapper, project } of configurations.filter((faceConfiguration) =>
293
+ sheets.includes(faceConfiguration.face),
286
294
  )) {
287
295
  expect(readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')).toContain('sheetProject(')
288
296
  expect(inspectSheetConfiguration(project)).toEqual([])
@@ -296,16 +304,18 @@ describe('selected faces', () => {
296
304
  )
297
305
  expect(readConfigScript(scripts, 'build:src')).toContain(`npm run build:src:${face}`)
298
306
  }
299
- for (const { project } of readings.filter((reading) =>
300
- frameworks.includes(`${reading.axis}/${reading.face}`),
307
+ for (const { project } of configurations.filter((faceConfiguration) =>
308
+ frameworks.includes(`${faceConfiguration.axis}/${faceConfiguration.face}`),
301
309
  )) {
302
310
  expect(project.optimizeDeps?.include).toContain('vue')
303
311
  expect(project.test?.setupFiles).toEqual(
304
312
  expect.arrayContaining(['./tests/setup.ts', './tests/setupBrowser.ts']),
305
313
  )
306
314
  }
307
- for (const { wrapper } of readings.filter(
308
- (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}`),
309
319
  )) {
310
320
  const text = readFileSync(resolve(POLICY_ROOT, wrapper), 'utf8')
311
321
  expect(text).toContain('rewriteCoreSpecifier')
@@ -804,7 +814,7 @@ describe('root configuration', () => {
804
814
  [
805
815
  'probe',
806
816
  {
807
- benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
817
+ benchmark: ['tmp/probes/**/*.bench.ts', 'tests/**/*.bench.ts'],
808
818
  include: 'tmp/probes/**/*.test.ts',
809
819
  parallel: false,
810
820
  pool: 'threads',
@@ -994,8 +1004,8 @@ describe('root configuration', () => {
994
1004
  sentinel: true,
995
1005
  }
996
1006
  const test = { name: { label: 'control' }, include: ['tests/control.test.ts'] }
997
- const ignoring = Object.defineProperty(() => ({ test }), 'name', { value: 'ignoring' })
998
- 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', {
999
1009
  value: 'spreading',
1000
1010
  })
1001
1011
  const wrappers = projects.filter((entry) => typeof entry === 'string')
@@ -1027,8 +1037,8 @@ describe('root configuration', () => {
1027
1037
  ).toThrow('expected')
1028
1038
  }
1029
1039
  const factories = projects.filter((entry) => typeof entry !== 'string')
1030
- const entries: readonly unknown[] = [...factories, ignoring, spreading, { test }]
1031
- const readings = entries.map((entry) => {
1040
+ const entries: readonly unknown[] = [...factories, ignoreInput, spreadInput, { test }]
1041
+ const outcomes = entries.map((entry) => {
1032
1042
  if (typeof entry !== 'function') {
1033
1043
  return { name: undefined, callable: false, mode: undefined, leaked: [] }
1034
1044
  }
@@ -1046,13 +1056,13 @@ describe('root configuration', () => {
1046
1056
  }
1047
1057
  })
1048
1058
  const forwarded = { callable: true, mode: 'sentinel-mode', leaked: [] }
1049
- expect(readings.slice(0, factories.length)).toStrictEqual(
1059
+ expect(outcomes.slice(0, factories.length)).toStrictEqual(
1050
1060
  factories.map((entry) => ({
1051
1061
  name: typeof entry === 'function' ? entry.name : undefined,
1052
1062
  ...forwarded,
1053
1063
  })),
1054
1064
  )
1055
- expect(readings.slice(factories.length)).toStrictEqual([
1065
+ expect(outcomes.slice(factories.length)).toStrictEqual([
1056
1066
  { name: 'ignoring', callable: true, mode: undefined, leaked: [] },
1057
1067
  {
1058
1068
  name: 'spreading',
@@ -1062,8 +1072,8 @@ describe('root configuration', () => {
1062
1072
  },
1063
1073
  { name: undefined, callable: false, mode: undefined, leaked: [] },
1064
1074
  ])
1065
- for (const reading of readings.slice(factories.length)) {
1066
- 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 })
1067
1077
  }
1068
1078
 
1069
1079
  // A record that carries no string `mode` is refused rather than forwarded as a project
@@ -1331,7 +1341,7 @@ describe('root configuration', () => {
1331
1341
  "process.stdout.write('lazy\\n')\nexport function lazy(): string {\n\treturn String(process.argv.length)\n}\n",
1332
1342
  )
1333
1343
  const rosters: Array<readonly string[]> = []
1334
- for (const splitting of [false, true]) {
1344
+ for (const chunked of [false, true]) {
1335
1345
  const loaded = await loadConfigFromFile(
1336
1346
  { command: 'build', mode: 'production', isSsrBuild: false, isPreview: false },
1337
1347
  wrapper,
@@ -1358,7 +1368,7 @@ describe('root configuration', () => {
1358
1368
  formats: ['cjs'],
1359
1369
  fileName: () => 'main.cjs',
1360
1370
  },
1361
- rolldownOptions: splitting
1371
+ rolldownOptions: chunked
1362
1372
  ? { ...rolldown, output: { codeSplitting: true } }
1363
1373
  : { ...rolldown },
1364
1374
  },
@@ -1647,6 +1657,49 @@ describe('root configuration', () => {
1647
1657
  expect(inspectPolicyConfiguration(controlled)).toEqual([
1648
1658
  'overrides must not configure policy/no-mocking',
1649
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
+ }
1650
1703
  })
1651
1704
 
1652
1705
  it('omits the audit-confirmed dead policy type exports', () => {
@@ -1761,6 +1814,80 @@ describe('policy plugin', () => {
1761
1814
  ],
1762
1815
  })
1763
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
+
1764
1891
  tester.run('no-nested-functions', NESTED_RULE, {
1765
1892
  valid: [
1766
1893
  {
@@ -2520,6 +2647,253 @@ createSomething({
2520
2647
  ],
2521
2648
  })
2522
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
+
2523
2897
  tester.run('no-malformed-summary', VOICE_RULE, {
2524
2898
  valid: [
2525
2899
  {
@@ -2887,7 +3261,7 @@ createSomething({
2887
3261
  range: [47, 63],
2888
3262
  declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
2889
3263
  }
2890
- const source: PolicySourceCode = { text: '', getAllComments: () => [] }
3264
+ const source = createPolicySource('')
2891
3265
 
2892
3266
  it('refuses a hole in the declarators of a statement as the declarator it reads as', () => {
2893
3267
  const declarator: PolicyExpression = {
@@ -2935,14 +3309,7 @@ createSomething({
2935
3309
 
2936
3310
  it('refuses a hole in the body of a function-domain module as the statement it reads as', () => {
2937
3311
  const reports: PolicyDiagnostic[] = []
2938
- const context: PolicyContext = {
2939
- filename: '/ws/src/server/execution/run.ts',
2940
- cwd: '/ws',
2941
- sourceCode: source,
2942
- report: (diagnostic) => {
2943
- reports.push(diagnostic)
2944
- },
2945
- }
3312
+ const context = createPolicyContext('/ws/src/server/execution/run.ts', '/ws', reports)
2946
3313
  const dense: PolicyExpression = { type: 'Program', range: [0, 0], body: [exported] }
2947
3314
  const sparse: PolicyExpression = {
2948
3315
  type: 'Program',
@@ -3008,12 +3375,12 @@ createSomething({
3008
3375
  { esmodule: false },
3009
3376
  ])
3010
3377
  if (!Array.isArray(configured.overrides)) throw new Error('Missing lint overrides')
3011
- const touching = configured.overrides
3378
+ const overrides = configured.overrides
3012
3379
  .map(readConfigRecord)
3013
3380
  .filter(
3014
3381
  (override) => readConfigRecord(override.rules)['import/no-dynamic-require'] !== undefined,
3015
3382
  )
3016
- expect(touching).toEqual([])
3383
+ expect(overrides).toEqual([])
3017
3384
  })
3018
3385
 
3019
3386
  it('permits a computed import() and refuses a computed require at every module path', () => {
@@ -3484,12 +3851,13 @@ createSomething({
3484
3851
  if (!Array.isArray(rule)) return []
3485
3852
  const patterns = readConfigRecord(rule[1]).patterns
3486
3853
  if (!Array.isArray(patterns)) throw new Error('Missing restriction patterns')
3487
- return patterns.map((patternValue: unknown, patternIndex: number) => {
3854
+ const fromPatterns = patterns.map((patternValue: unknown, patternIndex: number) => {
3488
3855
  const pattern = readConfigRecord(patternValue)
3489
3856
  const message = pattern.message
3490
3857
  if (typeof message !== 'string') throw new Error('Missing pattern message')
3491
3858
  const alias = /@src\/(core|browser|server) alias/u.exec(message)?.[1]
3492
- const listed: ReadonlyArray<readonly [string, boolean]> =
3859
+ const fence = POLICY_IMPORT_CASES.find(({ marker }) => message.includes(marker))
3860
+ const chained: ReadonlyArray<readonly [string, boolean]> =
3493
3861
  alias !== undefined
3494
3862
  ? [
3495
3863
  [`../${alias}/index.js`, true],
@@ -3504,73 +3872,90 @@ createSomething({
3504
3872
  [`../${alias}x/index.js`, false],
3505
3873
  [`../${alias}.ext/index.js`, false],
3506
3874
  ]
3507
- : message.includes('URL schemes')
3875
+ : message.includes('self-contained')
3508
3876
  ? [
3509
- ['https://host/x', true],
3510
- ['data:text/plain,x', true],
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],
3511
3887
  ['node:fs', false],
3512
- ['n:entry', true],
3513
- ['no:entry', true],
3514
- ['nod:entry', true],
3515
- ['nodea:entry', true],
3516
- ['1bad:entry', false],
3517
- ['./data:entry', false],
3888
+ ['node:child_process', false],
3518
3889
  ]
3519
- : message.includes('noncanonical dot')
3890
+ : message.includes('URL schemes')
3520
3891
  ? [
3521
- ['../core/../server/index.js', true],
3522
- ['../core/./index.js', true],
3523
- ['../core/index.js', false],
3524
- ['./index.js', false],
3525
- ['../../core/index.js', false],
3526
- ['.../../index.js', true],
3527
- ['.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],
3528
3901
  ]
3529
- : message.includes('sibling sheet')
3902
+ : message.includes('noncanonical dot')
3530
3903
  ? [
3531
- ['@src/print', true],
3532
- // Relative refusals now belong to the canonical filename-based matrix.
3533
- ['@src/core', false],
3534
- ['@src/browser', false],
3535
- ['@src/vue', false],
3536
- ['@app/core', false],
3537
- ['@app/browser', false],
3538
- ['@src/corex', true],
3539
- ['@src/', true],
3540
- // The app browser query is admitted there; src retains its alias refusal.
3541
- ['../browser?x', false],
3542
- ['../print.ext/index.js', false],
3543
- ...(Array.isArray(block.files) &&
3544
- block.files.some(
3545
- (file: unknown) =>
3546
- typeof file === 'string' && file.startsWith('app/vue/'),
3547
- )
3548
- ? [['@app/print', true] as const]
3549
- : []),
3550
- ]
3551
- : [
3552
- [
3553
- message.includes('absolute paths')
3554
- ? '/machine/module.js'
3555
- : message.includes('normalized traversal')
3556
- ? './../core/index.js'
3557
- : message.includes('forward slashes')
3558
- ? '..\\core\\index.js'
3559
- : message.includes('compiler API')
3560
- ? 'typescript'
3561
- : message.includes('private app')
3562
- ? '@app/core'
3563
- : message.includes('stylesheets')
3564
- ? './index.css'
3565
- : message.includes('host-independent') ||
3566
- message.includes('Node or server')
3567
- ? 'node:fs'
3568
- : message.includes('Vue extension')
3569
- ? '@src/vue'
3570
- : '@src/browser',
3571
- true,
3572
- ],
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],
3573
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
3574
3959
  const face =
3575
3960
  message.includes('another styles face') && typeof pattern.regex === 'string'
3576
3961
  ? /@src\/(bootstrap|tailwindcss|styles)\(/u.exec(pattern.regex)?.[1]
@@ -3591,24 +3976,31 @@ createSomething({
3591
3976
  [`@src/${face}x`, false],
3592
3977
  [`../${face}.ext/index.js`, false],
3593
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.
3594
3981
  const owner =
3595
3982
  Array.isArray(block.files) && typeof block.files[0] === 'string'
3596
- ? block.files[0].split('/**')[0]
3983
+ ? block.files[0]
3984
+ .split('/**')[0]
3985
+ ?.replace(/\/[^/]*\.[a-z]+$/u, '')
3986
+ .replaceAll('*', 'sample')
3597
3987
  : undefined
3598
3988
  if (owner === undefined) throw new Error('Missing pattern owner')
3599
- const cases = [...sources, ['./index.js', false] as const].map(
3600
- ([source, refusal], index) => {
3601
- const path = `${owner}/pattern${blockIndex}_${patternIndex}_${index}.ts`
3602
- mkdirSync(dirname(resolve(scratch, path)), { recursive: true })
3603
- writeFileSync(
3604
- resolve(scratch, path),
3605
- 'import * as boundary from ' +
3606
- JSON.stringify(source) +
3607
- '\nvoid boundary\ndebugger\n',
3608
- )
3609
- return { path, refusal }
3610
- },
3611
- )
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
+ })
3612
4004
  return {
3613
4005
  cases,
3614
4006
  override: {
@@ -3617,7 +4009,60 @@ createSomething({
3617
4009
  },
3618
4010
  }
3619
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]
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))
3620
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([])
3621
4066
  const overrides = built.map(({ override }) => override)
3622
4067
  const expected = new Map(
3623
4068
  built.flatMap(({ cases }) =>
@@ -3629,12 +4074,12 @@ createSomething({
3629
4074
  JSON.stringify({ rules: { 'no-debugger': 'error' }, overrides }),
3630
4075
  )
3631
4076
  const codes = readImportDiagnostics(scratch, [...expected.keys()])
3632
- const readings = [...expected].map(([path, refusal]) => ({
4077
+ const verdicts = [...expected].map(([path, refusal]) => ({
3633
4078
  path,
3634
4079
  expected: refusal,
3635
4080
  actual: codes.has('eslint(no-restricted-imports) ' + path),
3636
4081
  }))
3637
- expect(readings.filter((reading) => reading.expected !== reading.actual)).toEqual([])
4082
+ expect(verdicts.filter((verdict) => verdict.expected !== verdict.actual)).toEqual([])
3638
4083
  writeFileSync(
3639
4084
  resolve(scratch, '.oxlintrc.json'),
3640
4085
  JSON.stringify({ rules: { 'no-debugger': 'error', 'no-restricted-imports': 'off' } }),
@@ -3846,14 +4291,14 @@ createSomething({
3846
4291
  const [enabled, disabled] = reports
3847
4292
  if (enabled === undefined || disabled === undefined)
3848
4293
  throw new Error('Missing boundary readings')
3849
- const readings = [...expected].map(([path, refusal]) => ({
4294
+ const verdicts = [...expected].map(([path, refusal]) => ({
3850
4295
  path,
3851
4296
  expected: refusal,
3852
4297
  actual:
3853
4298
  enabled.has('eslint(no-restricted-imports) ' + path) ||
3854
4299
  enabled.has('policy(no-cross-vue-face) ' + path),
3855
4300
  }))
3856
- expect(readings.filter((reading) => reading.actual !== reading.expected)).toEqual([])
4301
+ expect(verdicts.filter((verdict) => verdict.actual !== verdict.expected)).toEqual([])
3857
4302
  const control = [...expected].map(([path, refusal]) => ({
3858
4303
  path,
3859
4304
  expected: refusal,
@@ -3862,7 +4307,7 @@ createSomething({
3862
4307
  disabled.has('policy(no-cross-vue-face) ' + path),
3863
4308
  }))
3864
4309
  expect(() =>
3865
- expect(control.filter((reading) => reading.actual !== reading.expected)).toEqual([]),
4310
+ expect(control.filter((verdict) => verdict.actual !== verdict.expected)).toEqual([]),
3866
4311
  ).toThrow('deeply equal')
3867
4312
  expect(control).toContainEqual({ path: 'src/vue/refused3.ts', expected: true, actual: false })
3868
4313
  expect(control).toContainEqual({ path: 'src/vue/refused8.ts', expected: true, actual: false })
@@ -3881,13 +4326,52 @@ createSomething({
3881
4326
  expect(declared).toContain('no-misplaced-function')
3882
4327
  expect(declared).toContain('no-host-line-endings')
3883
4328
  expect(declared).toContain('no-cross-vue-face')
4329
+ expect(declared).toContain('no-malformed-script')
4330
+ expect(declared).toContain('no-mutable-property')
3884
4331
  expect(
3885
4332
  inspectPolicyWiring(
3886
4333
  parsed,
3887
4334
  declared.map((name) => `policy/${name}`),
3888
- [POLICY_PLACEMENT_GLOBS, POLICY_ENDING_GLOBS],
4335
+ [POLICY_PLACEMENT_GLOBS, POLICY_ENDING_GLOBS, POLICY_SCRIPT_GLOBS, POLICY_NESTED_GLOBS],
3889
4336
  ),
3890
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'])
3891
4375
  })
3892
4376
 
3893
4377
  it('loads every configured policy rule through the real binary', () => {
@@ -3916,6 +4400,15 @@ createSomething({
3916
4400
  "export const STATUS = 'ready'",
3917
4401
  "export const lines = text.trim().split('\\n')",
3918
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',
3919
4412
  'void PrivateMember',
3920
4413
  'void ParameterMember',
3921
4414
  'void PublicMember',
@@ -3933,6 +4426,180 @@ createSomething({
3933
4426
  scratch.write('src/violations/composables.ts', "export const READY = 'yes'\n")
3934
4427
  scratch.write('app/browser/composables/useTheme.ts', 'export function useMode(): void {}\n')
3935
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
+ )
3936
4603
  // The line-ending population reaches src, app, and configs alone, so this module
3937
4604
  // outside them carries the same defect and must draw no diagnostic. The `debugger`
3938
4605
  // statement is the arrival control the root `no-debugger` rule reports, which proves
@@ -3985,7 +4652,18 @@ createSomething({
3985
4652
  const config = resolve(scratch.path, '.oxlintrc.json')
3986
4653
  const violations = spawnSync(
3987
4654
  process.execPath,
3988
- [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
+ ],
3989
4667
  { cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
3990
4668
  )
3991
4669
  const clean = spawnSync(
@@ -4023,7 +4701,33 @@ createSomething({
4023
4701
  for (const { code, filename } of [
4024
4702
  { code: 'policy(no-mocking)', filename: 'src/violations/fixture.ts' },
4025
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
+ },
4026
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' },
4027
4731
  { code: 'policy(no-misplaced-type)', filename: 'src/violations/fixture.ts' },
4028
4732
  { code: 'policy(no-misplaced-data)', filename: 'src/violations/fixture.ts' },
4029
4733
  { code: 'policy(no-misplaced-function)', filename: 'src/violations/fixture.ts' },
@@ -4053,12 +4757,55 @@ createSomething({
4053
4757
  `${code} ${normalizePolicyFilename(comparisonRoot, filename)}`,
4054
4758
  )
4055
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
+ )
4056
4771
  expect(violationCodes).toContain(
4057
4772
  `eslint(no-debugger) ${normalizePolicyFilename(comparisonRoot, 'scripts/read.ts')}`,
4058
4773
  )
4059
4774
  expect(violationCodes).not.toContain(
4060
4775
  `policy(no-host-line-endings) ${normalizePolicyFilename(comparisonRoot, 'scripts/read.ts')}`,
4061
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
+ }
4062
4809
  expect(clean.status).toBe(0)
4063
4810
  expect(cleanCodes).toHaveLength(0)
4064
4811
  } finally {
@@ -4228,6 +4975,7 @@ describe('configuration helpers', () => {
4228
4975
  'readBoundedFile',
4229
4976
  'readHandleBytes',
4230
4977
  'readCompilerOutput',
4978
+ 'resolveCompiler',
4231
4979
  'rewriteCoreSpecifier',
4232
4980
  'sourceFallback',
4233
4981
  'workspacePath',
@@ -4335,7 +5083,7 @@ describe('configuration helpers', () => {
4335
5083
  const scratch = createPolicyScratch({ prefix: 'orkestrel-config-handle-' })
4336
5084
  try {
4337
5085
  // Each case is a file's text, the offset already read into the buffer, and the largest total allowed.
4338
- const readings = [
5086
+ const results = [
4339
5087
  { text: 'abc', offset: 0, limit: 4 },
4340
5088
  { text: 'abcd', offset: 0, limit: 4 },
4341
5089
  { text: 'abcde', offset: 0, limit: 4 },
@@ -4355,8 +5103,8 @@ describe('configuration helpers', () => {
4355
5103
  })
4356
5104
  // A file at or under the limit answers its length; the shifted read adds the offset to the
4357
5105
  // length and fills the buffer past it; a file over the limit answers nothing.
4358
- expect(readings.map(({ count }) => count)).toStrictEqual([3, 4, undefined, 0, 5, undefined])
4359
- 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', ''])
4360
5108
  } finally {
4361
5109
  scratch.destroy()
4362
5110
  }
@@ -4409,18 +5157,18 @@ describe('configuration helpers', () => {
4409
5157
  logLevel: 'silent',
4410
5158
  build: { outDir: 'out', lib: { entry, formats: ['es'] } },
4411
5159
  }
4412
- const building = new configHelpers.BuildState()
4413
- building.resolve(await resolveConfig(inline, 'build'))
4414
- expect(building.building).toBe(true)
4415
- expect(building.output).toBe(resolve(scratch.path, 'out'))
4416
- expect(building.root).toBe(realpathSync.native(scratch.path))
4417
- 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)
4418
5166
 
4419
5167
  // The control: a serve command is not a build, and a config with no library entry has no source.
4420
- const serving = new configHelpers.BuildState()
4421
- serving.resolve(await resolveConfig({ ...inline, build: { outDir: 'out' } }, 'serve'))
4422
- expect(serving.building).toBe(false)
4423
- 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('')
4424
5172
  } finally {
4425
5173
  scratch.destroy()
4426
5174
  }
@@ -4615,6 +5363,65 @@ describe('configuration helpers', () => {
4615
5363
  }
4616
5364
  })
4617
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
+
4618
5425
  // The scope reading applies only where the workspace publishes source from `src`, which is what
4619
5426
  // `publishes` reads, and the name ends at that mechanism. An absent face project is the defect
4620
5427
  // this case exists to report rather than a second reason to excuse it, so a workspace holding
@@ -4626,7 +5433,7 @@ describe('configuration helpers', () => {
4626
5433
  it.skipIf(!POLICY_PUBLISHES)(
4627
5434
  'reads the compiler scope a declaration roll-up requires [inapplicable where src holds no recognized environment directory]',
4628
5435
  () => {
4629
- const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
5436
+ const compiler = configHelpers.resolveCompiler(import.meta.url)
4630
5437
  // The order mirrors ENVIRONMENTS in src/core/constants.ts; a server-only workspace vendors
4631
5438
  // no core project, so this walks to the first face the workspace actually carries.
4632
5439
  const faces = ['core', 'browser', 'server']
@@ -4686,7 +5493,7 @@ describe('configuration helpers', () => {
4686
5493
  // reading a face project must not excuse it from the helpers behind the roll-up, which it
4687
5494
  // vendors whether or not it publishes.
4688
5495
  it('reads the refusals, guards, overrides, and rewrites a declaration roll-up requires from every workspace', () => {
4689
- const compiler = createRequire(import.meta.url).resolve('typescript/bin/tsc')
5496
+ const compiler = configHelpers.resolveCompiler(import.meta.url)
4690
5497
  // Any project path resolves these, because each reading is refused by the text before the
4691
5498
  // path is read. The root project is the one file every workspace carries.
4692
5499
  const project = resolve(POLICY_ROOT, 'tsconfig.json')
@@ -4850,8 +5657,15 @@ describe('configuration helpers', () => {
4850
5657
  const rewritten = join(workspace, 'rewritten')
4851
5658
  const kept = join(workspace, 'kept')
4852
5659
  const builds = [
4853
- { output: rewritten, rewrite: true },
4854
- { 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 }) },
4855
5669
  ]
4856
5670
  for (const face of builds) {
4857
5671
  await build({
@@ -4859,19 +5673,7 @@ describe('configuration helpers', () => {
4859
5673
  configFile: false,
4860
5674
  logLevel: 'silent',
4861
5675
  publicDir: false,
4862
- plugins: [
4863
- configHelpers.declarationRollup(
4864
- face.rewrite
4865
- ? {
4866
- project,
4867
- rewrite: (text) =>
4868
- configHelpers.rewriteBrowserSpecifier(
4869
- configHelpers.rewriteCoreSpecifier(text),
4870
- ),
4871
- }
4872
- : { project },
4873
- ),
4874
- ],
5676
+ plugins: [face.plugin],
4875
5677
  build: {
4876
5678
  write: true,
4877
5679
  outDir: face.output,
@@ -4886,12 +5688,12 @@ describe('configuration helpers', () => {
4886
5688
  }
4887
5689
 
4888
5690
  const idle = join(workspace, 'idle')
4889
- const serving = configHelpers.declarationRollup({
5691
+ const plugins = configHelpers.declarationRollup({
4890
5692
  project,
4891
5693
  rewrite: configHelpers.rewriteCoreSpecifier,
4892
5694
  })
4893
- const configure = serving.configResolved
4894
- const close = serving.closeBundle
5695
+ const configure = plugins.configResolved
5696
+ const close = plugins.closeBundle
4895
5697
  if (typeof configure !== 'function' || typeof close !== 'function') {
4896
5698
  throw new Error('The declaration roll-up exposes no build hooks')
4897
5699
  }
@@ -4938,28 +5740,7 @@ describe('configuration helpers', () => {
4938
5740
  // list reads every entry first. Each case runs a policy reader over a recorded list and pins the
4939
5741
  // outcome and the entries it read, as the loop the reader replaced gave them.
4940
5742
  describe('policy readers consume a caller-supplied list one entry at a time', () => {
4941
- const name: PolicyExpression = { type: 'Identifier', range: [0, 0], name: 'run' }
4942
- const declarator = (): PolicyExpression => ({
4943
- type: 'VariableDeclarator',
4944
- range: [0, 0],
4945
- id: name,
4946
- init: { type: 'ArrowFunctionExpression', range: [0, 0] },
4947
- })
4948
- const exported = (): PolicyExpression => ({
4949
- type: 'ExportNamedDeclaration',
4950
- range: [47, 63],
4951
- declaration: { type: 'FunctionDeclaration', range: [0, 0], id: name },
4952
- })
4953
- const comment = (): PolicyComment => ({ type: 'Block', value: '* One.', range: [0, 11] })
4954
- // A revoked proxy refuses every read, which fails the reader once it holds the entry.
4955
- const refused =
4956
- <T extends object>(entry: () => T) =>
4957
- (): T => {
4958
- const { proxy, revoke } = Proxy.revocable(entry(), {})
4959
- revoke()
4960
- return proxy
4961
- }
4962
- const throwing = [
5743
+ const failureCases = [
4963
5744
  [POLICY_LAZY_SHAPES.hole, 'throw TypeError', []],
4964
5745
  [POLICY_LAZY_SHAPES.holeFailure, 'throw TypeError', []],
4965
5746
  [POLICY_LAZY_SHAPES.failureFirst, 'throw RangeError', [0]],
@@ -4975,11 +5756,11 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4975
5756
  statementToPolicyBindings({ type: 'VariableDeclaration', range: [0, 0], declarations }).map(
4976
5757
  (binding) => binding.name,
4977
5758
  ),
4978
- { valid: declarator, refusal: refused(declarator) },
5759
+ { valid: createPolicyDeclarator, refusal: createPolicyRefused(createPolicyDeclarator) },
4979
5760
  [
4980
5761
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
4981
5762
  [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
4982
- ...throwing,
5763
+ ...failureCases,
4983
5764
  ],
4984
5765
  ),
4985
5766
  ...createPolicyLazyCases(
@@ -4990,7 +5771,7 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4990
5771
  {
4991
5772
  filename: '/ws/src/server/execution/run.ts',
4992
5773
  cwd: '/ws',
4993
- sourceCode: { text: '', getAllComments: () => [] },
5774
+ sourceCode: { text: '', getAllComments: () => [], getDeclaredVariables: () => [] },
4994
5775
  report: (diagnostic) => {
4995
5776
  reports.push(diagnostic.messageId)
4996
5777
  },
@@ -4999,11 +5780,11 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
4999
5780
  )
5000
5781
  return reports
5001
5782
  },
5002
- { valid: exported, refusal: refused(exported) },
5783
+ { valid: createPolicyExport, refusal: createPolicyRefused(createPolicyExport) },
5003
5784
  [
5004
5785
  [POLICY_LAZY_SHAPES.empty, 'return ["module"]', []],
5005
5786
  [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
5006
- ...throwing,
5787
+ ...failureCases,
5007
5788
  ],
5008
5789
  ),
5009
5790
  ...createPolicyLazyCases(
@@ -5011,27 +5792,30 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
5011
5792
  (body: PolicyExpression[]) =>
5012
5793
  programToPolicyDocs(
5013
5794
  { type: 'Program', range: [0, 0], body },
5014
- { text: '/** One. */\nexport function run() {}', getAllComments: () => [comment()] },
5795
+ {
5796
+ text: '/** One. */\nexport function run() {}',
5797
+ getAllComments: () => [createPolicyComment()],
5798
+ },
5015
5799
  ).map((doc) => doc.name),
5016
- { valid: exported, refusal: refused(exported) },
5800
+ { valid: createPolicyExport, refusal: createPolicyRefused(createPolicyExport) },
5017
5801
  [
5018
5802
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
5019
5803
  [POLICY_LAZY_SHAPES.dense, 'return []', [0]],
5020
- ...throwing,
5804
+ ...failureCases,
5021
5805
  ],
5022
5806
  ),
5023
5807
  ...createPolicyLazyCases(
5024
5808
  'programToPolicyDocs comments',
5025
5809
  (comments: PolicyComment[]) =>
5026
5810
  programToPolicyDocs(
5027
- { type: 'Program', range: [0, 0], body: [exported()] },
5811
+ { type: 'Program', range: [0, 0], body: [createPolicyExport()] },
5028
5812
  { text: '/** One. */\n', getAllComments: () => comments },
5029
5813
  ).map((doc) => doc.name),
5030
- { valid: comment, refusal: refused(comment) },
5814
+ { valid: createPolicyComment, refusal: createPolicyRefused(createPolicyComment) },
5031
5815
  [
5032
5816
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
5033
5817
  [POLICY_LAZY_SHAPES.dense, 'return ["run"]', [0]],
5034
- ...throwing,
5818
+ ...failureCases,
5035
5819
  ],
5036
5820
  ),
5037
5821
  ...createPolicyLazyCases(
@@ -5041,7 +5825,7 @@ describe('policy readers consume a caller-supplied list one entry at a time', ()
5041
5825
  { type: 'Program', range: [0, 0], body: [{ type: 'ImportDeclaration', range: [0, 0] }] },
5042
5826
  { text: 'import x', getAllComments: () => comments },
5043
5827
  ).map((doc) => doc.name),
5044
- { valid: comment, refusal: refused(comment) },
5828
+ { valid: createPolicyComment, refusal: createPolicyRefused(createPolicyComment) },
5045
5829
  [
5046
5830
  [POLICY_LAZY_SHAPES.empty, 'return []', []],
5047
5831
  [POLICY_LAZY_SHAPES.dense, 'return []', []],