@orkestrel/scaffold 0.0.62 → 0.0.64

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 (52) hide show
  1. package/README.md +18 -103
  2. package/dist/bin/main.js +95 -27
  3. package/dist/bin/main.js.map +1 -1
  4. package/dist/host/AGENTS.md +2 -2
  5. package/dist/host/CLAUDE.md +6 -0
  6. package/dist/host/agents/orchestration.md +23 -15
  7. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
  8. package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +6 -6
  9. package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +4 -4
  10. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +5 -5
  11. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +15 -15
  13. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +43 -17
  14. package/dist/host/agents/skills/orkestrel-publish/references/window.md +41 -16
  15. package/dist/host/claude/agents/orkestrel.md +56 -56
  16. package/dist/host/claude/agents/reviewer.md +13 -0
  17. package/dist/host/claude/rules/architecture.md +51 -45
  18. package/dist/host/claude/rules/documentation.md +18 -1
  19. package/dist/host/claude/rules/portability.md +2 -0
  20. package/dist/host/claude/rules/quality.md +1 -1
  21. package/dist/host/claude/rules/tests.md +12 -11
  22. package/dist/host/claude/rules/typescript.md +5 -0
  23. package/dist/host/claude/rules/workspace.md +23 -18
  24. package/dist/host/claude/rules/writing.md +4 -0
  25. package/dist/host/codex/agents/orkestrel.toml +3 -3
  26. package/dist/host/codex/agents/reviewer.toml +4 -2
  27. package/dist/host/configs/helpers.ts +311 -2
  28. package/dist/host/configs/policy.ts +1100 -51
  29. package/dist/host/dotfiles/oxlintrc.json +72 -1
  30. package/dist/host/guides/guide.md +749 -222
  31. package/dist/host/guides/scaffold.md +472 -378
  32. package/dist/host/manifest.json +34 -33
  33. package/dist/host/scripts/codex.sh +0 -0
  34. package/dist/host/scripts/cursor.sh +0 -0
  35. package/dist/host/scripts/deps.sh +0 -0
  36. package/dist/host/scripts/ollama.sh +0 -0
  37. package/dist/host/tests/config.test.ts +1200 -16
  38. package/dist/host/tests/policy.test.ts +157 -173
  39. package/dist/host/tests/setupPolicy.ts +522 -1007
  40. package/dist/src/core/index.cjs +373 -279
  41. package/dist/src/core/index.cjs.map +1 -1
  42. package/dist/src/core/index.d.cts +130 -120
  43. package/dist/src/core/index.d.ts +130 -120
  44. package/dist/src/core/index.js +373 -278
  45. package/dist/src/core/index.js.map +1 -1
  46. package/dist/src/server/index.cjs +28 -21
  47. package/dist/src/server/index.cjs.map +1 -1
  48. package/dist/src/server/index.d.cts +38 -33
  49. package/dist/src/server/index.d.ts +38 -33
  50. package/dist/src/server/index.js +28 -21
  51. package/dist/src/server/index.js.map +1 -1
  52. package/package.json +17 -18
@@ -8,34 +8,27 @@ import {
8
8
  writeFileSync,
9
9
  } from 'node:fs'
10
10
  import { tmpdir } from 'node:os'
11
- import { basename, dirname, join, matchesGlob } from 'node:path'
12
- import * as ts from 'typescript'
11
+ import { basename, dirname, join, matchesGlob, relative as relativePath, resolve } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { stripPolicyCode, textToPolicyHits } from '../configs/policy.js'
13
14
 
14
- /** A rule the fleet placement instrument can decide from syntax and a file path. */
15
+ /** Names a rule the fleet sweep decides from workspace text and paths. */
15
16
  export type PolicyRule =
16
17
  | 'bridge'
17
- | 'class'
18
- | 'constant'
19
- | 'data'
20
- | 'domain'
21
- | 'export'
22
- | 'factory'
23
- | 'function'
24
18
  | 'mirror'
25
- | 'parser'
26
19
  | 'portability'
20
+ | 'prose'
27
21
  | 'rules'
28
22
  | 'skill'
29
23
  | 'suppression'
30
- | 'type'
31
24
 
32
- /** One TypeScript source supplied to the placement instrument. */
25
+ /** Describes one workspace file a physical control writes. */
33
26
  export interface PolicySource {
34
27
  readonly path: string
35
28
  readonly content: string
36
29
  }
37
30
 
38
- /** One placement failure reported by the instrument. */
31
+ /** Describes one policy failure the sweep reports. */
39
32
  export interface PolicyViolation {
40
33
  readonly rule: PolicyRule
41
34
  readonly path: string
@@ -43,7 +36,7 @@ export interface PolicyViolation {
43
36
  readonly message: string
44
37
  }
45
38
 
46
- /** One physical negative control, including the population boundary it attacks. */
39
+ /** Describes one physical negative control, including the population boundary it attacks. */
47
40
  export interface PolicyControl {
48
41
  readonly label: string
49
42
  readonly membership: string
@@ -91,7 +84,7 @@ export function createPolicyScratch(options: { readonly prefix: string }): Polic
91
84
  }
92
85
  }
93
86
 
94
- /** Parsed skill frontmatter and the exact scalar source used for bridge comparison. */
87
+ /** Holds parsed skill frontmatter and the exact scalar source used for bridge comparison. */
95
88
  export interface SkillFrontmatter {
96
89
  readonly keys: readonly string[]
97
90
  readonly name: string | undefined
@@ -102,113 +95,29 @@ export interface SkillFrontmatter {
102
95
  }
103
96
  }
104
97
 
105
- /** The directory whose immediate child directories form the complete skill family. */
98
+ /** Names the directory whose immediate child directories form the complete skill family. */
106
99
  export const SKILL_FAMILY_ROOT = '.agents/skills'
107
100
 
108
- /** The directory whose immediate child directories form the Claude skill bridge family. */
101
+ /** Names the directory whose immediate child directories form the Claude skill bridge family. */
109
102
  export const SKILL_BRIDGE_ROOT = '.claude/skills'
110
103
 
111
- /** Minimal valid skill text for physical family controls. */
104
+ /** Holds the minimal valid skill text for physical family controls. */
112
105
  export const SKILL_POLICY_TEXT =
113
106
  '---\nname: sample\ndescription: Use this skill for a policy fixture.\n---\n\n# Skill\n'
114
107
 
115
- /** Skill text naming one reference for physical family controls. */
108
+ /** Holds skill text naming one reference for physical family controls. */
116
109
  export const SKILL_REFERENCE_TEXT = `${SKILL_POLICY_TEXT}\nRead references/example.md.\n`
117
110
 
118
- /** Minimal valid provider bridge text for physical bridge controls. */
111
+ /** Holds the minimal valid provider bridge text for physical bridge controls. */
119
112
  export const SKILL_BRIDGE_TEXT = `${SKILL_POLICY_TEXT}\nRead \`.agents/skills/sample/SKILL.md\`.\n`
120
113
 
121
- /** Canonical skill metadata whose values each carry YAML's escaped apostrophe. */
114
+ /** Holds canonical skill metadata whose values each carry YAML's escaped apostrophe. */
122
115
  export const SKILL_APOSTROPHE_METADATA =
123
116
  "interface:\n display_name: 'Owner''s Fixture'\n" +
124
117
  " short_description: 'Exercise the family''s apostrophe rule'\n" +
125
118
  " default_prompt: 'Use $sample for this fixture''s value.'\n"
126
119
 
127
- /** Every centralized module named by the architecture kind table. */
128
- export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
129
- 'cloners.ts',
130
- 'combinators.ts',
131
- 'compilers.ts',
132
- 'constants.ts',
133
- 'contracts.ts',
134
- 'errors.ts',
135
- 'factories.ts',
136
- 'handlers.ts',
137
- 'helpers.ts',
138
- 'index.ts',
139
- 'inferers.ts',
140
- 'middlewares.ts',
141
- 'parsers.ts',
142
- 'relations.ts',
143
- 'routes.ts',
144
- 'schemas.ts',
145
- 'seeders.ts',
146
- 'shapers.ts',
147
- 'templates.ts',
148
- 'types.ts',
149
- 'validators.ts',
150
- ])
151
-
152
- /** The exhaustive centralized-file set that permits module functions. */
153
- export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
154
- 'cloners.ts',
155
- 'combinators.ts',
156
- 'compilers.ts',
157
- 'errors.ts',
158
- 'factories.ts',
159
- 'handlers.ts',
160
- 'helpers.ts',
161
- 'inferers.ts',
162
- 'middlewares.ts',
163
- 'parsers.ts',
164
- 'relations.ts',
165
- 'schemas.ts',
166
- 'seeders.ts',
167
- 'shapers.ts',
168
- 'validators.ts',
169
- ])
170
-
171
- /** Centralized files that permit module data by declaration syntax. */
172
- export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
173
- 'combinators.ts',
174
- 'constants.ts',
175
- 'contracts.ts',
176
- 'relations.ts',
177
- 'routes.ts',
178
- 'schemas.ts',
179
- 'shapers.ts',
180
- 'templates.ts',
181
- 'validators.ts',
182
- ])
183
-
184
- /**
185
- * Files excluded from the module-data rule because their namespace values hold helper behavior.
186
- * This exclusion also permits unrelated module data such as `export const RETRIES = 3`.
187
- */
188
- export const DATA_EXEMPT_FILES: readonly string[] = Object.freeze(['helpers.ts'])
189
-
190
- /** Fleet-registered folders whose direct modules each contain one named function. */
191
- export const FUNCTION_DOMAIN_FOLDERS: readonly string[] = Object.freeze([
192
- 'app/browser/composables',
193
- 'src/server/execution',
194
- ])
195
-
196
- /** Every ambient declaration suffix the source glob collects and the parsed population excludes. */
197
- export const POLICY_AMBIENT_SUFFIXES: readonly string[] = Object.freeze([
198
- '.d.cts',
199
- '.d.mts',
200
- '.d.ts',
201
- ])
202
-
203
- /** TypeScript source extensions whose declaration syntax the sweep reads. */
204
- export const POLICY_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
205
- 'cts',
206
- 'mts',
207
- 'ts',
208
- 'tsx',
209
- ])
210
-
211
- /** Every extension through which a mirrored test can name a module. */
120
+ /** Lists every extension through which a mirrored test can name a module. */
212
121
  export const POLICY_MODULE_EXTENSIONS: readonly string[] = Object.freeze([
213
122
  'cts',
214
123
  'mts',
@@ -219,42 +128,45 @@ export const POLICY_MODULE_EXTENSIONS: readonly string[] = Object.freeze([
219
128
  'css',
220
129
  ])
221
130
 
222
- /** Module extensions whose extensionless stem can resolve a leading-underscore partial. */
131
+ /**
132
+ * Lists the module extensions whose extensionless stem can resolve a leading-underscore partial.
133
+ */
223
134
  export const POLICY_PARTIAL_EXTENSIONS: readonly string[] = Object.freeze(['scss', 'css'])
224
135
 
225
- /** The reserved stem prefix whose mirrored module can resolve inside the tests axis. */
136
+ /** Names the reserved stem prefix whose mirrored module can resolve inside the tests axis. */
226
137
  export const POLICY_TESTS_MODULE_PREFIX = 'setup'
227
138
 
228
- /** The complete parsed TypeScript source population under either workspace axis. */
229
- export const POLICY_SOURCE_GLOB = `{app,src}/**/*.{${POLICY_SOURCE_EXTENSIONS.join(',')}}`
230
-
231
- /** The complete module population available to mirrored tests under either workspace axis. */
139
+ /**
140
+ * Matches the complete module population available to mirrored tests under either workspace axis.
141
+ */
232
142
  export const POLICY_MODULE_GLOB = `{app,src}/**/*.{${POLICY_MODULE_EXTENSIONS.join(',')}}`
233
143
 
234
- /** The tests-axis setup module population available to mirrored tests. */
144
+ /** Matches the tests-axis setup module population available to mirrored tests. */
235
145
  export const POLICY_TESTS_MODULE_GLOB = `tests/**/${POLICY_TESTS_MODULE_PREFIX}*.ts`
236
146
 
237
- /** The mirrored module-test population inspected under either workspace axis. */
147
+ /** Matches the mirrored module-test population inspected under either workspace axis. */
238
148
  export const POLICY_TEST_GLOB = 'tests/{app,src}/**/*.test.ts'
239
149
 
240
150
  // Compose suppression tokens so the instrument does not report its own definitions or controls.
241
151
  export const POLICY_SUPPRESSION_DIRECTIVE = ['oxlint', '-disable'].join('')
242
152
 
243
- /** Source, test, config, and script files inspected for lint suppression directives. */
153
+ /** Matches the source, test, config, and script files inspected for lint suppression directives. */
244
154
  export const POLICY_SUPPRESSION_GLOB: readonly string[] = Object.freeze([
245
155
  '{src,app,tests,configs,scripts}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
246
156
  '*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
247
157
  ])
248
158
 
249
- /** Rules whose workspace-wide lint wiring must not be weakened by configuration. */
159
+ /** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
250
160
  export const POLICY_WIRING_RULES: readonly string[] = Object.freeze([
251
161
  'policy/no-mocking',
252
162
  'policy/no-keyword-privacy',
163
+ 'policy/no-malformed-summary',
164
+ 'policy/no-banned-term',
253
165
  'typescript/parameter-properties',
254
166
  'typescript/explicit-member-accessibility',
255
167
  ])
256
168
 
257
- /** Linted workspace roots that ignore patterns must not reach. */
169
+ /** Lists the linted workspace roots that ignore patterns must not reach. */
258
170
  export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
259
171
  'src',
260
172
  'app',
@@ -262,13 +174,16 @@ export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
262
174
  'configs',
263
175
  ])
