@orkestrel/scaffold 0.0.23 → 0.0.25

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 (39) hide show
  1. package/README.md +84 -99
  2. package/dist/bin/main.js +1094 -0
  3. package/dist/bin/main.js.map +1 -0
  4. package/dist/host/CLAUDE.md +3 -1
  5. package/dist/host/agents/orchestration.md +61 -4
  6. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
  7. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
  8. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
  9. package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
  10. package/dist/host/claude/agents/orkestrel.md +4 -4
  11. package/dist/host/claude/rules/architecture.md +45 -3
  12. package/dist/host/claude/rules/quality.md +4 -0
  13. package/dist/host/claude/rules/tests.md +57 -1
  14. package/dist/host/claude/rules/workspace.md +50 -17
  15. package/dist/host/codex/agents/orkestrel.toml +1 -1
  16. package/dist/host/configs/helpers.ts +762 -0
  17. package/dist/host/dotfiles/oxlintrc.json +2 -1
  18. package/dist/host/guides/scaffold.md +862 -0
  19. package/dist/host/manifest.json +40 -33
  20. package/dist/host/tests/config.test.ts +544 -0
  21. package/dist/host/tests/policy.test.ts +46 -0
  22. package/dist/host/tests/setupPolicy.ts +529 -701
  23. package/dist/src/core/index.cjs +3568 -10576
  24. package/dist/src/core/index.cjs.map +1 -1
  25. package/dist/src/core/index.d.cts +2361 -2800
  26. package/dist/src/core/index.d.ts +2361 -2800
  27. package/dist/src/core/index.js +3512 -10440
  28. package/dist/src/core/index.js.map +1 -1
  29. package/dist/src/server/index.cjs +2855 -3765
  30. package/dist/src/server/index.cjs.map +1 -1
  31. package/dist/src/server/index.d.cts +1915 -1330
  32. package/dist/src/server/index.d.ts +1915 -1330
  33. package/dist/src/server/index.js +2812 -3680
  34. package/dist/src/server/index.js.map +1 -1
  35. package/package.json +16 -23
  36. package/dist/bin/scaffold.js +0 -1896
  37. package/dist/bin/scaffold.js.map +0 -1
  38. package/dist/host/guides/src/scaffold.md +0 -2922
  39. /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
@@ -1,19 +1,55 @@
1
- import { globSync, readFileSync } from 'node:fs'
2
- import { isBuiltin } from 'node:module'
3
- import { basename, extname, join } from 'node:path'
1
+ import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { basename, dirname, join } from 'node:path'
4
4
  import * as ts from 'typescript'
5
5
 
6
- /** Centralized source modules whose top-level declarations must all be exported. */
6
+ /** A rule the fleet placement instrument can decide from syntax and a file path. */
7
+ export type PolicyRule =
8
+ | 'class'
9
+ | 'constant'
10
+ | 'data'
11
+ | 'domain'
12
+ | 'export'
13
+ | 'factory'
14
+ | 'function'
15
+ | 'mirror'
16
+ | 'parser'
17
+ | 'type'
18
+
19
+ /** One TypeScript source supplied to the placement instrument. */
20
+ export interface PolicySource {
21
+ readonly path: string
22
+ readonly content: string
23
+ }
24
+
25
+ /** One placement failure reported by the instrument. */
26
+ export interface PolicyViolation {
27
+ readonly rule: PolicyRule
28
+ readonly path: string
29
+ readonly line?: number
30
+ readonly message: string
31
+ }
32
+
33
+ /** One physical negative control, including the population boundary it attacks. */
34
+ export interface PolicyControl {
35
+ readonly label: string
36
+ readonly membership: string
37
+ readonly rule: PolicyRule
38
+ readonly files: readonly PolicySource[]
39
+ }
40
+
41
+ /** Every centralized module named by the architecture kind table. */
7
42
  export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
8
- 'combinators.ts',
9
43
  'cloners.ts',
44
+ 'combinators.ts',
10
45
  'compilers.ts',
11
46
  'constants.ts',
12
47
  'contracts.ts',
13
48
  'errors.ts',
14
49
  'factories.ts',
15
- 'helpers.ts',
16
50
  'handlers.ts',
51
+ 'helpers.ts',
52
+ 'index.ts',
17
53
  'inferers.ts',
18
54
  'middlewares.ts',
19
55
  'parsers.ts',
@@ -27,10 +63,10 @@ export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
27
63
  'validators.ts',
28
64
  ])
29
65
 
30
- /** Centralized files that own module-scope function declarations. */
66
+ /** The exhaustive centralized-file set that permits module functions. */
31
67
  export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
32
- 'combinators.ts',
33
68
  'cloners.ts',
69
+ 'combinators.ts',
34
70
  'compilers.ts',
35
71
  'errors.ts',
36
72
  'factories.ts',
@@ -40,14 +76,13 @@ export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
40
76
  'middlewares.ts',
41
77
  'parsers.ts',
42
78
  'relations.ts',
43
- 'routes.ts',
44
79
  'schemas.ts',
45
80
  'seeders.ts',
46
81
  'shapers.ts',
47
82
  'validators.ts',
48
83
  ])
49
84
 
50
- /** Centralized files that own module-scope data declarations. */
85
+ /** Centralized files that permit module data by declaration syntax. */
51
86
  export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
52
87
  'combinators.ts',
53
88
  'constants.ts',
@@ -60,837 +95,630 @@ export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
60
95
  'validators.ts',
61
96
  ])
62
97
 
63
- /** Domain folders whose modules each export one named function rather than one class. */
98
+ /** Fleet-registered folders whose direct modules each contain one named function. */
64
99
  export const FUNCTION_DOMAIN_FOLDERS: readonly string[] = Object.freeze(['app/browser/composables'])
65
100
 
66
- /** Worker-only value globals that WebWorker typing must not expose to core implementations. */
67
- export const WORKER_SCOPE_VALUE_GLOBALS: readonly string[] = Object.freeze([
68
- 'name',
69
- 'onrtctransform',
70
- 'close',
71
- 'postMessage',
72
- 'dispatchEvent',
73
- 'location',
74
- 'onerror',
75
- 'onlanguagechange',
76
- 'onoffline',
77
- 'ononline',
78
- 'onrejectionhandled',
79
- 'onunhandledrejection',
80
- 'self',
81
- 'importScripts',
82
- 'fonts',
83
- 'caches',
84
- 'crossOriginIsolated',
85
- 'indexedDB',
86
- 'isSecureContext',
87
- 'origin',
88
- 'scheduler',
89
- 'createImageBitmap',
90
- 'reportError',
91
- 'cancelAnimationFrame',
92
- 'requestAnimationFrame',
93
- 'onmessage',
94
- 'onmessageerror',
95
- 'addEventListener',
96
- 'removeEventListener',
97
- ])
98
-
99
- /** Source extensions inspected by the repository coding-law sweep. */
100
- export const CODING_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
101
- 'cjs',
101
+ /** TypeScript source extensions whose declaration syntax the sweep reads. */
102
+ export const POLICY_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
102
103
  'cts',
103
- 'js',
104
- 'jsx',
105
- 'mjs',
106
104
  'mts',
107
105
  'ts',
108
106
  'tsx',
109
- 'vue',
110
107
  ])
111
108
 
112
- /** Production-source glob derived from the complete inspected extension vocabulary. */
113
- export const CODING_SOURCE_GLOB = `{app,src}/**/*.{${CODING_SOURCE_EXTENSIONS.join(',')}}`
109
+ /** The complete TypeScript source population inspected under either workspace axis. */
110
+ export const POLICY_SOURCE_GLOB = `{app,src}/**/*.{${POLICY_SOURCE_EXTENSIONS.join(',')}}`
114
111
 
115
- /** Fleet-owned policy files that must stay free of any one package's architecture. */
116
- export const POLICY_INFRASTRUCTURE_FILES: readonly string[] = Object.freeze([
117
- 'tests/policy.test.ts',
118
- 'tests/setupPolicy.ts',
119
- ])
120
-
121
- /** Source environments whose prefixed paths fleet policy must not name in a literal. */
122
- export const POLICY_SOURCE_ENVIRONMENTS: readonly string[] = Object.freeze([
123
- 'browser',
124
- 'core',
125
- 'server',
126
- ])
112
+ /** The mirrored module-test population inspected under either workspace axis. */
113
+ export const POLICY_TEST_GLOB = 'tests/{app,src}/**/*.test.ts'
127
114
 
128
115
  /**
129
- * The prefixed source-environment path fleet policy must not name in a literal.
116
+ * Normalize platform separators for stable matching and diagnostics.
130
117
  *
131
- * @remarks
132
- * Built rather than written, so this file states no matching literal of its own
133
- * and can never report itself. Policy names an environment without the source
134
- * prefix when it must name one at all, which is why the prefix is the rule.
118
+ * @param path - The workspace-relative path to normalize.
119
+ * @returns The path with forward slashes and no duplicate separators.
135
120
  */
136
- export const POLICY_ENVIRONMENT_PATTERN: RegExp = new RegExp(
137
- `src/(?:${POLICY_SOURCE_ENVIRONMENTS.join('|')})/`,
138
- 'u',
139
- )
140
-
141
- /** Virtual source text used while binding one policy-inspected module. */
142
- export const POLICY_SOURCE_TEXTS: Map<string, string> = new Map()
143
-
144
- /** One script block extracted from a Vue SFC by the official compiler. */
145
- export interface VueScriptBlockInterface {
146
- readonly content: string
147
- readonly lang?: string
148
- }
149
-
150
- /** An injected official Vue SFC script-block extractor. */
151
- export interface VueScriptExtractorInterface {
152
- (path: string, content: string): readonly VueScriptBlockInterface[]
153
- }
154
-
155
- /** Normalize platform separators and duplicate glob segments for stable diagnostics. */
156
121
  export function normalizePolicyPath(path: string): string {
157
122
  return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
158
123
  }
159
124
 
160
- /** Whether a path belongs to the production-source coding-law corpus. */
161
- export function isCodingSourcePath(path: string): boolean {
162
- const normalized = normalizePolicyPath(path)
163
- const extension = normalized.split('.').pop()
164
- return (
165
- (normalized.startsWith('app/') || normalized.startsWith('src/')) &&
166
- extension !== undefined &&
167
- CODING_SOURCE_EXTENSIONS.includes(extension)
168
- )
169
- }
170
-
171
125
  /**
172
- * Whether a path is an eligible function-domain module.
126
+ * Whether a declaration carries a specified TypeScript modifier.
173
127
  *
174
- * @param path - The workspace-relative source path to inspect
175
- * @returns `true` when the path is a direct module of a registered function domain
128
+ * @param node - The declaration to inspect.
129
+ * @param modifier - The modifier syntax to find.
130
+ * @returns `true` when the declaration carries the modifier.
176
131
  */
177
- export function isFunctionDomainPath(path: string): boolean {
178
- const normalized = normalizePolicyPath(path)
179
- const file = basename(normalized)
180
- const separator = normalized.lastIndexOf('/')
181
- const parent = separator < 0 ? '' : normalized.slice(0, separator)
182
- return (
183
- FUNCTION_DOMAIN_FOLDERS.includes(parent) &&
184
- /^[a-z][A-Za-z0-9]*\.ts$/u.test(file) &&
185
- file !== 'index.ts' &&
186
- file !== 'main.ts' &&
187
- !CENTRAL_SOURCE_FILES.includes(file) &&
188
- !FUNCTION_SOURCE_FILES.includes(file) &&
189
- !DATA_SOURCE_FILES.includes(file)
190
- )
191
- }
192
-
193
- /** Whether a declaration carries an explicit export modifier. */
194
- export function hasExportModifier(node: ts.Node): boolean {
195
- return (
196
- ts.canHaveModifiers(node) &&
197
- ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
198
- true
199
- )
200
- }
201
-
202
- /** Whether a declaration carries a specified modifier. */
203
- export function hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
132
+ export function hasPolicyModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
204
133
  return (
205
134
  ts.canHaveModifiers(node) &&
206
135
  ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
207
136
  )
208
137
  }
209
138
 