264
176
 
265
- /** Either lint suppression token the text sweep refuses. */
177
+ /** Matches either lint suppression token the text sweep refuses. */
266
178
  export const POLICY_SUPPRESSION_PATTERN = new RegExp(
267
179
  [['eslint', '-disable'].join(''), POLICY_SUPPRESSION_DIRECTIVE].join('|'),
268
180
  'u',
269
181
  )
270
182
 
271
- /** The workspace-authored path population inspected for a name a Windows checkout cannot hold. */
183
+ /**
184
+ * Matches the workspace-authored path population inspected for a name a Windows checkout cannot
185
+ * hold.
186
+ */
272
187
  export const POLICY_PORTABILITY_GLOB: readonly string[] = Object.freeze([
273
188
  '{src,app,configs,tests,scripts,guides}/**/*',
274
189
  '{.agents,.claude,.codex,.cursor,.github}/**/*',
@@ -276,10 +191,7 @@ export const POLICY_PORTABILITY_GLOB: readonly string[] = Object.freeze([
276
191
  '.*',
277
192
  ])
278
193
 
279
- /** The TypeScript source population parsed for host-specific line-ending handling. */
280
- export const POLICY_PORTABILITY_SOURCE_GLOB = '{src,app,configs}/**/*.ts'
281
-
282
- /** Every device name Windows reserves, whatever extension the segment carries. */
194
+ /** Lists every device name Windows reserves, whatever extension the segment carries. */
283
195
  export const POLICY_RESERVED_NAMES: readonly string[] = Object.freeze([
284
196
  'aux',
285
197
  'com1',
@@ -305,513 +217,109 @@ export const POLICY_RESERVED_NAMES: readonly string[] = Object.freeze([
305
217
  'prn',
306
218
  ])
307
219
 
308
- /** Every character Windows refuses inside a path segment. */
220
+ /** Matches every character Windows refuses inside a path segment. */
309
221
  export const POLICY_RESERVED_PATTERN = /[<>:"|?*]/u
310
222
 
311
- /** A shell script named as a complete path token inside a manifest script. */
223
+ /** Matches a shell script named as a complete path token inside a manifest script. */
312
224
  export const POLICY_SHELL_PATTERN = /\.sh\b/u
313
225
 
314
- /** The directory whose direct Markdown files form the complete rule family. */
226
+ /** Names the directory whose direct Markdown files form the complete rule family. */
315
227
  export const POLICY_RULE_ROOT = '.claude/rules'
316
228
 
317
- /** The root instruction file whose rule map registers the rule family. */
229
+ /** Names the root instruction file whose rule map registers the rule family. */
318
230
  export const POLICY_RULE_MAP_FILE = 'AGENTS.md'
319
231
 
320
- /** The heading that opens the root instruction file's rule map table. */
232
+ /** Names the heading that opens the root instruction file's rule map table. */
321
233
  export const POLICY_RULE_MAP_HEADING = '## Rule map'
322
234
 
323
- /** The workspace manifest whose scripts run on every supported host. */
235
+ /** Names the workspace manifest whose scripts run on every supported host. */
324
236
  export const POLICY_MANIFEST_FILE = 'package.json'
325
237
 
326
- /**
327
- * Normalize platform separators for stable matching and diagnostics.
328
- *
329
- * @param path - The workspace-relative path to normalize.
330
- * @returns The path with forward slashes and no duplicate separators.
331
- */
332
- export function normalizePolicyPath(path: string): string {
333
- return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
334
- }
238
+ /** Lists the directory names the prose sweep never descends into. */
239
+ export const POLICY_PROSE_EXCLUSIONS: readonly string[] = Object.freeze([
240
+ '.git',
241
+ '.orkestrel',
242
+ 'dist',
243
+ 'node_modules',
244
+ 'tmp',
245
+ ])
335
246
 
336
- /**
337
- * Whether a declaration carries a specified TypeScript modifier.
338
- *
339
- * @param node - The declaration to inspect.
340
- * @param modifier - The modifier syntax to find.
341
- * @returns `true` when the declaration carries the modifier.
342
- */
343
- export function hasPolicyModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
344
- return (
345
- ts.canHaveModifiers(node) &&
346
- ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
347
- )
348
- }
247
+ /** Matches a top-level guide path and captures the package short name it is written for. */
248
+ export const POLICY_MIRROR_PATTERN = /^guides\/([^/]+)\.md$/u
349
249
 
350
- /**
351
- * Return the one-based line where a syntax node begins.
352
- *
353
- * @param node - The syntax node to locate.
354
- * @returns Its one-based source line.
355
- */
356
- export function getPolicyLine(node: ts.Node): number {
357
- const position = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart())
358
- return position.line + 1
359
- }
250
+ /** Names the guide a workspace holds as its own index rather than as a mirror. */
251
+ export const POLICY_GUIDE_MAP = 'README'
360
252
 
361
- /**
362
- * Whether a path is a direct module of a fleet-registered function domain.
363
- *
364
- * @param path - The workspace-relative source path to inspect.
365
- * @returns `true` when the path has the registered function-module shape.
366
- */
367
- export function isFunctionDomainPath(path: string): boolean {
368
- const normalized = normalizePolicyPath(path)
369
- const file = basename(normalized)
370
- return (
371
- FUNCTION_DOMAIN_FOLDERS.includes(dirname(normalized).replaceAll('\\', '/')) &&
372
- /^[a-z][A-Za-z0-9]*\.ts$/u.test(file) &&
373
- file !== 'index.ts' &&
374
- file !== 'main.ts' &&
375
- !CENTRAL_SOURCE_FILES.includes(file)
376
- )
377
- }
253
+ /** Names the rule file whose substitution table is the denylist's source. */
254
+ export const POLICY_TERM_FILE = '.claude/rules/writing.md'
378
255
 
379
- /**
380
- * Read the function expression an expression holds directly, through any parentheses.
381
- *
382
- * @param expression - The expression to unwrap.
383
- * @returns The arrow or function expression it holds directly, or `undefined` for anything else.
384
- */
385
- export function expressionToPolicyFunction(
386
- expression: ts.Expression | undefined,
387
- ): ts.ArrowFunction | ts.FunctionExpression | undefined {
388
- let current = expression
389
- while (current !== undefined && ts.isParenthesizedExpression(current)) {
390
- current = current.expression
391
- }
392
- if (current === undefined) return undefined
393
- return ts.isArrowFunction(current) || ts.isFunctionExpression(current) ? current : undefined
394
- }
256
+ /** Names the heading that opens the substitution table. */
257
+ export const POLICY_TERM_HEADING = '## Substitutions'
395
258
 
396
259
  /**
397
- * Whether a variable initializer is directly a function expression.
260
+ * Names the catalog agent file whose table registers every fleet package.
398
261
  *
399
- * @param initializer - The initializer to inspect.
400
- * @returns `true` for a direct arrow or function expression.
262
+ * @remarks
263
+ * The path is written here rather than read from the scaffold constant that plans it, because this
264
+ * module is vendored byte-identical into every workspace and imports nothing from the package.
401
265
  */
402
- export function isPolicyFunctionInitializer(initializer: ts.Expression | undefined): boolean {
403
- return expressionToPolicyFunction(initializer) !== undefined
404
- }
266
+ export const POLICY_CATALOG_FILE = '.claude/agents/orkestrel.md'
267
+
268
+ /** Names the heading that opens the package catalog. */
269
+ export const POLICY_CATALOG_HEADING = '## Package catalog'
405
270
 
406
271
  /**
407
- * Whether a module region holds a module policy function.
272
+ * Normalizes platform separators for stable matching and diagnostics.
408
273
  *
409
- * A module region is syntax outside every permitted function. A permitted function's parameters and
410
- * the arguments of a call sitting inside one are read under this same rule, because the law grants
411
- * those positions nothing extra.
412
- *
413
- * @param node - The module region to inspect.
414
- * @returns `true` when the region holds an arrow or function expression the law does not permit.
415
- * An arrow or function expression passed directly as a call or `new` argument is exempt itself, and
416
- * reports only for what its parameters and body nest. A class expression is never function syntax.
274
+ * @param path - The workspace-relative path to normalize.
275
+ * @returns The path with forward slashes and no duplicate separators.
417
276
  */
418
- export function hasModulePolicyFunction(node: ts.Node | undefined): boolean {
419
- if (node === undefined) return false
420
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return true
421
- if (ts.isClassExpression(node)) return false
422
- if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
423
- if (hasModulePolicyFunction(node.expression)) return true
424
- if (node.arguments !== undefined) {
425
- for (const argument of node.arguments) {
426
- const permitted = expressionToPolicyFunction(argument)
427
- if (permitted === undefined) {
428
- if (hasModulePolicyFunction(argument)) return true
429
- } else if (nestsPolicyFunction(permitted)) return true
430
- }
431
- }
432
- return false
433
- }
434
- let found = false
435
- ts.forEachChild(node, (child) => {
436
- if (!found && hasModulePolicyFunction(child)) found = true
437
- })
438
- return found
277
+ export function normalizePolicyPath(path: string): string {
278
+ return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
439
279
  }
440
280
 
441
281
  /**
442
- * Whether a permitted function nests a policy function the law does not permit.
282
+ * Normalizes a native path or `file:` URI for workspace-relative policy comparisons.
443
283
  *
444
- * The function is exempt in its own right. Its parameters read as a module region, and its body
445
- * reads as the region inside a permitted function, where a nested function keeps permission only by
446
- * qualifying independently as a direct callback or result.
447
- *
448
- * @param node - The permitted callback or returned function to inspect.
449
- * @returns `true` when its parameters or body nest function syntax the law does not permit.
284
+ * @param root - The host path used as the comparison root.
285
+ * @param filename - The diagnostic filename as a native path or `file:` URI.
286
+ * @returns The normalized path relative to the resolved comparison root.
287
+ * @throws Thrown when a `file:` filename is not a valid file URI.
450
288
  */
451
- export function nestsPolicyFunction(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
452
- for (const parameter of node.parameters) {
453
- if (hasModulePolicyFunction(parameter)) return true
454
- }
455
- if (!ts.isBlock(node.body)) {
456
- const returned = expressionToPolicyFunction(node.body)
457
- if (returned !== undefined) return nestsPolicyFunction(returned)
458
- }
459
- return hasNestedPolicyFunction(node.body)
289
+ export function normalizePolicyFilename(root: string, filename: string): string {
290
+ const base = resolve(root)
291
+ const path = filename.startsWith('file:') ? fileURLToPath(filename) : resolve(base, filename)
292
+ return normalizePolicyPath(relativePath(base, path))
460
293
  }
461
294
 
462
295
  /**
463
- * Whether a region inside a permitted function holds a nested policy function.
464
- *
465
- * A direct return keeps its permission through control flow. Every other nested function reports
466
- * unless it independently qualifies as a direct callback. Class-expression members stay outside
467
- * the instrument's reach.
468
- *
469
- * @param node - The region inside a permitted function to inspect.
470
- * @returns `true` when the region holds function syntax the law does not permit.
296
+ * Reports whether a parsed configuration value is a plain record rather than an array or a
297
+ * primitive.
471
298
  */
472
- export function hasNestedPolicyFunction(node: ts.Node): boolean {
473
- if (ts.isFunctionDeclaration(node)) return true
474
- if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return true
475
- if (ts.isClassExpression(node)) return false
476
- if (ts.isReturnStatement(node)) {
477
- const returned = expressionToPolicyFunction(node.expression)
478
- return returned === undefined
479
- ? hasModulePolicyFunction(node.expression)
480
- : nestsPolicyFunction(returned)
481
- }
482
- if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
483
- return hasModulePolicyFunction(node)
484
- }
485
- let found = false
486
- ts.forEachChild(node, (child) => {
487
- if (!found && hasNestedPolicyFunction(child)) found = true
488
- })
489
- return found
299
+ export function isPolicyRecord(value: unknown): value is Readonly<Record<string, unknown>> {
300
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
490
301
  }
491
302
 
492
303
  /**
493
- * Create one stable violation for an inspection result.
304
+ * Creates one stable violation for an inspection result.
494
305
  *
495
306
  * @param rule - The rule that failed.
496
307
  * @param path - The workspace-relative source path.
497
308
  * @param message - The failure text.
498
- * @param node - The syntax node that failed, when one exists.
309
+ * @param line - The one-based source line the violation occurred on, when known.
499
310
  * @returns The stable violation record.
500
311
  */
501
312
  export function createPolicyViolation(
502
313
  rule: PolicyRule,
503
314
  path: string,
504
315
  message: string,
505
- node?: ts.Node,
316
+ line?: number,
506
317
  ): PolicyViolation {
507
- return {
508
- rule,
509
- path,
510
- ...(node === undefined ? {} : { line: getPolicyLine(node) }),
511
- message,
512
- }
513
- }
514
-
515
- /**
516
- * Inspect a registered function module's exact declaration shape.
517
- *
518
- * @param path - The workspace-relative source path.
519
- * @param source - The parsed TypeScript source.
520
- * @returns A domain-shape violation when the module is malformed.
521
- */
522
- export function inspectFunctionDomain(
523
- path: string,
524
- source: ts.SourceFile,
525
- ): readonly PolicyViolation[] {
526
- const expected = basename(path, '.ts')
527
- const declarations = source.statements.filter(ts.isFunctionDeclaration)
528
- const implementations = declarations.filter((declaration) => declaration.body !== undefined)
529
- const invalid = source.statements.filter(
530
- (statement) => !ts.isImportDeclaration(statement) && !ts.isFunctionDeclaration(statement),
531
- )
532
- const valid =
533
- implementations.length === 1 &&
534
- invalid.length === 0 &&
535
- declarations.length > 0 &&
536
- declarations.every(
537
- (declaration) =>
538
- declaration.name?.text === expected &&
539
- hasPolicyModifier(declaration, ts.SyntaxKind.ExportKeyword) &&
540
- !hasPolicyModifier(declaration, ts.SyntaxKind.DefaultKeyword),
541
- )
542
- return valid
543
- ? []
544
- : [
545
- createPolicyViolation(
546
- 'domain',
547
- path,
548
- 'registered function modules contain imports and one matching named export',
549
- ),
550
- ]
551
- }
552
-
553
- /**
554
- * Inspect parser and factory function names without inferring their meaning.
555
- *
556
- * @param path - The workspace-relative source path.
557
- * @param file - The source filename.
558
- * @param name - The declared function name, when present.
559
- * @param node - The function declaration or binding.
560
- * @returns A parser or factory name violation when the prefix is wrong.
561
- */
562
- export function inspectPolicyFunctionName(
563
- path: string,
564
- file: string,
565
- name: string | undefined,
566
- node: ts.Node,
567
- ): readonly PolicyViolation[] {
568
- if (file === 'parsers.ts' && (name === undefined || !name.startsWith('parse'))) {
569
- return [createPolicyViolation('parser', path, 'parser functions use the parse prefix', node)]
570
- }
571
- if (file === 'factories.ts' && (name === undefined || !name.startsWith('create'))) {
572
- return [createPolicyViolation('factory', path, 'factory functions use the create prefix', node)]
573
- }
574
- return []
575
- }
576
-
577
- /**
578
- * Inspect one variable statement for data and function placement.
579
- *
580
- * @param path - The workspace-relative source path.
581
- * @param file - The source filename.
582
- * @param statement - The variable statement to inspect.
583
- * @param functionDomain - Whether the path is a registered function module.
584
- * @returns Every data, function, parser, and factory violation in declaration order.
585
- */
586
- export function inspectPolicyVariables(
587
- path: string,
588
- file: string,
589
- statement: ts.VariableStatement,
590
- functionDomain: boolean,
591
- ): readonly PolicyViolation[] {
592
- const violations: PolicyViolation[] = []
593
- for (const declaration of statement.declarationList.declarations) {
594
- const directFunction = isPolicyFunctionInitializer(declaration.initializer)
595
- const containsFunction = hasModulePolicyFunction(declaration.initializer)
596
- if (!directFunction && !DATA_SOURCE_FILES.includes(file) && !DATA_EXEMPT_FILES.includes(file)) {
597
- violations.push(
598
- createPolicyViolation('data', path, 'module data sits in a data-kind file', declaration),
599
- )
600
- }
601
- if (containsFunction && !functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
602
- violations.push(
603
- createPolicyViolation(
604
- 'function',
605
- path,
606
- 'module function syntax sits in a function-kind file',
607
- declaration,
608
- ),
609
- )
610
- }
611
- if (directFunction && ts.isIdentifier(declaration.name)) {
612
- violations.push(...inspectPolicyFunctionName(path, file, declaration.name.text, declaration))
613
- }
614
- }
615
- return violations
616
- }
617
-
618
- /**
619
- * Inspect the const, name, and bare-collection rules for constants.ts.
620
- *
621
- * @param path - The workspace-relative source path.
622
- * @param statement - The variable statement to inspect.
623
- * @returns Every constants.ts syntax violation in declaration order.
624
- */
625
- export function inspectPolicyConstants(
626
- path: string,
627
- statement: ts.VariableStatement,
628
- ): readonly PolicyViolation[] {
629
- const violations: PolicyViolation[] = []
630
- if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
631
- violations.push(
632
- createPolicyViolation(
633
- 'constant',
634
- path,
635
- 'constants.ts permits only const declarations',
636
- statement,
637
- ),
638
- )
639
- }
640
- for (const declaration of statement.declarationList.declarations) {
641
- if (!ts.isIdentifier(declaration.name) || !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)) {
642
- violations.push(
643
- createPolicyViolation(
644
- 'constant',
645
- path,
646
- 'constants.ts names declarations in UPPER_SNAKE_CASE',
647
- declaration,
648
- ),
649
- )
650
- }
651
- if (
652
- declaration.initializer !== undefined &&
653
- (ts.isArrayLiteralExpression(declaration.initializer) ||
654
- ts.isObjectLiteralExpression(declaration.initializer))
655
- ) {
656
- violations.push(
657
- createPolicyViolation(
658
- 'constant',
659
- path,
660
- 'constants.ts forbids bare collection literals',
661
- declaration,
662
- ),
663
- )
664
- }
665
- }
666
- return violations
667
- }
668
-
669
- /**
670
- * Whether a top-level statement declares a centralized symbol.
671
- *
672
- * @param statement - The top-level statement to classify.
673
- * @returns `true` when the statement declares a symbol governed by the export rule.
674
- */
675
- export function isPolicyDeclaration(statement: ts.Statement): boolean {
676
- return (
677
- ts.isClassDeclaration(statement) ||
678
- ts.isEnumDeclaration(statement) ||
679
- ts.isFunctionDeclaration(statement) ||
680
- ts.isInterfaceDeclaration(statement) ||
681
- ts.isModuleDeclaration(statement) ||
682
- ts.isTypeAliasDeclaration(statement) ||
683
- ts.isVariableStatement(statement)
684
- )
685
- }
686
-
687
- /**
688
- * Inspect one source file against the fleet's syntactic placement register.
689
- *
690
- * @param source - The path and TypeScript text to inspect.
691
- * @returns Every syntactic placement violation in source order.
692
- */
693
- export function inspectPolicySource(source: PolicySource): readonly PolicyViolation[] {
694
- const path = normalizePolicyPath(source.path)
695
- const file = basename(path)
696
- const syntax = ts.createSourceFile(path, source.content, ts.ScriptTarget.Latest, true)
697
- const violations: PolicyViolation[] = []
698
- const functionDomain = isFunctionDomainPath(path)
699
- const domainNames = FUNCTION_DOMAIN_FOLDERS.map((folder) => basename(folder))
700
-
701
- if (domainNames.includes(basename(file, '.ts'))) {
702
- violations.push(
703
- createPolicyViolation(
704
- 'domain',
705
- path,
706
- 'a registered function domain is a folder rather than a source file',
707
- ),
708
- )
709
- }
710
-
711
- for (const statement of syntax.statements) {
712
- if (
713
- CENTRAL_SOURCE_FILES.includes(file) &&
714
- isPolicyDeclaration(statement) &&
715
- !hasPolicyModifier(statement, ts.SyntaxKind.ExportKeyword)
716
- ) {
717
- violations.push(
718
- createPolicyViolation(
719
- 'export',
720
- path,
721
- 'every centralized declaration is exported',
722
- statement,
723
- ),
724
- )
725
- }
726
-
727
- if (
728
- (ts.isEnumDeclaration(statement) ||
729
- ts.isInterfaceDeclaration(statement) ||
730
- ts.isModuleDeclaration(statement) ||
731
- ts.isTypeAliasDeclaration(statement)) &&
732
- file !== 'types.ts'
733
- ) {
734
- violations.push(
735
- createPolicyViolation('type', path, 'type declarations sit in types.ts', statement),
736
- )
737
- }
738
-
739
- if (ts.isFunctionDeclaration(statement)) {
740
- if (!functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
741
- violations.push(
742
- createPolicyViolation(
743
- 'function',
744
- path,
745
- 'module functions sit in a function-kind file',
746
- statement,
747
- ),
748
- )
749
- }
750
- violations.push(...inspectPolicyFunctionName(path, file, statement.name?.text, statement))
751
- }
752
-
753
- if (ts.isVariableStatement(statement)) {
754
- violations.push(...inspectPolicyVariables(path, file, statement, functionDomain))
755
- if (file === 'constants.ts') violations.push(...inspectPolicyConstants(path, statement))
756
- }
757
-
758
- if (ts.isClassDeclaration(statement)) {
759
- const expected = basename(file, '.ts')
760
- if (
761
- file !== 'errors.ts' &&
762
- (!/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file) || statement.name?.text !== expected)
763
- ) {
764
- violations.push(
765
- createPolicyViolation(
766
- 'class',
767
- path,
768
- 'classes sit in their matching implementation or errors file',
769
- statement,
770
- ),
771
- )
772
- }
773
- }
774
- }
775
-
776
- if (functionDomain) violations.push(...inspectFunctionDomain(path, syntax))
777
- return violations
318
+ return line === undefined ? { rule, path, message } : { rule, path, line, message }
778
319
  }
779
320
 
780
321
  /**
781
- * Inspect an explicit parsed population through the same per-file route as the workspace sweep.
782
- *
783
- * @param sources - The TypeScript files to inspect.
784
- * @returns Every syntactic placement violation in source order.
785
- */
786
- export function inspectPolicySources(sources: readonly PolicySource[]): readonly PolicyViolation[] {
787
- const violations: PolicyViolation[] = []
788
- for (const source of sources) violations.push(...inspectPolicySource(source))
789
- return violations
790
- }
791
-
792
- /**
793
- * Read the parsed TypeScript source population beneath one workspace.
794
- *
795
- * @param root - The workspace root to read.
796
- * @param glob - The population to read. Default: the src and app placement population.
797
- * @returns Every non-ambient TypeScript source the population names, sorted by path.
798
- */
799
- export function readPolicySources(
800
- root: string,
801
- glob: string | readonly string[] = POLICY_SOURCE_GLOB,
802
- ): readonly PolicySource[] {
803
- return globSync(glob, { cwd: root })
804
- .map(normalizePolicyPath)
805
- .filter((path) => !POLICY_AMBIENT_SUFFIXES.some((suffix) => basename(path).endsWith(suffix)))
806
- .sort()
807
- .map((path) => ({
808
- path,
809
- content: readFileSync(join(root, path), 'utf8'),
810
- }))
811
- }
812
-
813
- /**
814
- * Derive the extensionless module stem for one mirrored module test.
322
+ * Derives the extensionless module stem for one mirrored module test.
815
323
  *
816
324
  * @param path - The workspace-relative test path.
817
325
  * @returns The extensionless module stem, or `undefined` for a reserved scope test.
@@ -824,7 +332,7 @@ export function testToPolicyStem(path: string): string | undefined {
824
332
  }
825
333
 
826
334
  /**
827
- * The candidate set is every module name a registered language resolves for a stem.
335
+ * Lists every module name a registered language resolves for a stem.
828
336
  *
829
337
  * @param stem - The extensionless workspace-relative module stem.
830
338
  * @returns Direct modules, partial modules, then a matching tests-axis setup module.
@@ -842,7 +350,7 @@ export function stemToPolicyCandidates(stem: string): readonly string[] {
842
350
  }
843
351
 
844
352
  /**
845
- * Inspect mirrored test paths against an explicit module-path population.
353
+ * Inspects mirrored test paths against an explicit module-path population.
846
354
  *
847
355
  * @param tests - The module-test paths to inspect.
848
356
  * @param modules - The existing module paths in every registered language.
@@ -872,7 +380,7 @@ export function inspectPolicyMirrorPaths(
872
380
  }
873
381
 
874
382
  /**
875
- * Inspect every mirrored module test beneath one workspace.
383
+ * Inspects every mirrored module test beneath one workspace.
876
384
  *
877
385
  * @param root - The workspace root to inspect.
878
386
  * @returns Every missing mirror violation in test-path order.
@@ -887,7 +395,7 @@ export function inspectPolicyMirrors(root: string): readonly PolicyViolation[] {
887
395
  }
888
396
 
889
397
  /**
890
- * Inspect code-shaped workspace files for lint suppression directives.
398
+ * Inspects code-shaped workspace files for lint suppression directives.
891
399
  *
892
400
  * @param root - The workspace root to inspect.
893
401
  * @returns Every suppression occurrence in path and line order.
@@ -900,12 +408,14 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
900
408
  for (let index = 0; index < lines.length; index += 1) {
901
409
  const line = lines[index]
902
410
  if (line !== undefined && POLICY_SUPPRESSION_PATTERN.test(line)) {
903
- violations.push({
904
- rule: 'suppression',
905
- path,
906
- line: index + 1,
907
- message: 'file carries a lint suppression directive',
908
- })
411
+ violations.push(
412
+ createPolicyViolation(
413
+ 'suppression',
414
+ path,
415
+ 'file carries a lint suppression directive',
416
+ index + 1,
417
+ ),
418
+ )
909
419
  }
910
420
  }
911
421
  }
@@ -913,23 +423,22 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
913
423
  }
914
424
 
915
425
  /**
916
- * Inspect the lint configuration that keeps policy rules active across the workspace.
426
+ * Inspects the lint configuration that keeps policy rules active across the workspace.
917
427
  *
918
428
  * @param configuration - The parsed Oxlint configuration to inspect.
919
429
  * @returns Every wiring violation in rule and configuration order.
920
430
  */
921
431
  export function inspectPolicyConfiguration(configuration: unknown): readonly string[] {
922
432
  const violations: string[] = []
923
- if (typeof configuration !== 'object' || configuration === null || Array.isArray(configuration)) {
433
+ if (!isPolicyRecord(configuration)) {
924
434
  return ['Oxlint configuration must be a record']
925
435
  }
926
436
 
927
437
  const rules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
928
438
  for (const rule of POLICY_WIRING_RULES) {
929
- const setting =
930
- typeof rules === 'object' && rules !== null && !Array.isArray(rules)
931
- ? Object.getOwnPropertyDescriptor(rules, rule)?.value
932
- : undefined
439
+ const setting = isPolicyRecord(rules)
440
+ ? Object.getOwnPropertyDescriptor(rules, rule)?.value
441
+ : undefined
933
442
  const severity = Array.isArray(setting) ? setting[0] : setting
934
443
  if (severity !== 'error') violations.push(`${rule} must have top-level error severity`)
935
444
  }
@@ -960,17 +469,12 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
960
469
  violations.push('overrides must be an array when declared')
961
470
  } else if (Array.isArray(overrides)) {
962
471
  for (const override of overrides) {
963
- if (typeof override !== 'object' || override === null || Array.isArray(override)) {
472
+ if (!isPolicyRecord(override)) {
964
473
  violations.push('override entries must be records')
965
474
  continue
966
475
  }
967
476
  const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
968
- if (
969
- overrideRules === undefined ||
970
- typeof overrideRules !== 'object' ||
971
- overrideRules === null ||
972
- Array.isArray(overrideRules)
973
- ) {
477
+ if (!isPolicyRecord(overrideRules)) {
974
478
  continue
975
479
  }
976
480
  for (const rule of POLICY_WIRING_RULES) {
@@ -985,7 +489,56 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
985
489
  }
986
490
 
987
491
  /**
988
- * Resolve an exact-case directory beneath a physical root.
492
+ * Inspects the lint configuration that keeps every named rule and population wired.
493
+ *
494
+ * @param configuration - The parsed Oxlint configuration to inspect.
495
+ * @param rules - Every rule id that some top-level or override rules record must enable.
496
+ * @param populations - Every glob population some override's files list must declare exactly.
497
+ * @returns One violation line per unenabled rule, then one per undeclared population.
498
+ */
499
+ export function inspectPolicyWiring(
500
+ configuration: unknown,
501
+ rules: readonly string[],
502
+ populations: ReadonlyArray<readonly string[]>,
503
+ ): readonly string[] {
504
+ if (!isPolicyRecord(configuration)) {
505
+ return ['Oxlint configuration must be a record']
506
+ }
507
+
508
+ const records: unknown[] = [Object.getOwnPropertyDescriptor(configuration, 'rules')?.value]
509
+ const declaredPopulations: string[] = []
510
+ const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
511
+ if (Array.isArray(overrides)) {
512
+ for (const entry of overrides) {
513
+ if (!isPolicyRecord(entry)) continue
514
+ records.push(Object.getOwnPropertyDescriptor(entry, 'rules')?.value)
515
+ const files: unknown = Object.getOwnPropertyDescriptor(entry, 'files')?.value
516
+ if (Array.isArray(files)) declaredPopulations.push(files.join(' '))
517
+ }
518
+ }
519
+
520
+ const enabled = new Set<string>()
521
+ for (const record of records) {
522
+ if (!isPolicyRecord(record)) continue
523
+ for (const name of Object.getOwnPropertyNames(record)) enabled.add(name)
524
+ }
525
+
526
+ const violations: string[] = []
527
+ for (const rule of rules) {
528
+ if (!enabled.has(rule))
529
+ violations.push(`${rule} is enabled by no top-level or override rules record`)
530
+ }
531
+ for (const population of populations) {
532
+ const joined = population.join(' ')
533
+ if (!declaredPopulations.includes(joined)) {
534
+ violations.push(`no override files list declares the population exactly: ${joined}`)
535
+ }
536
+ }
537
+ return violations
538
+ }
539
+
540
+ /**
541
+ * Resolves an exact-case directory beneath a physical root.
989
542
  *
990
543
  * @param root - The physical directory from which resolution starts.
991
544
  * @param path - The relative directory path to resolve.
@@ -1006,7 +559,7 @@ export function resolvePolicyDirectory(root: string, path: string): string | und
1006
559
  }
1007
560
 
1008
561
  /**
1009
- * Whether an exact-case path resolves to a regular file beneath a physical root.
562
+ * Reports whether an exact-case path resolves to a regular file beneath a physical root.
1010
563
  *
1011
564
  * @param root - The physical directory from which resolution starts.
1012
565
  * @param path - The relative file path to inspect.
@@ -1023,7 +576,7 @@ export function isPolicyFile(root: string, path: string): boolean {
1023
576
  }
1024
577
 
1025
578
  /**
1026
- * Read the immediate child directories beneath one workspace-relative path.
579
+ * Reads the immediate child directories beneath one workspace-relative path.
1027
580
  *
1028
581
  * @param root - The workspace root to inspect.
1029
582
  * @param path - The workspace-relative parent directory.
@@ -1039,7 +592,7 @@ export function readPolicyDirectories(root: string, path: string): readonly stri
1039
592
  }
1040
593
 
1041
594
  /**
1042
- * Discover the skill family from immediate directories in the workspace tree.
595
+ * Discovers the skill family from immediate directories in the workspace tree.
1043
596
  *
1044
597
  * @param root - The workspace root to inspect.
1045
598
  * @returns The sorted directory names that belong to the skill family.
@@ -1049,7 +602,7 @@ export function readSkillFamily(root: string): readonly string[] {
1049
602
  }
1050
603
 
1051
604
  /**
1052
- * Parse one skill document's frontmatter without interpreting arbitrary body lines as keys.
605
+ * Parses one skill document's frontmatter without interpreting arbitrary body lines as keys.
1053
606
  *
1054
607
  * @param content - The raw SKILL.md text.
1055
608
  * @returns The parsed fields and exact scalar source, or `undefined` for an unsupported shape.
@@ -1134,7 +687,7 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | undef
1134
687
  }
1135
688
 
1136
689
  /**
1137
- * Test whether a description carries a sentence that begins with the case-sensitive word `Use`.
690
+ * Reports whether a description carries a sentence that begins with the case-sensitive word `Use`.
1138
691
  *
1139
692
  * @param description - The parsed skill description.
1140
693
  * @returns True when the description contains the canonical trigger sentence.
@@ -1144,7 +697,7 @@ export function matchesSkillTrigger(description: string): boolean {
1144
697
  }
1145
698
 
1146
699
  /**
1147
- * Read the direct Markdown files owned by one skill's references directory.
700
+ * Reads the direct Markdown files owned by one skill's references directory.
1148
701
  *
1149
702
  * @param root - The workspace root to inspect.
1150
703
  * @param name - The discovered skill directory name.
@@ -1160,7 +713,7 @@ export function readSkillReferences(root: string, name: string): readonly string
1160
713
  }
1161
714
 
1162
715
  /**
1163
- * Create metadata in the canonical skill interface shape.
716
+ * Creates metadata in the canonical skill interface shape.
1164
717
  *
1165
718
  * @param name - The skill token the default prompt invokes.
1166
719
  * @returns Canonical skill interface metadata ending in one newline.
@@ -1177,7 +730,7 @@ export function createSkillMetadata(name: string): string {
1177
730
  }
1178
731
 
1179
732
  /**
1180
- * Parse the default prompt from the canonical skill interface shape.
733
+ * Parses the default prompt from the canonical skill interface shape.
1181
734
  *
1182
735
  * Each value is a non-empty single-quoted scalar in which `''` carries an apostrophe.
1183
736
  *
@@ -1198,7 +751,7 @@ export function parseSkillPrompt(content: string): string | undefined {
1198
751
  }
1199
752
 
1200
753
  /**
1201
- * Test whether a default prompt names one skill's token in complete form.
754
+ * Reports whether a default prompt names one skill's token in complete form.
1202
755
  *
1203
756
  * A skill directory name is lowercase letters and hyphens, so a match followed by either continues
1204
757
  * a longer name and names a different skill.
@@ -1217,7 +770,7 @@ export function matchesSkillToken(prompt: string, name: string): boolean {
1217
770
  }
1218
771
 
1219
772
  /**
1220
- * Extract the direct Markdown reference paths named in one skill document.
773
+ * Extracts the direct Markdown reference paths named in one skill document.
1221
774
  *
1222
775
  * @param content - The raw SKILL.md text.
1223
776
  * @returns Each distinct references/name.md token in sorted order.
@@ -1278,12 +831,9 @@ export function inspectSkillTemplateTODOs(
1278
831
  todo !== -1 && todo < end;
1279
832
  todo = line.indexOf('TODO', todo + 4)
1280
833
  ) {
1281
- violations.push({
1282
- rule,
1283
- path,
1284
- line: index + 1,
1285
- message: 'skill documents contain no template TODOs',
1286
- })
834
+ violations.push(
835
+ createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
836
+ )
1287
837
  }
1288
838
  if (openingBacktick === -1) break
1289
839
  const closingBacktick = line.indexOf('`', openingBacktick + 1)
@@ -1294,7 +844,7 @@ export function inspectSkillTemplateTODOs(
1294
844
  }
1295
845
 
1296
846
  /**
1297
- * Inspect one discovered skill's required files, metadata, token, and references.
847
+ * Inspects one discovered skill's required files, metadata, token, and references.
1298
848
  *
1299
849
  * @param root - The workspace root to inspect.
1300
850
  * @param name - The discovered skill directory name.
@@ -1484,7 +1034,7 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
1484
1034
  }
1485
1035
 
1486
1036
  /**
1487
- * Inspect every immediate member of the discovered skill family.
1037
+ * Inspects every immediate member of the discovered skill family.
1488
1038
  *
1489
1039
  * @param root - The workspace root to inspect.
1490
1040
  * @returns Every skill-family violation in directory and invariant order.
@@ -1496,7 +1046,7 @@ export function inspectSkillFamily(root: string): readonly PolicyViolation[] {
1496
1046
  }
1497
1047
 
1498
1048
  /**
1499
- * Inspect one provider bridge against its canonical skill twin.
1049
+ * Inspects one provider bridge against its canonical skill twin.
1500
1050
  *
1501
1051
  * @param root - The workspace root to inspect.
1502
1052
  * @param name - The shared canonical and bridge directory name.
@@ -1584,7 +1134,7 @@ export function inspectBridge(root: string, name: string): readonly PolicyViolat
1584
1134
  }
1585
1135
 
1586
1136
  /**
1587
- * Inspect the provider bridge set and every bridge shared with the canonical skill family.
1137
+ * Inspects the provider bridge set and every bridge shared with the canonical skill family.
1588
1138
  *
1589
1139
  * @param root - The workspace root to inspect.
1590
1140
  * @returns Every bridge-set and bridge-content violation in directory order.
@@ -1623,7 +1173,7 @@ export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
1623
1173
  }
1624
1174
 
1625
1175
  /**
1626
- * Read every rule path the root instruction file's rule map registers.
1176
+ * Reads every rule path the root instruction file's rule map registers.
1627
1177
  *
1628
1178
  * @param content - The raw root instruction text.
1629
1179
  * @returns Each backticked first cell beneath the rule map heading, in table order.
@@ -1643,7 +1193,7 @@ export function readPolicyRuleMap(content: string): readonly string[] {
1643
1193
  }
1644
1194
 
1645
1195
  /**
1646
- * Inspect the discovered rule family against the root instruction file's rule map.
1196
+ * Inspects the discovered rule family against the root instruction file's rule map.
1647
1197
  *
1648
1198
  * A workspace with no rule file has no rule map to keep, so the population is empty there.
1649
1199
  *
@@ -1679,7 +1229,7 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
1679
1229
  }
1680
1230
 
1681
1231
  /**
1682
- * Inspect an explicit path population for a name a Windows checkout cannot hold.
1232
+ * Inspects an explicit path population for a name a Windows checkout cannot hold.
1683
1233
  *
1684
1234
  * Each path is read through its own final segment, because the population lists every directory as
1685
1235
  * its own entry. A Windows host refuses the reserved characters and folds a case collision into one
@@ -1736,7 +1286,7 @@ export function inspectPolicyFilenamePaths(paths: readonly string[]): readonly P
1736
1286
  }
1737
1287
 
1738
1288
  /**
1739
- * Read the workspace-authored path population, directories included.
1289
+ * Reads the workspace-authored path population, directories included.
1740
1290
  *
1741
1291
  * @param root - The workspace root to read.
1742
1292
  * @returns Every authored path, sorted by path.
@@ -1746,7 +1296,7 @@ export function readPolicyPaths(root: string): readonly string[] {
1746
1296
  }
1747
1297
 
1748
1298
  /**
1749
- * Inspect the workspace-authored path population for a name a Windows checkout cannot hold.
1299
+ * Inspects the workspace-authored path population for a name a Windows checkout cannot hold.
1750
1300
  *
1751
1301
  * @param root - The workspace root to inspect.
1752
1302
  * @returns Every unusable-name and case-collision violation in path order.
@@ -1756,7 +1306,7 @@ export function inspectPolicyFilenames(root: string): readonly PolicyViolation[]
1756
1306
  }
1757
1307
 
1758
1308
  /**
1759
- * Parse the manifest's script record without interpreting any other manifest field.
1309
+ * Parses the manifest's script record without interpreting any other manifest field.
1760
1310
  *
1761
1311
  * @param content - The raw package.json text.
1762
1312
  * @returns Each script name and its command, in manifest order.
@@ -1769,9 +1319,9 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
1769
1319
  } catch {
1770
1320
  return scripts
1771
1321
  }
1772
- if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) return scripts
1322
+ if (!isPolicyRecord(manifest)) return scripts
1773
1323
  const record: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
1774
- if (typeof record !== 'object' || record === null || Array.isArray(record)) return scripts
1324
+ if (!isPolicyRecord(record)) return scripts
1775
1325
  for (const name of Object.getOwnPropertyNames(record)) {
1776
1326
  const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
1777
1327
  if (typeof command === 'string') scripts.set(name, command)
@@ -1780,7 +1330,7 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
1780
1330
  }
1781
1331
 
1782
1332
  /**
1783
- * Inspect every manifest script for a shell file no Windows host runs.
1333
+ * Inspects every manifest script for a shell file no Windows host runs.
1784
1334
  *
1785
1335
  * @param root - The workspace root to inspect.
1786
1336
  * @returns Every shell-script violation in manifest order.
@@ -1804,155 +1354,236 @@ export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
1804
1354
  }
1805
1355
 
1806
1356
  /**
1807
- * Whether a call trims a whole payload before splitting it on a line feed.
1357
+ * Reads every authored Markdown path in one workspace, sorted by path.
1808
1358
  *
1809
- * @param node - The syntax node to inspect.
1810
- * @returns True when the node is the split chain that leaves a carriage return on each line but
1811
- * the last; false otherwise.
1359
+ * @remarks
1360
+ * The walk descends the whole tree apart from the directory names
1361
+ * {@link POLICY_PROSE_EXCLUSIONS} lists, which hold installed packages, built output, scratch
1362
+ * work, and campaign records rather than prose this workspace authors.
1363
+ *
1364
+ * @param root - The workspace root to read.
1365
+ * @returns Every workspace-relative Markdown path, sorted by path.
1812
1366
  */
1813
- export function matchesPolicySplit(node: ts.Node): boolean {
1814
- if (!ts.isCallExpression(node) || node.arguments.length !== 1) return false
1815
- const split = node.expression
1816
- if (!ts.isPropertyAccessExpression(split) || split.name.text !== 'split') return false
1817
- const argument = node.arguments[0]
1818
- if (argument === undefined) return false
1819
- if (!ts.isStringLiteral(argument) && !ts.isNoSubstitutionTemplateLiteral(argument)) return false
1820
- if (argument.text !== '\n') return false
1821
- const trim = split.expression
1822
- return (
1823
- ts.isCallExpression(trim) &&
1824
- trim.arguments.length === 0 &&
1825
- ts.isPropertyAccessExpression(trim.expression) &&
1826
- trim.expression.name.text === 'trim'
1827
- )
1367
+ export function readPolicyProse(root: string): readonly string[] {
1368
+ const paths: string[] = []
1369
+ const pending: string[] = ['']
1370
+ while (pending.length > 0) {
1371
+ const relative = pending.pop() ?? ''
1372
+ for (const entry of readdirSync(join(root, relative), { withFileTypes: true })) {
1373
+ const path = relative === '' ? entry.name : `${relative}/${entry.name}`
1374
+ if (entry.isDirectory()) {
1375
+ if (!POLICY_PROSE_EXCLUSIONS.includes(entry.name)) pending.push(path)
1376
+ continue
1377
+ }
1378
+ if (entry.name.endsWith('.md')) paths.push(normalizePolicyPath(path))
1379
+ }
1380
+ }
1381
+ return paths.sort()
1828
1382
  }
1829
1383
 
1830
1384
  /**
1831
- * Whether an expression reads the host line ending from a binding named os.
1385
+ * Reads the short name one workspace manifest declares, its scope removed.
1832
1386
  *
1833
- * @param node - The syntax node to inspect.
1834
- * @returns True for an EOL member read on a binding named os; false otherwise.
1387
+ * @param root - The workspace root to read.
1388
+ * @returns The manifest name after its scope, or undefined where no manifest declares one.
1835
1389
  */
1836
- export function matchesPolicyTerminator(node: ts.Node): boolean {
1837
- return (
1838
- ts.isPropertyAccessExpression(node) &&
1839
- node.name.text === 'EOL' &&
1840
- ts.isIdentifier(node.expression) &&
1841
- node.expression.text === 'os'
1842
- )
1390
+ export function readPolicyPackage(root: string): string | undefined {
1391
+ if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return undefined
1392
+ let manifest: unknown
1393
+ try {
1394
+ manifest = JSON.parse(readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8'))
1395
+ } catch {
1396
+ return undefined
1397
+ }
1398
+ if (!isPolicyRecord(manifest)) return undefined
1399
+ const name: unknown = Object.getOwnPropertyDescriptor(manifest, 'name')?.value
1400
+ if (typeof name !== 'string') return undefined
1401
+ return name.slice(name.lastIndexOf('/') + 1)
1843
1402
  }
1844
1403
 
1845
1404
  /**
1846
- * Whether an import declaration takes the EOL member from the host module.
1405
+ * Reads every package short name the catalog table registers.
1406
+ *
1407
+ * @remarks
1408
+ * A workspace holds this file because the `catalog` verb refuses a target that lacks it, and a
1409
+ * workspace that has not received one yet registers no package, so the read yields an empty list
1410
+ * there rather than failing.
1847
1411
  *
1848
- * @param node - The syntax node to inspect.
1849
- * @returns True for a named EOL import from node:os or os; false otherwise.
1412
+ * @param root - The workspace root to read.
1413
+ * @returns Each catalog row's package short name, its scope removed, in table order.
1850
1414
  */
1851
- export function importsPolicyTerminator(node: ts.Node): boolean {
1852
- if (!ts.isImportDeclaration(node)) return false
1853
- const specifier = node.moduleSpecifier
1854
- if (!ts.isStringLiteral(specifier)) return false
1855
- if (specifier.text !== 'node:os' && specifier.text !== 'os') return false
1856
- const bindings = node.importClause?.namedBindings
1857
- if (bindings === undefined || !ts.isNamedImports(bindings)) return false
1858
- return bindings.elements.some((element) => (element.propertyName ?? element.name).text === 'EOL')
1415
+ export function readPolicyCatalog(root: string): readonly string[] {
1416
+ if (!isPolicyFile(root, POLICY_CATALOG_FILE)) return []
1417
+ const lines = readFileSync(join(root, POLICY_CATALOG_FILE), 'utf8')
1418
+ .replaceAll('\r\n', '\n')
1419
+ .split('\n')
1420
+ const heading = lines.indexOf(POLICY_CATALOG_HEADING)
1421
+ if (heading === -1) return []
1422
+ const names: string[] = []
1423
+ for (let index = heading + 1; index < lines.length; index += 1) {
1424
+ const line = lines[index]
1425
+ if (line === undefined || line.startsWith('## ')) break
1426
+ const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
1427
+ if (cell !== undefined) names.push(cell.slice(cell.lastIndexOf('/') + 1))
1428
+ }
1429
+ return names
1859
1430
  }
1860
1431
 
1861
1432
  /**
1862
- * Inspect one syntax node and its descendants for host-specific line-ending handling.
1433
+ * Reads the guide name one prose path carries when that guide is another package's to account for.
1863
1434
  *
1864
- * @param path - The workspace-relative source path.
1865
- * @param node - The syntax node to inspect.
1866
- * @returns Every line-ending violation in source order.
1435
+ * @remarks
1436
+ * A top-level `guides/<name>.md` path yields its name unless the name is the guide index or this
1437
+ * workspace's own package; every other path yields undefined. {@link isPolicyMirror} and
1438
+ * {@link isPolicyStray} split that name by catalog membership.
1439
+ *
1440
+ * @param root - The workspace root the path belongs to.
1441
+ * @param path - The workspace-relative prose path to read.
1442
+ * @returns The guide's name, or undefined where the path is not a top-level guide another package
1443
+ * could own.
1867
1444
  */
1868
- export function inspectPolicyEndingNode(path: string, node: ts.Node): readonly PolicyViolation[] {
1869
- const violations: PolicyViolation[] = []
1870
- if (matchesPolicySplit(node)) {
1871
- violations.push(
1872
- createPolicyViolation(
1873
- 'portability',
1874
- path,
1875
- 'sources split arrived text before trimming each line',
1876
- node,
1877
- ),
1878
- )
1879
- }
1880
- if (matchesPolicyTerminator(node) || importsPolicyTerminator(node)) {
1881
- violations.push(
1882
- createPolicyViolation(
1883
- 'portability',
1884
- path,
1885
- 'sources emit a line feed rather than the host line ending',
1886
- node,
1887
- ),
1888
- )
1889
- }
1890
- ts.forEachChild(node, (child) => {
1891
- violations.push(...inspectPolicyEndingNode(path, child))
1892
- })
1893
- return violations
1445
+ export function readPolicyGuide(root: string, path: string): string | undefined {
1446
+ const name = normalizePolicyPath(path).match(POLICY_MIRROR_PATTERN)?.[1]
1447
+ if (name === undefined || name === POLICY_GUIDE_MAP) return undefined
1448
+ return name === readPolicyPackage(root) ? undefined : name
1894
1449
  }
1895
1450
 
1896
1451
  /**
1897
- * Inspect one parsed source for host-specific line-ending handling.
1452
+ * Reports whether one prose path is a top-level guide the catalog registers to another package.
1453
+ *
1454
+ * @remarks
1455
+ * A mirror is fetched bytes rather than authored prose, so the term sweep leaves it to the package
1456
+ * that wrote it. The catalog table is the evidence, and it is the only evidence: the workspace's own
1457
+ * guide and the guide index are authored here, and a top-level guide the catalog does not register
1458
+ * is a finding rather than a silent exclusion.
1459
+ *
1460
+ * @param root - The workspace root the path belongs to.
1461
+ * @param path - The workspace-relative prose path to judge.
1462
+ * @returns True if the path is a top-level guide the catalog registers to a package other than this
1463
+ * one; false otherwise.
1464
+ */
1465
+ export function isPolicyMirror(root: string, path: string): boolean {
1466
+ const name = readPolicyGuide(root, path)
1467
+ return name !== undefined && readPolicyCatalog(root).includes(name)
1468
+ }
1469
+
1470
+ /**
1471
+ * Reports whether one prose path is a top-level guide no evidence accounts for.
1898
1472
  *
1899
- * @param source - The path and TypeScript text to inspect.
1900
- * @returns Every line-ending violation in source order.
1473
+ * @param root - The workspace root the path belongs to.
1474
+ * @param path - The workspace-relative prose path to judge.
1475
+ * @returns True if the path is a top-level guide that is neither this package's own, nor the index,
1476
+ * nor a catalog row; false otherwise.
1901
1477
  */
1902
- export function inspectPolicyEndingSource(source: PolicySource): readonly PolicyViolation[] {
1903
- const path = normalizePolicyPath(source.path)
1904
- const syntax = ts.createSourceFile(path, source.content, ts.ScriptTarget.Latest, true)
1905
- return inspectPolicyEndingNode(path, syntax)
1478
+ export function isPolicyStray(root: string, path: string): boolean {
1479
+ const name = readPolicyGuide(root, path)
1480
+ return name !== undefined && !readPolicyCatalog(root).includes(name)
1906
1481
  }
1907
1482
 
1908
1483
  /**
1909
- * Inspect every source axis the portability population covers for line-ending handling.
1484
+ * Inspects every authored Markdown file for a term the substitution table bans unconditionally.
1485
+ *
1486
+ * @remarks
1487
+ * Each file is stripped of its fenced blocks, its inline code spans, its link tags, and its URLs
1488
+ * before the match, by the same reader the comment rule uses, so a term inside one of those regions
1489
+ * is not prose. Stripping holds every offset, so the reported line is the line in the file.
1490
+ *
1491
+ * A top-level guide the catalog registers to another package is a mirror and is skipped, and a
1492
+ * top-level guide no evidence accounts for reports instead, so an exclusion is never silent.
1910
1493
  *
1911
1494
  * @param root - The workspace root to inspect.
1912
- * @returns Every line-ending violation in path and source order.
1495
+ * @returns Every unaccounted-guide violation, then every banned-term violation in path and offset
1496
+ * order.
1913
1497
  */
1914
- export function inspectPolicyEndings(root: string): readonly PolicyViolation[] {
1498
+ export function inspectPolicyProse(root: string): readonly PolicyViolation[] {
1915
1499
  const violations: PolicyViolation[] = []
1916
- for (const source of readPolicySources(root, POLICY_PORTABILITY_SOURCE_GLOB)) {
1917
- violations.push(...inspectPolicyEndingSource(source))
1500
+ for (const path of readPolicyProse(root)) {
1501
+ if (isPolicyStray(root, path)) {
1502
+ violations.push(
1503
+ createPolicyViolation(
1504
+ 'prose',
1505
+ path,
1506
+ "guide is the package's own, the map, or a catalog row",
1507
+ ),
1508
+ )
1509
+ }
1510
+ if (isPolicyMirror(root, path)) continue
1511
+ const prose = stripPolicyCode(readFileSync(join(root, path), 'utf8'))
1512
+ for (const hit of textToPolicyHits(prose)) {
1513
+ violations.push(
1514
+ createPolicyViolation(
1515
+ 'prose',
1516
+ path,
1517
+ `prose carries no banned term: ${hit.term.term} (${hit.term.replacement})`,
1518
+ prose.slice(0, hit.index).split('\n').length,
1519
+ ),
1520
+ )
1521
+ }
1918
1522
  }
1919
1523
  return violations
1920
1524
  }
1921
1525
 
1922
1526
  /**
1923
- * Inspect every host portability rule across one workspace.
1527
+ * Reads every term the substitution table's first column registers.
1528
+ *
1529
+ * @remarks
1530
+ * Each row's first cell carries its terms as code spans, so the read takes the backticked tokens
1531
+ * and drops the parenthetical qualifier a row writes beside one.
1532
+ *
1533
+ * @param content - The raw rule text carrying the substitution table.
1534
+ * @returns Each registered term, in table order.
1535
+ */
1536
+ export function readPolicyTerms(content: string): readonly string[] {
1537
+ const lines = content.replaceAll('\r\n', '\n').split('\n')
1538
+ const heading = lines.indexOf(POLICY_TERM_HEADING)
1539
+ if (heading === -1) return []
1540
+ const terms: string[] = []
1541
+ for (let index = heading + 1; index < lines.length; index += 1) {
1542
+ const line = lines[index]
1543
+ if (line === undefined || line.startsWith('## ')) break
1544
+ const cell = line.match(/^\|([^|]*)\|/u)?.[1]
1545
+ if (cell === undefined) continue
1546
+ for (const match of cell.matchAll(/`([^`]+)`/gu)) {
1547
+ const term = match[1]
1548
+ if (term !== undefined) terms.push(term)
1549
+ }
1550
+ }
1551
+ return terms
1552
+ }
1553
+
1554
+ /**
1555
+ * Inspects every host portability rule across one workspace.
1924
1556
  *
1925
1557
  * @param root - The workspace root to inspect.
1926
- * @returns Every rule-map, filename, manifest-script, and line-ending violation.
1558
+ * @returns Every rule-map, filename, and manifest-script violation.
1927
1559
  */
1928
1560
  export function inspectPolicyPortability(root: string): readonly PolicyViolation[] {
1929
1561
  return [
1930
1562
  ...inspectPolicyRuleMap(root),
1931
1563
  ...inspectPolicyFilenames(root),
1932
1564
  ...inspectPolicyScripts(root),
1933
- ...inspectPolicyEndings(root),
1934
1565
  ]
1935
1566
  }
1936
1567
 
1937
1568
  /**
1938
- * Inspect every policy rule across one workspace.
1569
+ * Inspects every policy rule across one workspace.
1939
1570
  *
1940
1571
  * @param root - The workspace root to inspect.
1941
- * @returns Every source, mirror, suppression, skill, bridge, and portability violation.
1572
+ * @returns Every mirror, suppression, skill, bridge, portability, and prose violation.
1942
1573
  */
1943
1574
  export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
1944
1575
  return [
1945
- ...inspectPolicySources(readPolicySources(root)),
1946
1576
  ...inspectPolicyMirrors(root),
1947
1577
  ...inspectPolicySuppressions(root),
1948
1578
  ...inspectSkillFamily(root),
1949
1579
  ...inspectSkillBridges(root),
1950
1580
  ...inspectPolicyPortability(root),
1581
+ ...inspectPolicyProse(root),
1951
1582
  ]
1952
1583
  }
1953
1584
 
1954
1585
  /**
1955
- * Write a control to a real temporary workspace and run the production sweep over it.
1586
+ * Writes a control to a real temporary workspace and runs the production sweep over it.
1956
1587
  *
1957
1588
  * The control's rule selects the sweep: `skill` inspects the canonical family, `bridge` inspects
1958
1589
  * provider bridges, and every other rule inspects the whole workspace route.
@@ -1979,7 +1610,7 @@ export function inspectPolicyControl(control: PolicyControl): readonly PolicyVio
1979
1610
  }
1980
1611
  }
1981
1612
 
1982
- /** Physical negative controls, one for each rule the instrument claims to enforce. */
1613
+ /** Lists the physical negative controls, one for each rule the sweep claims to enforce. */
1983
1614
  export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1984
1615
  {
1985
1616
  label: 'rejects a suppression directive in a scanned source file',
@@ -2003,96 +1634,6 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2003
1634
  },
2004
1635
  ],
2005
1636
  },
2006
- {
2007
- label: 'rejects a type outside types.ts',
2008
- membership: 'top-level type declarations whose filename is not types.ts',
2009
- rule: 'type',
2010
- files: [{ path: 'src/mobile/helpers.ts', content: 'export interface ValueInterface {}\n' }],
2011
- },
2012
- {
2013
- label: 'rejects an inline function in routes.ts',
2014
- membership: 'module-level function syntax whose filename is absent from the function register',
2015
- rule: 'function',
2016
- files: [
2017
- {
2018
- path: 'src/worker/routes.ts',
2019
- content: 'export const ROUTES = Object.freeze([{ handler: () => undefined }])\n',
2020
- },
2021
- ],
2022
- },
2023
- {
2024
- label: 'rejects data in handlers.ts',
2025
- membership: 'module data whose filename is absent from the data register',
2026
- rule: 'data',
2027
- files: [{ path: 'app/edge/handlers.ts', content: "export const STATUS = 'ready'\n" }],
2028
- },
2029
- {
2030
- label: 'rejects a hidden centralized declaration',
2031
- membership: 'centralized declarations without an export modifier',
2032
- rule: 'export',
2033
- files: [{ path: 'src/worker/helpers.ts', content: 'function buildValue(): void {}\n' }],
2034
- },
2035
- {
2036
- label: 'rejects a class that differs from its file',
2037
- membership: 'class declarations outside errors.ts whose names differ from their filename',
2038
- rule: 'class',
2039
- files: [{ path: 'app/desktop/Widget.ts', content: 'export class Other {}\n' }],
2040
- },
2041
- {
2042
- label: 'rejects mutable constants',
2043
- membership: 'variable statements in constants.ts that are not const',
2044
- rule: 'constant',
2045
- files: [{ path: 'src/worker/constants.ts', content: 'export let COUNT = 1\n' }],
2046
- },
2047
- {
2048
- label: 'rejects lower-case constants',
2049
- membership: 'declarations in constants.ts whose names are not UPPER_SNAKE_CASE',
2050
- rule: 'constant',
2051
- files: [{ path: 'src/worker/constants.ts', content: 'export const count = 1\n' }],
2052
- },
2053
- {
2054
- label: 'rejects bare collection constants',
2055
- membership: 'declarations in constants.ts with direct array or object literal initializers',
2056
- rule: 'constant',
2057
- files: [{ path: 'src/worker/constants.ts', content: 'export const VALUES = []\n' }],
2058
- },
2059
- {
2060
- label: 'rejects a parser without the parse prefix',
2061
- membership: 'function declarations in parsers.ts whose names do not start with parse',
2062
- rule: 'parser',
2063
- files: [{ path: 'app/edge/parsers.ts', content: 'export function coerceValue(): void {}\n' }],
2064
- },
2065
- {
2066
- label: 'rejects a factory without the create prefix',
2067
- membership: 'function declarations in factories.ts whose names do not start with create',
2068
- rule: 'factory',
2069
- files: [{ path: 'app/edge/factories.ts', content: 'export function buildValue(): void {}\n' }],
2070
- },
2071
- {
2072
- label: 'rejects a malformed registered function module',
2073
- membership: 'direct camelCase modules in a registered function-domain folder',
2074
- rule: 'domain',
2075
- files: [
2076
- {
2077
- path: 'app/browser/composables/useTheme.ts',
2078
- content: 'export function useMode(): void {}\n',
2079
- },
2080
- ],
2081
- },
2082
- {
2083
- label: 'rejects a file named for a function domain',
2084
- membership: 'source files whose stem is registered as a function-domain folder name',
2085
- rule: 'domain',
2086
- files: [{ path: 'app/edge/composables.ts', content: '' }],
2087
- },
2088
- {
2089
- label: 'rejects a function in an unregistered domain',
2090
- membership: 'function modules whose parent path is absent from the domain register',
2091
- rule: 'function',
2092
- files: [
2093
- { path: 'src/worker/jobs/runTask.ts', content: 'export function runTask(): void {}\n' },
2094
- ],
2095
- },
2096
1637
  {
2097
1638
  label: 'rejects an unmirrored module test',
2098
1639
  membership: 'module tests below tests/src or tests/app except integration.test.ts',
@@ -2137,97 +1678,9 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2137
1678
  { path: 'tests/app/core/widget.test.ts', content: '' },
2138
1679
  ],
2139
1680
  },
2140
- {
2141
- label: 'rejects a type in a non-ambient env module',
2142
- membership: 'non-ambient TypeScript modules whose filename is not types.ts',
2143
- rule: 'type',
2144
- files: [{ path: 'app/browser/env.ts', content: 'export interface EnvironmentInterface {}\n' }],
2145
- },
2146
- {
2147
- label: 'rejects a property-held arrow in constants.ts',
2148
- membership: 'module-level function syntax not passed directly as an argument',
2149
- rule: 'function',
2150
- files: [
2151
- {
2152
- path: 'app/edge/constants.ts',
2153
- content: 'export const HANDLERS = Object.freeze({ run: () => undefined })\n',
2154
- },
2155
- ],
2156
- },
2157
- {
2158
- label: 'rejects a callback parameter default function',
2159
- membership: 'function expressions in callback parameter defaults',
2160
- rule: 'function',
2161
- files: [
2162
- {
2163
- path: 'app/edge/constants.ts',
2164
- content: 'export const VALUES = Object.freeze(C.map((c = () => 1) => c))\n',
2165
- },
2166
- ],
2167
- },
2168
- {
2169
- label: 'rejects a destructured callback parameter default function',
2170
- membership: 'function expressions in destructured callback parameter defaults',
2171
- rule: 'function',
2172
- files: [
2173
- {
2174
- path: 'app/edge/constants.ts',
2175
- content: 'export const VALUES = Object.freeze(C.map(({ f = () => 1 }) => f))\n',
2176
- },
2177
- ],
2178
- },
2179
- {
2180
- label: 'rejects an assignment inside callback control flow',
2181
- membership: 'function assignments inside callback control-flow branches',
2182
- rule: 'function',
2183
- files: [
2184
- {
2185
- path: 'app/edge/constants.ts',
2186
- content:
2187
- 'export const VALUES = Object.freeze(C.map((c) => { if (c) { const f = () => 1; return f() } return 2 }))\n',
2188
- },
2189
- ],
2190
- },
2191
- {
2192
- label: 'rejects an assignment inside a direct callback',
2193
- membership: 'function assignments inside the body of a callback passed directly as an argument',
2194
- rule: 'function',
2195
- files: [
2196
- {
2197
- path: 'app/edge/constants.ts',
2198
- content:
2199
- 'export const VALUES = Object.freeze(C.map((c) => { const f = () => c; return f() }))\n',
2200
- },
2201
- ],
2202
- },
2203
- {
2204
- label: 'rejects a declaration inside a direct callback',
2205
- membership:
2206
- 'function declarations inside the body of a callback passed directly as an argument',
2207
- rule: 'function',
2208
- files: [
2209
- {
2210
- path: 'app/edge/constants.ts',
2211
- content:
2212
- 'export const LABELS = Object.freeze(COLUMNS.map((column) => { function format() { return column.label } return format() }))\n',
2213
- },
2214
- ],
2215
- },
2216
- {
2217
- label: 'rejects an assignment two direct callbacks down',
2218
- membership: 'function assignments inside a callback the outer callback passes directly',
2219
- rule: 'function',
2220
- files: [
2221
- {
2222
- path: 'app/edge/constants.ts',
2223
- content:
2224
- 'export const VALUES = Object.freeze(C.map((c) => wrap((d) => { const g = () => d; return g() })))\n',
2225
- },
2226
- ],
2227
- },
2228
1681
  ])
2229
1682
 
2230
- /** Physical in-family controls for every skill-family assertion class. */
1683
+ /** Lists the physical in-family controls for every skill-family assertion class. */
2231
1684
  export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2232
1685
  {
2233
1686
  label: 'rejects a SKILL.md without frontmatter',
@@ -2503,7 +1956,7 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2503
1956
  },
2504
1957
  ])
2505
1958
 
2506
- /** Physical controls for provider-bridge assertions. */
1959
+ /** Lists the physical controls for provider-bridge assertions. */
2507
1960
  export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2508
1961
  {
2509
1962
  label: 'rejects a canonical skill without a provider bridge',
@@ -2632,7 +2085,9 @@ export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2632
2085
  },
2633
2086
  ])
2634
2087
 
2635
- /** An in-family skill whose metadata values carry escaped apostrophes, proving they parse. */
2088
+ /**
2089
+ * Describes an in-family skill whose metadata values carry escaped apostrophes, proving they parse.
2090
+ */
2636
2091
  export const SKILL_POLICY_APOSTROPHE: PolicyControl = Object.freeze({
2637
2092
  label: 'accepts escaped apostrophes in agents/openai.yaml values',
2638
2093
  membership: 'immediate directories beneath .agents/skills',
@@ -2643,7 +2098,9 @@ export const SKILL_POLICY_APOSTROPHE: PolicyControl = Object.freeze({
2643
2098
  ],
2644
2099
  })
2645
2100
 
2646
- /** A folded description containing a colon, proving continuation lines do not become keys. */
2101
+ /**
2102
+ * Describes a folded description containing a colon, proving continuation lines do not become keys.
2103
+ */
2647
2104
  export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
2648
2105
  label: 'accepts a folded description containing a colon',
2649
2106
  membership: 'folded description scalars in discovered skill frontmatter',
@@ -2658,7 +2115,7 @@ export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
2658
2115
  ],
2659
2116
  })
2660
2117
 
2661
- /** A healthy skill reference whose prose carries the documented backticked TODO form. */
2118
+ /** Describes a healthy skill reference whose prose carries the documented backticked TODO form. */
2662
2119
  export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
2663
2120
  label: 'accepts a backticked TODO in skill prose',
2664
2121
  membership: 'TODO occurrences inside matched inline backtick spans in discovered skill documents',
@@ -2673,7 +2130,7 @@ export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
2673
2130
  ],
2674
2131
  })
2675
2132
 
2676
- /** A healthy skill whose fenced example carries a template-TODO spelling. */
2133
+ /** Describes a healthy skill whose fenced example carries a template-TODO spelling. */
2677
2134
  export const SKILL_POLICY_FENCED: PolicyControl = Object.freeze({
2678
2135
  label: 'accepts a TODO in a three-space-indented fenced skill example',
2679
2136
  membership:
@@ -2690,7 +2147,7 @@ export const SKILL_POLICY_FENCED: PolicyControl = Object.freeze({
2690
2147
  ],
2691
2148
  })
2692
2149
 
2693
- /** A folded description whose blank scalar line separates its paragraphs. */
2150
+ /** Describes a folded description whose blank scalar line separates its paragraphs. */
2694
2151
  export const SKILL_POLICY_PARAGRAPHS: PolicyControl = Object.freeze({
2695
2152
  label: 'accepts a folded description containing two paragraphs',
2696
2153
  membership: 'folded description scalars in discovered skill frontmatter',
@@ -2705,7 +2162,9 @@ export const SKILL_POLICY_PARAGRAPHS: PolicyControl = Object.freeze({
2705
2162
  ],
2706
2163
  })
2707
2164
 
2708
- /** A bridge skill outside the discovered family, used to prove the membership boundary. */
2165
+ /**
2166
+ * Describes a bridge skill outside the discovered family, used to prove the membership boundary.
2167
+ */
2709
2168
  export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
2710
2169
  label: 'excludes .claude/skills from the skill family',
2711
2170
  membership: 'directories outside .agents/skills',
@@ -2714,7 +2173,7 @@ export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
2714
2173
  })
2715
2174
 
2716
2175
  /**
2717
- * Create root instruction text whose rule map names an explicit rule set.
2176
+ * Creates root instruction text whose rule map names an explicit rule set.
2718
2177
  *
2719
2178
  * @param rules - The workspace-relative rule paths the map registers.
2720
2179
  * @returns Root instruction text carrying one rule map table.
@@ -2733,7 +2192,27 @@ export function createPolicyRuleMap(rules: readonly string[]): string {
2733
2192
  )
2734
2193
  }
2735
2194
 
2736
- /** Physical controls for every rule-map parity assertion the workspace route reaches. */
2195
+ /**
2196
+ * Creates catalog agent text whose package table names an explicit package set.
2197
+ *
2198
+ * @param names - The package short names the catalog registers.
2199
+ * @returns Catalog agent text carrying one package table.
2200
+ */
2201
+ export function createPolicyCatalog(names: readonly string[]): string {
2202
+ return (
2203
+ [
2204
+ '# Orkestrel',
2205
+ '',
2206
+ POLICY_CATALOG_HEADING,
2207
+ '',
2208
+ '| Package | Version |',
2209
+ '| ------- | ------- |',
2210
+ ...names.map((name) => `| \`@orkestrel/${name}\` | \`0.0.1\` |`),
2211
+ ].join('\n') + '\n'
2212
+ )
2213
+ }
2214
+
2215
+ /** Lists the physical controls for every rule-map parity assertion the workspace route reaches. */
2737
2216
  export const RULES_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2738
2217
  {
2739
2218
  label: 'rejects a rule file the rule map omits',
@@ -2763,137 +2242,173 @@ export const RULES_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2763
2242
  },
2764
2243
  ])
2765
2244
 
2766
- /** Physical controls for every portability assertion the workspace route reaches. */
2767
- export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2768
- {
2769
- label: 'rejects a reserved device name',
2770
- membership: 'path segments in the workspace-authored path population',
2771
- rule: 'portability',
2772
- message: 'path segments avoid the names Windows reserves',
2773
- files: [{ path: 'src/worker/con.ts', content: '' }],
2774
- },
2245
+ /** Holds the manifest one prose control writes, naming the package its own guide belongs to. */
2246
+ export const PROSE_POLICY_MANIFEST = '{\n\t"name": "@orkestrel/sample"\n}\n'
2247
+
2248
+ /**
2249
+ * Lists the physical controls for every prose-population boundary the workspace route reaches.
2250
+ *
2251
+ * @remarks
2252
+ * Each control that attacks an exclusion also writes the arrival file, whose different term proves
2253
+ * the sweep ran over the control workspace rather than reporting nothing because it found nothing.
2254
+ */
2255
+ export const PROSE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2775
2256
  {
2776
- label: 'rejects a segment that ends with a dot',
2777
- membership: 'path segments in the workspace-authored path population',
2778
- rule: 'portability',
2779
- message: 'path segments end with neither a dot nor a space',
2780
- files: [{ path: 'src/worker/helpers.ts.', content: '' }],
2257
+ label: 'rejects a banned term in the workspace front page',
2258
+ membership: 'authored Markdown outside the excluded directories and the guide mirrors',
2259
+ rule: 'prose',
2260
+ line: 3,
2261
+ message: 'prose carries no banned term: should (must, can, might, or the imperative)',
2262
+ files: [
2263
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2264
+ { path: 'README.md', content: '# Front page\n\nA reader should meet this term.\n' },
2265
+ ],
2781
2266
  },
2782
2267
  {
2783
- label: 'rejects a segment that ends with a space',
2784
- membership: 'path segments in the workspace-authored path population',
2785
- rule: 'portability',
2786
- message: 'path segments end with neither a dot nor a space',
2787
- files: [{ path: 'src/worker/helpers.ts ', content: '' }],
2268
+ label: 'rejects a banned term in the package guide',
2269
+ membership: 'the top-level guide whose name matches the manifest name',
2270
+ rule: 'prose',
2271
+ line: 3,
2272
+ message: 'prose carries no banned term: should (must, can, might, or the imperative)',
2273
+ files: [
2274
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2275
+ { path: 'guides/sample.md', content: '# Sample\n\nA reader should meet this term.\n' },
2276
+ ],
2788
2277
  },
2789
2278
  {
2790
- label: 'rejects a shell script named by a manifest script',
2791
- membership: 'string values beneath the manifest scripts record',
2792
- rule: 'portability',
2793
- message: 'manifest scripts name no .sh file: prepare',
2279
+ label: 'rejects a banned term in a rule file',
2280
+ membership: 'authored Markdown below a dot directory the sweep descends into',
2281
+ rule: 'prose',
2282
+ line: 3,
2283
+ message: 'prose carries no banned term: should (must, can, might, or the imperative)',
2794
2284
  files: [
2285
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2795
2286
  {
2796
- path: POLICY_MANIFEST_FILE,
2797
- content: '{\n\t"scripts": {\n\t\t"prepare": "bash scripts/prepare.sh"\n\t}\n}\n',
2287
+ path: `${POLICY_RULE_ROOT}/sample.md`,
2288
+ content: '# Sample\n\nA reader should meet this term.\n',
2289
+ },
2290
+ {
2291
+ path: POLICY_RULE_MAP_FILE,
2292
+ content: createPolicyRuleMap([`${POLICY_RULE_ROOT}/sample.md`]),
2798
2293
  },
2799
2294
  ],
2800
2295
  },
2801
2296
  {
2802
- label: 'rejects a payload trimmed before it is split',
2803
- membership: 'split calls carrying a line-feed string literal in the parsed source axes',
2804
- rule: 'portability',
2805
- message: 'sources split arrived text before trimming each line',
2297
+ label: 'accepts a banned term inside a fenced block',
2298
+ membership: 'fenced regions, whose lines are code rather than prose',
2299
+ rule: 'prose',
2300
+ line: 3,
2301
+ message: 'prose carries no banned term: via (through, by using)',
2806
2302
  files: [
2303
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2304
+ { path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
2807
2305
  {
2808
- path: 'src/worker/helpers.ts',
2809
- content:
2810
- "export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
2306
+ path: 'guides/README.md',
2307
+ content: '# Index\n\n```text\nshould inside a fence\n```\n',
2811
2308
  },
2812
2309
  ],
2813
2310
  },
2814
2311
  {
2815
- label: 'rejects a read of the host line ending',
2816
- membership: 'EOL member reads on a binding named os in the parsed source axes',
2817
- rule: 'portability',
2818
- message: 'sources emit a line feed rather than the host line ending',
2312
+ label: 'accepts a banned term inside a code span a line break runs through',
2313
+ membership: 'inline code spans, whose text is a token rather than prose',
2314
+ rule: 'prose',
2315
+ line: 3,
2316
+ message: 'prose carries no banned term: via (through, by using)',
2819
2317
  files: [
2318
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2319
+ { path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
2820
2320
  {
2821
- path: 'configs/helpers.ts',
2822
- content:
2823
- "import * as os from 'node:os'\nexport function endLine(): string {\n\treturn os.EOL\n}\n",
2321
+ path: 'guides/README.md',
2322
+ content: '# Index\n\nA span `that\nspans lines with should` here.\n',
2824
2323
  },
2825
2324
  ],
2826
2325
  },
2827
2326
  {
2828
- label: 'rejects an EOL import from node:os',
2829
- membership: 'named import specifiers from node:os in the parsed source axes',
2830
- rule: 'portability',
2831
- message: 'sources emit a line feed rather than the host line ending',
2327
+ label: 'accepts a banned term in a guide the catalog registers to another package',
2328
+ membership: 'top-level guides the catalog registers to a package other than this one',
2329
+ rule: 'prose',
2330
+ line: 3,
2331
+ message: 'prose carries no banned term: via (through, by using)',
2332
+ files: [
2333
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2334
+ { path: POLICY_CATALOG_FILE, content: createPolicyCatalog(['other', 'sample']) },
2335
+ { path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
2336
+ { path: 'guides/other.md', content: '# Other\n\nA reader should meet this term.\n' },
2337
+ ],
2338
+ },
2339
+ {
2340
+ label: 'rejects a top-level guide the catalog does not register',
2341
+ membership: 'top-level guides that are neither this package, nor the index, nor a catalog row',
2342
+ rule: 'prose',
2343
+ message: "guide is the package's own, the map, or a catalog row",
2832
2344
  files: [
2345
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2346
+ { path: POLICY_CATALOG_FILE, content: createPolicyCatalog(['other', 'sample']) },
2347
+ { path: 'guides/stray.md', content: '# Stray\n\nA reader reads this guide.\n' },
2348
+ ],
2349
+ },
2350
+ {
2351
+ label: 'accepts a banned term inside an installed package',
2352
+ membership: 'Markdown below a directory name the sweep never descends into',
2353
+ rule: 'prose',
2354
+ line: 3,
2355
+ message: 'prose carries no banned term: via (through, by using)',
2356
+ files: [
2357
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2358
+ { path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
2833
2359
  {
2834
- path: 'configs/helpers.ts',
2835
- content:
2836
- "import { EOL } from 'node:os'\nexport function endLine(): string {\n\treturn EOL\n}\n",
2360
+ path: 'node_modules/sample/README.md',
2361
+ content: '# Installed\n\nA reader should meet this term.\n',
2837
2362
  },
2838
2363
  ],
2839
2364
  },
2365
+ {
2366
+ label: 'accepts a banned term inside scratch work',
2367
+ membership: 'Markdown below a directory name the sweep never descends into',
2368
+ rule: 'prose',
2369
+ line: 3,
2370
+ message: 'prose carries no banned term: via (through, by using)',
2371
+ files: [
2372
+ { path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
2373
+ { path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
2374
+ { path: 'tmp/notes.md', content: '# Notes\n\nA reader should meet this term.\n' },
2375
+ ],
2376
+ },
2840
2377
  ])
2841
2378
 
2842
- /** A trimmed split outside the parsed source axes, proving the population boundary. */
2843
- export const PORTABILITY_POLICY_EXCLUSION: PolicyControl = Object.freeze({
2844
- label: 'excludes a script module from the parsed source axes',
2845
- membership: 'TypeScript modules outside the src, app, and configs axes',
2846
- rule: 'portability',
2847
- files: [
2848
- {
2849
- path: 'scripts/read.ts',
2850
- content:
2851
- "export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
2852
- },
2853
- ],
2854
- })
2855
-
2856
- /** A locally declared line-ending constant, which the host line-ending rule leaves legal. */
2857
- export const PORTABILITY_POLICY_LOCAL: PolicyControl = Object.freeze({
2858
- label: 'accepts a locally declared EOL constant',
2859
- membership: 'EOL identifiers that neither read a binding named os nor import from node:os',
2860
- rule: 'portability',
2861
- files: [{ path: 'src/worker/constants.ts', content: "export const EOL = '\\n'\n" }],
2862
- })
2863
-
2864
- /** A split on the host-independent line-ending pattern, which the split rule leaves legal. */
2865
- export const PORTABILITY_POLICY_SPLIT: PolicyControl = Object.freeze({
2866
- label: 'accepts a split on the line-ending pattern',
2867
- membership: 'split calls whose argument is not a line-feed string literal',
2868
- rule: 'portability',
2869
- files: [
2870
- {
2871
- path: 'src/worker/helpers.ts',
2872
- content:
2873
- 'export function readLines(text: string): readonly string[] {\n\treturn text.trim().split(/\\r\\n|\\n/u)\n}\n',
2874
- },
2875
- ],
2876
- })
2877
-
2878
- /** A differently shaped workspace with app, browser, and worker environments but no core. */
2879
- export const GENERIC_POLICY_SOURCES: readonly PolicySource[] = Object.freeze([
2379
+ /** Lists the physical controls for every portability assertion the workspace route reaches. */
2380
+ export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2880
2381
  {
2881
- path: 'src/worker/types.ts',
2882
- content: 'export interface TaskInterface { readonly id: string }\n',
2382
+ label: 'rejects a reserved device name',
2383
+ membership: 'path segments in the workspace-authored path population',
2384
+ rule: 'portability',
2385
+ message: 'path segments avoid the names Windows reserves',
2386
+ files: [{ path: 'src/worker/con.ts', content: '' }],
2883
2387
  },
2884
- { path: 'src/worker/Worker.ts', content: 'export class Worker {}\n' },
2885
2388
  {
2886
- path: 'app/browser/composables/useTheme.ts',
2887
- content: 'export function useTheme(): void {}\n',
2389
+ label: 'rejects a segment that ends with a dot',
2390
+ membership: 'path segments in the workspace-authored path population',
2391
+ rule: 'portability',
2392
+ message: 'path segments end with neither a dot nor a space',
2393
+ files: [{ path: 'src/worker/helpers.ts.', content: '' }],
2888
2394
  },
2889
- { path: 'app/browser/handlers.ts', content: 'export function open(): void {}\n' },
2890
2395
  {
2891
- path: 'app/browser/routes.ts',
2892
- content:
2893
- "import { open } from './handlers.js'\nexport const ROUTES = Object.freeze([{ method: 'GET', path: '/', handler: open }])\n",
2396
+ label: 'rejects a segment that ends with a space',
2397
+ membership: 'path segments in the workspace-authored path population',
2398
+ rule: 'portability',
2399
+ message: 'path segments end with neither a dot nor a space',
2400
+ files: [{ path: 'src/worker/helpers.ts ', content: '' }],
2894
2401
  },
2895
2402
  {
2896
- path: 'src/worker/constants.ts',
2897
- content: "export const LABELS = Object.freeze(['ready'])\n",
2403
+ label: 'rejects a shell script named by a manifest script',
2404
+ membership: 'string values beneath the manifest scripts record',
2405
+ rule: 'portability',
2406
+ message: 'manifest scripts name no .sh file: prepare',
2407
+ files: [
2408
+ {
2409
+ path: POLICY_MANIFEST_FILE,
2410
+ content: '{\n\t"scripts": {\n\t\t"prepare": "bash scripts/prepare.sh"\n\t}\n}\n',
2411
+ },
2412
+ ],
2898
2413
  },
2899
2414
  ])