210
- /** Whether a module specifier uses a URL scheme other than Node's builtin namespace. */
211
- export function isUnsupportedModuleSpecifier(specifier: string): boolean {
212
- return /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(specifier) && !specifier.startsWith('node:')
213
- }
214
-
215
- /** Whether an arrow/function expression is an anonymous callback passed directly as an argument. */
216
- export function isDirectCallback(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
217
- const parent = node.parent
218
- return (
219
- (ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
220
- parent.arguments?.some((argument) => argument === node) === true
221
- )
222
- }
223
-
224
139
  /**
225
- * Whether an arrow/function expression is returned directly as a factory or combinator result.
140
+ * Return the one-based line where a syntax node begins.
226
141
  *
227
- * @param node - The function expression to inspect
228
- * @returns `true` when the node is a direct return value, including one parenthesized layer
142
+ * @param node - The syntax node to locate.
143
+ * @returns Its one-based source line.
229
144
  */
230
- export function isDirectReturn(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
231
- const parent = node.parent
232
- if (ts.isReturnStatement(parent)) return true
233
- if (ts.isArrowFunction(parent) && parent.body === node) return true
234
- if (!ts.isParenthesizedExpression(parent)) return false
235
- const container = parent.parent
236
- return (
237
- ts.isReturnStatement(container) || (ts.isArrowFunction(container) && container.body === parent)
238
- )
239
- }
240
-
241
- /** Whether a function expression is assigned by one module-scope variable declaration. */
242
- export function isModuleFunction(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
243
- const declaration = node.parent
244
- const list = declaration.parent
245
- const statement = list.parent
246
- return (
247
- ts.isVariableDeclaration(declaration) &&
248
- ts.isVariableDeclarationList(list) &&
249
- ts.isVariableStatement(statement) &&
250
- ts.isSourceFile(statement.parent)
251
- )
145
+ export function getPolicyLine(node: ts.Node): number {
146
+ const position = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart())
147
+ return position.line + 1
252
148
  }
253
149
 
254
150
  /**
255
- * Format a syntax node's source position as a one-based line and character.
151
+ * Whether a path is a direct module of a fleet-registered function domain.
256
152
  *
257
- * @param node - The syntax node whose starting position to format
258
- * @returns The node's one-based `line:character` position
153
+ * @param path - The workspace-relative source path to inspect.
154
+ * @returns `true` when the path has the registered function-module shape.
259
155
  */
260
- export function formatPolicyPosition(node: ts.Node): string {
261
- const source = node.getSourceFile()
262
- const position = source.getLineAndCharacterOfPosition(node.getStart())
263
- return `${String(position.line + 1)}:${String(position.character + 1)}`
264
- }
265
-
266
- /** Whether a property signature belongs to a centralized interface or type alias contract. */
267
- export function isContractProperty(node: ts.PropertySignature): boolean {
268
- let parent: ts.Node = node.parent
269
- while (ts.isTypeLiteralNode(parent)) parent = parent.parent
270
- return ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)
271
- }
272
-
273
- /** Whether the sole production triple-slash reference is the generated browser Vite contract. */
274
- export function hasAllowedTripleSlashReference(path: string, source: ts.SourceFile): boolean {
156
+ export function isFunctionDomainPath(path: string): boolean {
157
+ const normalized = normalizePolicyPath(path)
158
+ const file = basename(normalized)
275
159
  return (
276
- path.replaceAll('\\', '/') === 'app/browser/env.d.ts' &&
277
- source.referencedFiles.length === 0 &&
278
- source.libReferenceDirectives.length === 0 &&
279
- source.typeReferenceDirectives.length === 1 &&
280
- source.typeReferenceDirectives[0]?.fileName === 'vite/client'
160
+ FUNCTION_DOMAIN_FOLDERS.includes(dirname(normalized).replaceAll('\\', '/')) &&
161
+ /^[a-z][A-Za-z0-9]*\.ts$/u.test(file) &&
162
+ file !== 'index.ts' &&
163
+ file !== 'main.ts' &&
164
+ !CENTRAL_SOURCE_FILES.includes(file)
281
165
  )
282
166
  }
283
167
 
284
168
  /**
285
- * Whether a source is self-contained around a positively identified Node runtime dependency.
169
+ * Whether a variable initializer is directly a function expression.
286
170
  *
287
- * @param source - The parsed source file to inspect.
288
- * @returns `true` when at least one value import names a real `node:` builtin and no sibling,
289
- * re-exported, or dynamic runtime dependency is present; type-only imports are erased.
290
- *
291
- * @example
292
- * ```ts
293
- * const source = ts.createSourceFile(
294
- * 'serve.ts',
295
- * "import { parentPort } from 'node:worker_threads'",
296
- * ts.ScriptTarget.Latest,
297
- * true,
298
- * )
299
- * isSelfContained(source) // true
300
- * ```
171
+ * @param initializer - The initializer to inspect.
172
+ * @returns `true` for a direct arrow or function expression.
301
173
  */
302
- export function isSelfContained(source: ts.SourceFile): boolean {
303
- const pending: ts.Node[] = [source]
304
- while (pending.length > 0) {
305
- const node = pending.pop()
306
- if (node === undefined) continue
307
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
308
- return false
309
- }
310
- ts.forEachChild(node, (child) => {
311
- pending.push(child)
312
- })
174
+ export function isPolicyFunctionInitializer(initializer: ts.Expression | undefined): boolean {
175
+ let expression = initializer
176
+ while (expression !== undefined && ts.isParenthesizedExpression(expression)) {
177
+ expression = expression.expression
313
178
  }
314
-
315
- let builtin = false
316
- for (const statement of source.statements) {
317
- if (ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
318
- return false
319
- }
320
- if (ts.isImportDeclaration(statement)) {
321
- const clause = statement.importClause
322
- const named = clause?.namedBindings
323
- const erased =
324
- clause?.phaseModifier === ts.SyntaxKind.TypeKeyword ||
325
- (clause !== undefined &&
326
- clause.name === undefined &&
327
- named !== undefined &&
328
- ts.isNamedImports(named) &&
329
- named.elements.length > 0 &&
330
- named.elements.every((element) => element.isTypeOnly))
331
- if (erased) continue
332
- const moduleSpecifier = statement.moduleSpecifier
333
- if (!ts.isStringLiteral(moduleSpecifier)) return false
334
- const specifier = moduleSpecifier.text
335
- if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
336
- builtin = true
337
- }
338
- if (ts.isImportEqualsDeclaration(statement)) {
339
- if (statement.isTypeOnly) continue
340
- const reference = statement.moduleReference
341
- if (
342
- !ts.isExternalModuleReference(reference) ||
343
- reference.expression === undefined ||
344
- !ts.isStringLiteral(reference.expression)
345
- ) {
346
- return false
347
- }
348
- const specifier = reference.expression.text
349
- if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
350
- builtin = true
351
- }
352
- }
353
- return builtin
179
+ return (
180
+ expression !== undefined &&
181
+ (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
182
+ )
354
183
  }
355
184
 
356
185
  /**
357
- * Whether the policy compiler can read one source path.
186
+ * Whether an expression contains module-level function syntax.
358
187
  *
359
- * @param path - The source path to inspect
360
- * @returns `true` when the virtual or physical source exists
188
+ * @param node - The initializer subtree to inspect.
189
+ * @returns `true` when the subtree contains an arrow or function expression.
361
190
  */
362
- export function hasPolicySource(path: string): boolean {
363
- return POLICY_SOURCE_TEXTS.has(path) || ts.sys.fileExists(path)
191
+ export function hasPolicyFunctionExpression(node: ts.Node | undefined): boolean {
192
+ if (node === undefined) return false
193
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return true
194
+ if (ts.isClassExpression(node)) return false
195
+ let found = false
196
+ ts.forEachChild(node, (child) => {
197
+ if (!found && hasPolicyFunctionExpression(child)) found = true
198
+ })
199
+ return found
364
200
  }
365
201
 
366
202
  /**
367
- * Read one virtual or physical policy source.
203
+ * Create one stable violation for an inspection result.
368
204
  *
369
- * @param path - The source path to read
370
- * @returns The source text when present
205
+ * @param rule - The rule that failed.
206
+ * @param path - The workspace-relative source path.
207
+ * @param message - The failure text.
208
+ * @param node - The syntax node that failed, when one exists.
209
+ * @returns The stable violation record.
371
210
  */
372
- export function readPolicySource(path: string): string | undefined {
373
- return POLICY_SOURCE_TEXTS.get(path) ?? ts.sys.readFile(path)
211
+ export function createPolicyViolation(
212
+ rule: PolicyRule,
213
+ path: string,
214
+ message: string,
215
+ node?: ts.Node,
216
+ ): PolicyViolation {
217
+ return {
218
+ rule,
219
+ path,
220
+ ...(node === undefined ? {} : { line: getPolicyLine(node) }),
221
+ message,
222
+ }
374
223
  }
375
224
 
376
225
  /**
377
- * Parse one virtual or physical policy source for the compiler host.
226
+ * Inspect a registered function module's exact declaration shape.
378
227
  *
379
- * @param path - The source path to parse
380
- * @param language - The requested TypeScript language target
381
- * @returns The parsed source file when present
228
+ * @param path - The workspace-relative source path.
229
+ * @param source - The parsed TypeScript source.
230
+ * @returns A domain-shape violation when the module is malformed.
382
231
  */
383
- export function createPolicySource(
232
+ export function inspectFunctionDomain(
384
233
  path: string,
385
- language: ts.ScriptTarget | ts.CreateSourceFileOptions,
386
- ): ts.SourceFile | undefined {
387
- const content = readPolicySource(path)
388
- return content === undefined ? undefined : ts.createSourceFile(path, content, language, true)
234
+ source: ts.SourceFile,
235
+ ): readonly PolicyViolation[] {
236
+ const expected = basename(path, '.ts')
237
+ const declarations = source.statements.filter(ts.isFunctionDeclaration)
238
+ const implementations = declarations.filter((declaration) => declaration.body !== undefined)
239
+ const invalid = source.statements.filter(
240
+ (statement) => !ts.isImportDeclaration(statement) && !ts.isFunctionDeclaration(statement),
241
+ )
242
+ const valid =
243
+ implementations.length === 1 &&
244
+ invalid.length === 0 &&
245
+ declarations.length > 0 &&
246
+ declarations.every(
247
+ (declaration) =>
248
+ declaration.name?.text === expected &&
249
+ hasPolicyModifier(declaration, ts.SyntaxKind.ExportKeyword) &&
250
+ !hasPolicyModifier(declaration, ts.SyntaxKind.DefaultKeyword),
251
+ )
252
+ return valid
253
+ ? []
254
+ : [
255
+ createPolicyViolation(
256
+ 'domain',
257
+ path,
258
+ 'registered function modules contain imports and one matching named export',
259
+ ),
260
+ ]
389
261
  }
390
262
 
391
263
  /**
392
- * Bind one policy-inspected module without loading ambient host declarations.
264
+ * Inspect parser and factory function names without inferring their meaning.
393
265
  *
394
- * @param path - The source path used in diagnostics
395
- * @param content - The TypeScript source text to bind
396
- * @returns A one-file TypeScript program whose checker resolves lexical bindings
266
+ * @param path - The workspace-relative source path.
267
+ * @param file - The source filename.
268
+ * @param name - The declared function name, when present.
269
+ * @param node - The function declaration or binding.
270
+ * @returns A parser or factory name violation when the prefix is wrong.
397
271
  */
398
- export function createPolicyProgram(path: string, content: string): ts.Program {
399
- const options: ts.CompilerOptions = {
400
- allowJs: true,
401
- noLib: true,
402
- noResolve: true,
403
- target: ts.ScriptTarget.Latest,
404
- types: [],
272
+ export function inspectPolicyFunctionName(
273
+ path: string,
274
+ file: string,
275
+ name: string | undefined,
276
+ node: ts.Node,
277
+ ): readonly PolicyViolation[] {
278
+ if (file === 'parsers.ts' && (name === undefined || !name.startsWith('parse'))) {
279
+ return [createPolicyViolation('parser', path, 'parser functions use the parse prefix', node)]
280
+ }
281
+ if (file === 'factories.ts' && (name === undefined || !name.startsWith('create'))) {
282
+ return [createPolicyViolation('factory', path, 'factory functions use the create prefix', node)]
405
283
  }
406
- POLICY_SOURCE_TEXTS.set(path, content)
407
- const host = ts.createCompilerHost(options)
408
- host.fileExists = hasPolicySource
409
- host.readFile = readPolicySource
410
- host.getSourceFile = createPolicySource
411
- const program = ts.createProgram([path], options, host)
412
- POLICY_SOURCE_TEXTS.delete(path)
413
- return program
284
+ return []
414
285
  }
415
286
 
416
287
  /**
417
- * Whether an identifier is a standalone runtime value reference.
288
+ * Inspect one variable statement for data and function placement.
418
289
  *
419
- * @param node - The identifier occurrence to classify
420
- * @param checker - The binder used to distinguish lexical values from ambient globals
421
- * @returns `true` only when the occurrence reads or writes a runtime value
290
+ * @param path - The workspace-relative source path.
291
+ * @param file - The source filename.
292
+ * @param statement - The variable statement to inspect.
293
+ * @param functionDomain - Whether the path is a registered function module.
294
+ * @returns Every data, function, parser, and factory violation in declaration order.
422
295
  */
423
- export function isValueReferenceIdentifier(node: ts.Identifier, checker: ts.TypeChecker): boolean {
424
- if (ts.isPartOfTypeNode(node)) return false
425
-
426
- let ancestor: ts.Node = node.parent
427
- while (!ts.isSourceFile(ancestor) && !ts.isStatement(ancestor)) {
428
- if (ts.isTypeQueryNode(ancestor)) return false
429
- if (ts.isComputedPropertyName(ancestor) && ts.isTypeElement(ancestor.parent)) return false
430
- ancestor = ancestor.parent
431
- }
432
-
433
- const parent = node.parent
434
- if (
435
- (ts.isPropertyAccessExpression(parent) && parent.name === node) ||
436
- (ts.isPropertyAssignment(parent) && parent.name === node) ||
437
- (ts.isPropertyDeclaration(parent) && parent.name === node) ||
438
- (ts.isPropertySignature(parent) && parent.name === node) ||
439
- (ts.isMethodDeclaration(parent) && parent.name === node) ||
440
- (ts.isMethodSignature(parent) && parent.name === node) ||
441
- (ts.isGetAccessorDeclaration(parent) && parent.name === node) ||
442
- (ts.isSetAccessorDeclaration(parent) && parent.name === node) ||
443
- (ts.isVariableDeclaration(parent) && parent.name === node) ||
444
- (ts.isParameter(parent) && parent.name === node) ||
445
- (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
446
- (ts.isFunctionDeclaration(parent) && parent.name === node) ||
447
- (ts.isFunctionExpression(parent) && parent.name === node) ||
448
- (ts.isClassDeclaration(parent) && parent.name === node) ||
449
- (ts.isClassExpression(parent) && parent.name === node) ||
450
- (ts.isInterfaceDeclaration(parent) && parent.name === node) ||
451
- (ts.isTypeAliasDeclaration(parent) && parent.name === node) ||
452
- (ts.isTypeParameterDeclaration(parent) && parent.name === node) ||
453
- (ts.isEnumDeclaration(parent) && parent.name === node) ||
454
- (ts.isEnumMember(parent) && parent.name === node) ||
455
- (ts.isModuleDeclaration(parent) && parent.name === node) ||
456
- ts.isImportClause(parent) ||
457
- ts.isImportSpecifier(parent) ||
458
- ts.isNamespaceImport(parent) ||
459
- ts.isImportEqualsDeclaration(parent) ||
460
- ts.isExportSpecifier(parent) ||
461
- ts.isNamespaceExport(parent) ||
462
- ts.isNamespaceExportDeclaration(parent) ||
463
- (ts.isLabeledStatement(parent) && parent.label === node) ||
464
- (ts.isBreakOrContinueStatement(parent) && parent.label === node) ||
465
- (ts.isJsxAttribute(parent) && parent.name === node)
466
- ) {
467
- return false
468
- }
469
- return (
470
- (ts.isShorthandPropertyAssignment(parent)
471
- ? checker.getShorthandAssignmentValueSymbol(parent)
472
- : checker.getSymbolAtLocation(node)) === undefined
473
- )
474
- }
475
-
476
- /** Inspect a Vue single-file component for syntax that can bypass declared import policy. */
477
- export function inspectVueCodingLaw(
296
+ export function inspectPolicyVariables(
478
297
  path: string,
479
- scripts: readonly VueScriptBlockInterface[] = [],
480
- ): readonly string[] {
481
- const violations: string[] = []
482
- for (const [index, script] of scripts.entries()) {
483
- if (
484
- script.lang !== 'ts' &&
485
- script.lang !== 'tsx' &&
486
- script.lang !== 'mts' &&
487
- script.lang !== 'cts'
488
- ) {
489
- violations.push(`${path}.script-${String(index)} requires a TypeScript script language`)
298
+ file: string,
299
+ statement: ts.VariableStatement,
300
+ functionDomain: boolean,
301
+ ): readonly PolicyViolation[] {
302
+ const violations: PolicyViolation[] = []
303
+ for (const declaration of statement.declarationList.declarations) {
304
+ const directFunction = isPolicyFunctionInitializer(declaration.initializer)
305
+ const containsFunction = hasPolicyFunctionExpression(declaration.initializer)
306
+ if (!directFunction && !DATA_SOURCE_FILES.includes(file)) {
307
+ violations.push(
308
+ createPolicyViolation('data', path, 'module data sits in a data-kind file', declaration),
309
+ )
310
+ }
311
+ if (containsFunction && !functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
312
+ violations.push(
313
+ createPolicyViolation(
314
+ 'function',
315
+ path,
316
+ 'module function syntax sits in a function-kind file',
317
+ declaration,
318
+ ),
319
+ )
320
+ }
321
+ if (directFunction && ts.isIdentifier(declaration.name)) {
322
+ violations.push(...inspectPolicyFunctionName(path, file, declaration.name.text, declaration))
490
323
  }
491
- const extension =
492
- script.lang === 'tsx' || script.lang === 'mts' || script.lang === 'cts' ? script.lang : 'ts'
493
- violations.push(
494
- ...inspectCodingLaw(`${path}.script-${String(index)}.${extension}`, script.content),
495
- )
496
- }
497
- return violations
498
- }
499
-
500
- /** Inspect one production source through the shared coding-law route. */
501
- export function inspectCodingSource(
502
- path: string,
503
- content: string,
504
- vueScripts?: VueScriptExtractorInterface,
505
- ): readonly string[] {
506
- const normalizedPath = normalizePolicyPath(path)
507
- if (!normalizedPath.endsWith('.vue')) return inspectCodingLaw(normalizedPath, content)
508
- const violations: string[] = []
509
- if (!normalizedPath.startsWith('app/browser/')) {
510
- violations.push(`${normalizedPath} Vue components belong in app/browser`)
511
- }
512
- // A missing extractor is reported, never absorbed. Returning [] here would make
513
- // every SFC's script block silently unchecked while the surrounding sweep still
514
- // reported success — an instrument claiming a coverage it does not have.
515
- if (vueScripts === undefined) {
516
- violations.push(
517
- `${normalizedPath} requires a Vue script extractor; its script blocks were not inspected`,
518
- )
519
- return violations
520
324
  }
521
- violations.push(...inspectVueCodingLaw(normalizedPath, vueScripts(normalizedPath, content)))
522
325
  return violations
523
326
  }
524
327
 
525
- /** Add syntax-wide coding-law violations while traversing one source tree. */
526
- export function inspectCodingNode(
328
+ /**
329
+ * Inspect the const, name, and bare-collection rules for constants.ts.
330
+ *
331
+ * @param path - The workspace-relative source path.
332
+ * @param statement - The variable statement to inspect.
333
+ * @returns Every constants.ts syntax violation in declaration order.
334
+ */
335
+ export function inspectPolicyConstants(
527
336
  path: string,
528
- node: ts.Node,
529
- violations: string[],
530
- checker: ts.TypeChecker,
531
- ): void {
532
- if (
533
- /^(?:app|src)[\\/]core[\\/]/u.test(path) &&
534
- ts.isIdentifier(node) &&
535
- WORKER_SCOPE_VALUE_GLOBALS.includes(node.text) &&
536
- isValueReferenceIdentifier(node, checker)
537
- ) {
337
+ statement: ts.VariableStatement,
338
+ ): readonly PolicyViolation[] {
339
+ const violations: PolicyViolation[] = []
340
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
538
341
  violations.push(
539
- `${path}:${formatPolicyPosition(node)} forbids worker-scope global ${node.text} in core`,
342
+ createPolicyViolation(
343
+ 'constant',
344
+ path,
345
+ 'constants.ts permits only const declarations',
346
+ statement,
347
+ ),
540
348
  )
541
349
  }
542
- if (
543
- ts.isAsExpression(node) ||
544
- ts.isTypeAssertionExpression(node) ||
545
- ts.isNonNullExpression(node)
546
- ) {
547
- violations.push(`${path}:${formatPolicyPosition(node)} forbids type/non-null assertions`)
548
- }
549
- if (node.kind === ts.SyntaxKind.AnyKeyword) {
550
- violations.push(`${path}:${formatPolicyPosition(node)} forbids any`)
551
- }
552
- if (
553
- (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
554
- node.moduleSpecifier !== undefined &&
555
- ts.isStringLiteral(node.moduleSpecifier) &&
556
- isUnsupportedModuleSpecifier(node.moduleSpecifier.text)
557
- ) {
558
- violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
559
- }
560
- if (
561
- ts.isPropertySignature(node) &&
562
- isContractProperty(node) &&
563
- !hasModifier(node, ts.SyntaxKind.ReadonlyKeyword)
564
- ) {
565
- violations.push(`${path}:${formatPolicyPosition(node)} requires readonly contract properties`)
566
- }
567
- if (
568
- ts.isCallExpression(node) &&
569
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
570
- (node.arguments.length !== 1 || !node.arguments.every(ts.isStringLiteral))
571
- ) {
572
- violations.push(
573
- `${path}:${formatPolicyPosition(node)} requires dynamic imports to use string literals so import policy remains enforceable`,
574
- )
575
- }
576
- if (
577
- ts.isCallExpression(node) &&
578
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
579
- node.arguments.length === 1 &&
580
- node.arguments.every(ts.isStringLiteral) &&
581
- isUnsupportedModuleSpecifier(node.arguments[0]?.text ?? '')
582
- ) {
583
- violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
584
- }
585
- if (ts.isFunctionDeclaration(node) && !ts.isSourceFile(node.parent)) {
586
- violations.push(`${path}:${formatPolicyPosition(node)} forbids nested function declarations`)
587
- }
588
- if (
589
- (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
590
- !isDirectCallback(node) &&
591
- !isDirectReturn(node) &&
592
- !isModuleFunction(node)
593
- ) {
594
- violations.push(`${path}:${formatPolicyPosition(node)} forbids hidden function assignments`)
350
+ for (const declaration of statement.declarationList.declarations) {
351
+ if (!ts.isIdentifier(declaration.name) || !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)) {
352
+ violations.push(
353
+ createPolicyViolation(
354
+ 'constant',
355
+ path,
356
+ 'constants.ts names declarations in UPPER_SNAKE_CASE',
357
+ declaration,
358
+ ),
359
+ )
360
+ }
361
+ if (
362
+ declaration.initializer !== undefined &&
363
+ (ts.isArrayLiteralExpression(declaration.initializer) ||
364
+ ts.isObjectLiteralExpression(declaration.initializer))
365
+ ) {
366
+ violations.push(
367
+ createPolicyViolation(
368
+ 'constant',
369
+ path,
370
+ 'constants.ts forbids bare collection literals',
371
+ declaration,
372
+ ),
373
+ )
374
+ }
595
375
  }
596
- ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations, checker))
376
+ return violations
597
377
  }
598
378
 
599
379
  /**
600
- * Inspect one eligible function-domain module for its required declaration shape.
380
+ * Whether a top-level statement declares a centralized symbol.
601
381
  *
602
- * @param path - The source path used in diagnostics
603
- * @param source - The parsed source file to inspect
604
- * @returns A shape violation when the module is not imports plus one matching named export
382
+ * @param statement - The top-level statement to classify.
383
+ * @returns `true` when the statement declares a symbol governed by the export rule.
605
384
  */
606
- export function inspectFunctionModule(path: string, source: ts.SourceFile): readonly string[] {
607
- const file = basename(normalizePolicyPath(path))
608
- const declarations = source.statements.filter(ts.isFunctionDeclaration)
609
- const functions = declarations.filter((declaration) => declaration.body !== undefined)
610
- const invalid = source.statements.filter(
611
- (statement) => !ts.isImportDeclaration(statement) && !ts.isFunctionDeclaration(statement),
385
+ export function isPolicyDeclaration(statement: ts.Statement): boolean {
386
+ return (
387
+ ts.isClassDeclaration(statement) ||
388
+ ts.isEnumDeclaration(statement) ||
389
+ ts.isFunctionDeclaration(statement) ||
390
+ ts.isInterfaceDeclaration(statement) ||
391
+ ts.isModuleDeclaration(statement) ||
392
+ ts.isTypeAliasDeclaration(statement) ||
393
+ ts.isVariableStatement(statement)
612
394
  )
613
- const declaration = functions[0]
614
- if (
615
- functions.length === 1 &&
616
- invalid.length === 0 &&
617
- declarations.every((candidate) => candidate.name?.text === file.slice(0, -3)) &&
618
- declaration !== undefined &&
619
- hasExportModifier(declaration) &&
620
- !hasModifier(declaration, ts.SyntaxKind.DefaultKeyword)
621
- ) {
622
- return []
623
- }
624
- return [`${path} declarations do not form one matching exported function implementation`]
625
395
  }
626
396
 
627
- /** Inspect one TypeScript source module for repository coding-law violations. */
628
- export function inspectCodingLaw(path: string, content: string): readonly string[] {
629
- const violations: string[] = []
630
- const program = createPolicyProgram(path, content)
631
- const source = program.getSourceFile(path)
632
- if (source === undefined) throw new Error(`Policy source was not bound at ${path}`)
633
- const checker = program.getTypeChecker()
397
+ /**
398
+ * Inspect one source file against the fleet's syntactic placement register.
399
+ *
400
+ * @param source - The path and TypeScript text to inspect.
401
+ * @returns Every syntactic placement violation in source order.
402
+ */
403
+ export function inspectPolicySource(source: PolicySource): readonly PolicyViolation[] {
404
+ const path = normalizePolicyPath(source.path)
634
405
  const file = basename(path)
635
- const stem = basename(file, extname(file))
636
- const functionModule = isFunctionDomainPath(path)
637
- const placementExempt =
638
- !CENTRAL_SOURCE_FILES.includes(file) &&
639
- !FUNCTION_SOURCE_FILES.includes(file) &&
640
- !DATA_SOURCE_FILES.includes(file) &&
641
- isSelfContained(source)
642
-
643
- if (/\.[cm]?jsx?$/u.test(path)) {
644
- violations.push(`${path} production modules use TypeScript source extensions`)
645
- }
646
- if (FUNCTION_DOMAIN_FOLDERS.some((folder) => basename(folder) === stem)) {
647
- violations.push(`${path} names a function domain, which belongs in a folder rather than a file`)
648
- }
649
- if (/@ts-(?:expect-error|ignore|nocheck)|eslint-disable|oxlint-disable/u.test(content)) {
650
- violations.push(`${path} forbids suppression directives`)
651
- }
652
- if (
653
- (source.referencedFiles.length > 0 ||
654
- source.libReferenceDirectives.length > 0 ||
655
- source.typeReferenceDirectives.length > 0) &&
656
- !hasAllowedTripleSlashReference(path, source)
657
- ) {
658
- violations.push(`${path} forbids triple-slash references outside app/browser/env.d.ts`)
659
- }
406
+ const syntax = ts.createSourceFile(path, source.content, ts.ScriptTarget.Latest, true)
407
+ const violations: PolicyViolation[] = []
408
+ const functionDomain = isFunctionDomainPath(path)
409
+ const domainNames = FUNCTION_DOMAIN_FOLDERS.map((folder) => basename(folder))
660
410
 
661
- if (file === 'index.ts') {
662
- for (const statement of source.statements) {
663
- if (
664
- !ts.isExportDeclaration(statement) ||
665
- statement.exportClause !== undefined ||
666
- statement.isTypeOnly ||
667
- statement.moduleSpecifier === undefined
668
- ) {
669
- violations.push(`${path} barrels contain only export * declarations`)
670
- }
671
- }
411
+ if (domainNames.includes(basename(file, '.ts'))) {
412
+ violations.push(
413
+ createPolicyViolation(
414
+ 'domain',
415
+ path,
416
+ 'a registered function domain is a folder rather than a source file',
417
+ ),
418
+ )
672
419
  }
673
420
 
674
- for (const statement of source.statements) {
675
- if (
676
- (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
677
- file !== 'types.ts'
678
- ) {
679
- violations.push(`${path} centralizes interfaces and type aliases in types.ts`)
680
- }
421
+ for (const statement of syntax.statements) {
681
422
  if (
682
423
  CENTRAL_SOURCE_FILES.includes(file) &&
683
- (ts.isClassDeclaration(statement) ||
684
- ts.isFunctionDeclaration(statement) ||
685
- ts.isInterfaceDeclaration(statement) ||
686
- ts.isTypeAliasDeclaration(statement) ||
687
- ts.isVariableStatement(statement)) &&
688
- !hasExportModifier(statement)
689
- ) {
690
- violations.push(`${path} exports every centralized declaration`)
691
- }
692
- if (
693
- !placementExempt &&
694
- !functionModule &&
695
- ts.isFunctionDeclaration(statement) &&
696
- !FUNCTION_SOURCE_FILES.includes(file)
697
- ) {
698
- violations.push(`${path} places module functions in their centralized kind file`)
699
- }
700
- if (
701
- !placementExempt &&
702
- !functionModule &&
703
- ts.isVariableStatement(statement) &&
704
- !DATA_SOURCE_FILES.includes(file)
424
+ isPolicyDeclaration(statement) &&
425
+ !hasPolicyModifier(statement, ts.SyntaxKind.ExportKeyword)
705
426
  ) {
706
- violations.push(`${path} places module data in its centralized kind file`)
427
+ violations.push(
428
+ createPolicyViolation(
429
+ 'export',
430
+ path,
431
+ 'every centralized declaration is exported',
432
+ statement,
433
+ ),
434
+ )
707
435
  }
436
+
708
437
  if (
709
- ts.isClassDeclaration(statement) &&
710
- file !== 'errors.ts' &&
711
- !/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)
438
+ (ts.isEnumDeclaration(statement) ||
439
+ ts.isInterfaceDeclaration(statement) ||
440
+ ts.isModuleDeclaration(statement) ||
441
+ ts.isTypeAliasDeclaration(statement)) &&
442
+ file !== 'types.ts'
712
443
  ) {
713
- violations.push(`${path} places each class in its matching implementation or errors file`)
714
- }
715
- if (ts.isEnumDeclaration(statement) && file !== 'types.ts') {
716
- violations.push(`${path} centralizes enum contracts in types.ts`)
444
+ violations.push(
445
+ createPolicyViolation('type', path, 'type declarations sit in types.ts', statement),
446
+ )
717
447
  }
718
- if (file === 'constants.ts' && ts.isVariableStatement(statement)) {
719
- if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
720
- violations.push(`${path} constants.ts permits only const declarations`)
721
- }
722
- for (const declaration of statement.declarationList.declarations) {
723
- if (
724
- !ts.isIdentifier(declaration.name) ||
725
- !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)
726
- ) {
727
- violations.push(
728
- `${path}:${formatPolicyPosition(declaration)} requires UPPER_SNAKE_CASE constants`,
729
- )
730
- }
731
- if (
732
- declaration.initializer !== undefined &&
733
- (ts.isArrayLiteralExpression(declaration.initializer) ||
734
- ts.isObjectLiteralExpression(declaration.initializer))
735
- ) {
736
- violations.push(
737
- `${path}:${formatPolicyPosition(declaration)} freezes collection constants`,
738
- )
739
- }
448
+
449
+ if (ts.isFunctionDeclaration(statement)) {
450
+ if (!functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
451
+ violations.push(
452
+ createPolicyViolation(
453
+ 'function',
454
+ path,
455
+ 'module functions sit in a function-kind file',
456
+ statement,
457
+ ),
458
+ )
740
459
  }
460
+ violations.push(...inspectPolicyFunctionName(path, file, statement.name?.text, statement))
741
461
  }
742
- }
743
462
 
744
- if (functionModule) {
745
- violations.push(...inspectFunctionModule(path, source))
746
- }
747
-
748
- if (/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)) {
749
- const classes = source.statements.filter(ts.isClassDeclaration)
750
- const invalid = source.statements.filter(
751
- (statement) => !ts.isImportDeclaration(statement) && !ts.isClassDeclaration(statement),
752
- )
753
- if (
754
- classes.length !== 1 ||
755
- invalid.length !== 0 ||
756
- classes[0]?.name?.text !== file.slice(0, -3) ||
757
- classes[0] === undefined ||
758
- !hasExportModifier(classes[0])
759
- ) {
760
- violations.push(
761
- `${path} implementation modules contain imports and one matching exported class`,
762
- )
463
+ if (ts.isVariableStatement(statement)) {
464
+ violations.push(...inspectPolicyVariables(path, file, statement, functionDomain))
465
+ if (file === 'constants.ts') violations.push(...inspectPolicyConstants(path, statement))
763
466
  }
764
- for (const member of classes[0]?.members ?? []) {
765
- if (hasModifier(member, ts.SyntaxKind.PrivateKeyword)) {
766
- violations.push(`${path}:${formatPolicyPosition(member)} uses runtime # privacy`)
467
+
468
+ if (ts.isClassDeclaration(statement)) {
469
+ const expected = basename(file, '.ts')
470
+ if (
471
+ file !== 'errors.ts' &&
472
+ (!/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file) || statement.name?.text !== expected)
473
+ ) {
474
+ violations.push(
475
+ createPolicyViolation(
476
+ 'class',
477
+ path,
478
+ 'classes sit in their matching implementation or errors file',
479
+ statement,
480
+ ),
481
+ )
767
482
  }
768
483
  }
769
484
  }
770
485
 
771
- inspectCodingNode(path, source, violations, checker)
486
+ if (functionDomain) violations.push(...inspectFunctionDomain(path, syntax))
772
487
  return violations
773
488
  }
774
489
 
775
490
  /**
776
- * Inspect every production source under one workspace.
491
+ * Inspect an explicit source population through the same per-file route as the workspace sweep.
777
492
  *
778
- * @remarks
779
- * `vueScripts` is required whenever the workspace contains a `.vue` file: script
780
- * blocks cannot be read without it, and omitting it is reported as a violation
781
- * against each SFC rather than passing silently.
493
+ * @param sources - The TypeScript files to inspect.
494
+ * @returns Every syntactic placement violation in source order.
782
495
  */
783
- export function inspectCodingWorkspace(
784
- root: string,
785
- vueScripts?: VueScriptExtractorInterface,
786
- ): readonly string[] {
787
- const violations: string[] = []
788
- for (const path of globSync(CODING_SOURCE_GLOB, {
789
- cwd: root,
790
- })) {
791
- const content = readFileSync(join(root, path), 'utf8')
792
- violations.push(...inspectCodingSource(path, content, vueScripts))
793
- }
496
+ export function inspectPolicySources(sources: readonly PolicySource[]): readonly PolicyViolation[] {
497
+ const violations: PolicyViolation[] = []
498
+ for (const source of sources) violations.push(...inspectPolicySource(source))
794
499
  return violations
795
500
  }
796
501
 
797
502
  /**
798
- * Read one workspace's declared package name.
503
+ * Read the complete TypeScript source population beneath one workspace.
799
504
  *
800
- * @param root - The workspace root holding the package manifest
801
- * @returns The declared package name
505
+ * @param root - The workspace root to read.
506
+ * @returns Every TypeScript source under the src and app axes, sorted by path.
802
507
  */
803
- export function readPackageName(root: string): string {
804
- const manifest: unknown = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
805
- const name =
806
- typeof manifest === 'object' && manifest !== null && 'name' in manifest
807
- ? manifest.name
808
- : undefined
809
- if (typeof name !== 'string') throw new Error(`Package manifest at ${root} declares no name`)
810
- return name
508
+ export function readPolicySources(root: string): readonly PolicySource[] {
509
+ return globSync(POLICY_SOURCE_GLOB, { cwd: root })
510
+ .sort()
511
+ .map((path) => ({
512
+ path: normalizePolicyPath(path),
513
+ content: readFileSync(join(root, path), 'utf8'),
514
+ }))
811
515
  }
812
516
 
813
517
  /**
814
- * Derive the identifier prefixes fleet policy must not use from a package name.
518
+ * Derive the required source module for one mirrored module test.
815
519
  *
816
- * @param name - The declared package name, scoped or bare
817
- * @returns The short name's upper-snake and Pascal spellings, deduped
818
- *
819
- * @example
820
- * ```ts
821
- * derivePolicyTokens('@orkestrel/my-router') // ['MY_ROUTER', 'MyRouter']
822
- * ```
520
+ * @param path - The workspace-relative test path.
521
+ * @returns The required TypeScript source path, or `undefined` for a reserved scope test.
823
522
  */
824
- export function derivePolicyTokens(name: string): readonly string[] {
825
- const short = name.slice(name.lastIndexOf('/') + 1)
826
- const words = short.split(/[^A-Za-z0-9]+/u).filter((word) => word.length > 0)
827
- const upper = words.map((word) => word.toUpperCase()).join('_')
828
- const pascal = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('')
829
- return [...new Set([upper, pascal])].filter((token) => token.length > 0)
523
+ export function testToPolicySource(path: string): string | undefined {
524
+ const normalized = normalizePolicyPath(path)
525
+ if (basename(normalized) === 'integration.test.ts') return undefined
526
+ if (!normalized.startsWith('tests/') || !normalized.endsWith('.test.ts')) return undefined
527
+ return `${normalized.slice('tests/'.length, -'.test.ts'.length)}.ts`
830
528
  }
831
529
 
832
530
  /**
833
- * Add package-architecture violations while traversing one fleet policy source tree.
531
+ * Inspect mirrored test paths against an explicit source-path population.
834
532
  *
835
- * @remarks
836
- * An identifier carries a package's architecture when it is named for that package,
837
- * so it must begin with the token. A word that merely holds the token somewhere
838
- * inside it belongs to the fleet's own vocabulary and is left alone.
533
+ * @param tests - The module-test paths to inspect.
534
+ * @param sources - The existing TypeScript source paths.
535
+ * @returns Every missing mirror violation in test-path order.
839
536
  */
840
- export function inspectPolicyNode(
841
- path: string,
842
- node: ts.Node,
843
- violations: string[],
844
- tokens: readonly string[],
845
- ): void {
846
- if (ts.isIdentifier(node)) {
847
- const token = tokens.find((candidate) => node.text.startsWith(candidate))
848
- if (token !== undefined) {
849
- violations.push(`${path}:${formatPolicyPosition(node)} forbids the ${token} package token`)
537
+ export function inspectPolicyMirrorPaths(
538
+ tests: readonly string[],
539
+ sources: ReadonlySet<string>,
540
+ ): readonly PolicyViolation[] {
541
+ const violations: PolicyViolation[] = []
542
+ for (const test of tests) {
543
+ const path = normalizePolicyPath(test)
544
+ const source = testToPolicySource(path)
545
+ if (source !== undefined && !sources.has(source)) {
546
+ violations.push(
547
+ createPolicyViolation('mirror', path, `module test requires matching source ${source}`),
548
+ )
850
549
  }
851
550
  }
852
- if (
853
- (ts.isStringLiteral(node) || ts.isTemplateLiteralToken(node)) &&
854
- POLICY_ENVIRONMENT_PATTERN.test(node.text)
855
- ) {
856
- violations.push(
857
- `${path}:${formatPolicyPosition(node)} forbids a source-environment path literal`,
858
- )
859
- }
860
- ts.forEachChild(node, (child) => inspectPolicyNode(path, child, violations, tokens))
551
+ return violations
861
552
  }
862
553
 
863
554
  /**
864
- * Inspect one fleet policy module for a single package's architecture.
555
+ * Inspect every mirrored module test beneath one workspace.
865
556
  *
866
- * @param path - The workspace-relative source path used in diagnostics
867
- * @param content - The TypeScript source text to inspect
868
- * @param tokens - The package identifier tokens no identifier may begin with
869
- * @returns Every violation, in source-position order
557
+ * @param root - The workspace root to inspect.
558
+ * @returns Every missing mirror violation in test-path order.
870
559
  */
871
- export function inspectPolicyPurity(
872
- path: string,
873
- content: string,
874
- tokens: readonly string[],
875
- ): readonly string[] {
876
- const violations: string[] = []
877
- const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true)
878
- inspectPolicyNode(path, source, violations, tokens)
879
- return violations
560
+ export function inspectPolicyMirrors(root: string): readonly PolicyViolation[] {
561
+ const tests = globSync(POLICY_TEST_GLOB, { cwd: root }).sort().map(normalizePolicyPath)
562
+ const sources = new Set(
563
+ globSync('{app,src}/**/*.ts', { cwd: root }).sort().map(normalizePolicyPath),
564
+ )
565
+ return inspectPolicyMirrorPaths(tests, sources)
566
+ }
567
+
568
+ /**
569
+ * Inspect source placement and test mirrors across one workspace.
570
+ *
571
+ * @param root - The workspace root to inspect.
572
+ * @returns Every source-placement and mirror violation.
573
+ */
574
+ export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
575
+ return [...inspectPolicySources(readPolicySources(root)), ...inspectPolicyMirrors(root)]
880
576
  }
881
577
 
882
578
  /**
883
- * Inspect every fleet policy module under one workspace.
579
+ * Write a control to a real temporary workspace and run the production sweep over it.
884
580
  *
885
- * @param root - The workspace root whose manifest names the consuming package
886
- * @returns Every violation across the fleet policy files, in file and position order
581
+ * @param control - The physical fixture and expected rule boundary.
582
+ * @returns Every violation reported through the production workspace route.
887
583
  */
888
- export function inspectPolicyWorkspace(root: string): readonly string[] {
889
- const tokens = derivePolicyTokens(readPackageName(root))
890
- const violations: string[] = []
891
- for (const path of POLICY_INFRASTRUCTURE_FILES) {
892
- const content = readFileSync(join(root, path), 'utf8')
893
- violations.push(...inspectPolicyPurity(path, content, tokens))
584
+ export function inspectPolicyControl(control: PolicyControl): readonly PolicyViolation[] {
585
+ const root = mkdtempSync(join(tmpdir(), 'orkestrel-policy-'))
586
+ try {
587
+ for (const file of control.files) {
588
+ const path = join(root, ...normalizePolicyPath(file.path).split('/'))
589
+ mkdirSync(dirname(path), { recursive: true })
590
+ writeFileSync(path, file.content, 'utf8')
591
+ }
592
+ return inspectPolicyWorkspace(root)
593
+ } finally {
594
+ rmSync(root, { recursive: true, force: true })
894
595
  }
895
- return violations
896
596
  }
597
+
598
+ /** Physical negative controls, one for each rule the instrument claims to enforce. */
599
+ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
600
+ {
601
+ label: 'rejects a type outside types.ts',
602
+ membership: 'top-level type declarations whose filename is not types.ts',
603
+ rule: 'type',
604
+ files: [{ path: 'src/mobile/helpers.ts', content: 'export interface ValueInterface {}\n' }],
605
+ },
606
+ {
607
+ label: 'rejects an inline function in routes.ts',
608
+ membership: 'module-level function syntax whose filename is absent from the function register',
609
+ rule: 'function',
610
+ files: [
611
+ {
612
+ path: 'src/worker/routes.ts',
613
+ content: 'export const ROUTES = Object.freeze([{ handler: () => undefined }])\n',
614
+ },
615
+ ],
616
+ },
617
+ {
618
+ label: 'rejects data in handlers.ts',
619
+ membership: 'module data whose filename is absent from the data register',
620
+ rule: 'data',
621
+ files: [{ path: 'app/edge/handlers.ts', content: "export const STATUS = 'ready'\n" }],
622
+ },
623
+ {
624
+ label: 'rejects a hidden centralized declaration',
625
+ membership: 'centralized declarations without an export modifier',
626
+ rule: 'export',
627
+ files: [{ path: 'src/worker/helpers.ts', content: 'function buildValue(): void {}\n' }],
628
+ },
629
+ {
630
+ label: 'rejects a class that differs from its file',
631
+ membership: 'class declarations outside errors.ts whose names differ from their filename',
632
+ rule: 'class',
633
+ files: [{ path: 'app/desktop/Widget.ts', content: 'export class Other {}\n' }],
634
+ },
635
+ {
636
+ label: 'rejects mutable constants',
637
+ membership: 'variable statements in constants.ts that are not const',
638
+ rule: 'constant',
639
+ files: [{ path: 'src/worker/constants.ts', content: 'export let COUNT = 1\n' }],
640
+ },
641
+ {
642
+ label: 'rejects lower-case constants',
643
+ membership: 'declarations in constants.ts whose names are not UPPER_SNAKE_CASE',
644
+ rule: 'constant',
645
+ files: [{ path: 'src/worker/constants.ts', content: 'export const count = 1\n' }],
646
+ },
647
+ {
648
+ label: 'rejects bare collection constants',
649
+ membership: 'declarations in constants.ts with direct array or object literal initializers',
650
+ rule: 'constant',
651
+ files: [{ path: 'src/worker/constants.ts', content: 'export const VALUES = []\n' }],
652
+ },
653
+ {
654
+ label: 'rejects a parser without the parse prefix',
655
+ membership: 'function declarations in parsers.ts whose names do not start with parse',
656
+ rule: 'parser',
657
+ files: [{ path: 'app/edge/parsers.ts', content: 'export function coerceValue(): void {}\n' }],
658
+ },
659
+ {
660
+ label: 'rejects a factory without the create prefix',
661
+ membership: 'function declarations in factories.ts whose names do not start with create',
662
+ rule: 'factory',
663
+ files: [{ path: 'app/edge/factories.ts', content: 'export function buildValue(): void {}\n' }],
664
+ },
665
+ {
666
+ label: 'rejects a malformed registered function module',
667
+ membership: 'direct camelCase modules in a registered function-domain folder',
668
+ rule: 'domain',
669
+ files: [
670
+ {
671
+ path: 'app/browser/composables/useTheme.ts',
672
+ content: 'export function useMode(): void {}\n',
673
+ },
674
+ ],
675
+ },
676
+ {
677
+ label: 'rejects a file named for a function domain',
678
+ membership: 'source files whose stem is registered as a function-domain folder name',
679
+ rule: 'domain',
680
+ files: [{ path: 'app/edge/composables.ts', content: '' }],
681
+ },
682
+ {
683
+ label: 'rejects a function in an unregistered domain',
684
+ membership: 'function modules whose parent path is absent from the domain register',
685
+ rule: 'function',
686
+ files: [
687
+ { path: 'src/worker/jobs/runTask.ts', content: 'export function runTask(): void {}\n' },
688
+ ],
689
+ },
690
+ {
691
+ label: 'rejects an unmirrored module test',
692
+ membership: 'module tests below tests/src or tests/app except integration.test.ts',
693
+ rule: 'mirror',
694
+ files: [
695
+ {
696
+ path: 'tests/app/worker/jobs/probe.test.ts',
697
+ content: "import { it } from 'vitest'\nit('runs', () => {})\n",
698
+ },
699
+ ],
700
+ },
701
+ ])
702
+
703
+ /** A differently shaped workspace with app, browser, and worker environments but no core. */
704
+ export const GENERIC_POLICY_SOURCES: readonly PolicySource[] = Object.freeze([
705
+ {
706
+ path: 'src/worker/types.ts',
707
+ content: 'export interface TaskInterface { readonly id: string }\n',
708
+ },
709
+ { path: 'src/worker/Worker.ts', content: 'export class Worker {}\n' },
710
+ {
711
+ path: 'app/browser/composables/useTheme.ts',
712
+ content: 'export function useTheme(): void {}\n',
713
+ },
714
+ { path: 'app/browser/handlers.ts', content: 'export function open(): void {}\n' },
715
+ {
716
+ path: 'app/browser/routes.ts',
717
+ content:
718
+ "import { open } from './handlers.js'\nexport const ROUTES = Object.freeze([{ method: 'GET', path: '/', handler: open }])\n",
719
+ },
720
+ {
721
+ path: 'src/worker/constants.ts',
722
+ content: "export const LABELS = Object.freeze(['ready'])\n",
723
+ },
724
+ ])