@orkestrel/scaffold 0.0.22 → 0.0.24

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 +9 -9
  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 +557 -602
  23. package/dist/src/core/index.cjs +3569 -10510
  24. package/dist/src/core/index.cjs.map +1 -1
  25. package/dist/src/core/index.d.cts +2361 -2789
  26. package/dist/src/core/index.d.ts +2361 -2789
  27. package/dist/src/core/index.js +3513 -10374
  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 +1920 -1335
  32. package/dist/src/server/index.d.ts +1920 -1335
  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 -2886
  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,710 +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(',')}}`
114
-
115
- /** Virtual source text used while binding one policy-inspected module. */
116
- export const POLICY_SOURCE_TEXTS: Map<string, string> = new Map()
109
+ /** The complete TypeScript source population inspected under either workspace axis. */
110
+ export const POLICY_SOURCE_GLOB = `{app,src}/**/*.{${POLICY_SOURCE_EXTENSIONS.join(',')}}`
117
111
 
118
- /** One script block extracted from a Vue SFC by the official compiler. */
119
- export interface VueScriptBlockInterface {
120
- readonly content: string
121
- readonly lang?: string
122
- }
112
+ /** The mirrored module-test population inspected under either workspace axis. */
113
+ export const POLICY_TEST_GLOB = 'tests/{app,src}/**/*.test.ts'
123
114
 
124
- /** An injected official Vue SFC script-block extractor. */
125
- export interface VueScriptExtractorInterface {
126
- (path: string, content: string): readonly VueScriptBlockInterface[]
127
- }
128
-
129
- /** Normalize platform separators and duplicate glob segments for stable diagnostics. */
115
+ /**
116
+ * Normalize platform separators for stable matching and diagnostics.
117
+ *
118
+ * @param path - The workspace-relative path to normalize.
119
+ * @returns The path with forward slashes and no duplicate separators.
120
+ */
130
121
  export function normalizePolicyPath(path: string): string {
131
122
  return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
132
123
  }
133
124
 
134
- /** Whether a path belongs to the production-source coding-law corpus. */
135
- export function isCodingSourcePath(path: string): boolean {
136
- const normalized = normalizePolicyPath(path)
137
- const extension = normalized.split('.').pop()
138
- return (
139
- (normalized.startsWith('app/') || normalized.startsWith('src/')) &&
140
- extension !== undefined &&
141
- CODING_SOURCE_EXTENSIONS.includes(extension)
142
- )
143
- }
144
-
145
125
  /**
146
- * Whether a path is an eligible function-domain module.
126
+ * Whether a declaration carries a specified TypeScript modifier.
147
127
  *
148
- * @param path - The workspace-relative source path to inspect
149
- * @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.
150
131
  */
151
- export function isFunctionDomainPath(path: string): boolean {
152
- const normalized = normalizePolicyPath(path)
153
- const file = basename(normalized)
154
- const separator = normalized.lastIndexOf('/')
155
- const parent = separator < 0 ? '' : normalized.slice(0, separator)
156
- return (
157
- FUNCTION_DOMAIN_FOLDERS.includes(parent) &&
158
- /^[a-z][A-Za-z0-9]*\.ts$/u.test(file) &&
159
- file !== 'index.ts' &&
160
- file !== 'main.ts' &&
161
- !CENTRAL_SOURCE_FILES.includes(file) &&
162
- !FUNCTION_SOURCE_FILES.includes(file) &&
163
- !DATA_SOURCE_FILES.includes(file)
164
- )
165
- }
166
-
167
- /** Whether a declaration carries an explicit export modifier. */
168
- export function hasExportModifier(node: ts.Node): boolean {
169
- return (
170
- ts.canHaveModifiers(node) &&
171
- ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
172
- true
173
- )
174
- }
175
-
176
- /** Whether a declaration carries a specified modifier. */
177
- export function hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
132
+ export function hasPolicyModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
178
133
  return (
179
134
  ts.canHaveModifiers(node) &&
180
135
  ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
181
136
  )
182
137
  }
183
138
 
184
- /** Whether a module specifier uses a URL scheme other than Node's builtin namespace. */
185
- export function isUnsupportedModuleSpecifier(specifier: string): boolean {
186
- return /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(specifier) && !specifier.startsWith('node:')
187
- }
188
-
189
- /** Whether an arrow/function expression is an anonymous callback passed directly as an argument. */
190
- export function isDirectCallback(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
191
- const parent = node.parent
192
- return (
193
- (ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
194
- parent.arguments?.some((argument) => argument === node) === true
195
- )
196
- }
197
-
198
139
  /**
199
- * 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.
200
141
  *
201
- * @param node - The function expression to inspect
202
- * @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.
203
144
  */
204
- export function isDirectReturn(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
205
- const parent = node.parent
206
- if (ts.isReturnStatement(parent)) return true
207
- if (ts.isArrowFunction(parent) && parent.body === node) return true
208
- if (!ts.isParenthesizedExpression(parent)) return false
209
- const container = parent.parent
210
- return (
211
- ts.isReturnStatement(container) || (ts.isArrowFunction(container) && container.body === parent)
212
- )
213
- }
214
-
215
- /** Whether a function expression is assigned by one module-scope variable declaration. */
216
- export function isModuleFunction(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
217
- const declaration = node.parent
218
- const list = declaration.parent
219
- const statement = list.parent
220
- return (
221
- ts.isVariableDeclaration(declaration) &&
222
- ts.isVariableDeclarationList(list) &&
223
- ts.isVariableStatement(statement) &&
224
- ts.isSourceFile(statement.parent)
225
- )
145
+ export function getPolicyLine(node: ts.Node): number {
146
+ const position = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart())
147
+ return position.line + 1
226
148
  }
227
149
 
228
150
  /**
229
- * 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.
230
152
  *
231
- * @param node - The syntax node whose starting position to format
232
- * @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.
233
155
  */
234
- export function formatPolicyPosition(node: ts.Node): string {
235
- const source = node.getSourceFile()
236
- const position = source.getLineAndCharacterOfPosition(node.getStart())
237
- return `${String(position.line + 1)}:${String(position.character + 1)}`
238
- }
239
-
240
- /** Whether a property signature belongs to a centralized interface or type alias contract. */
241
- export function isContractProperty(node: ts.PropertySignature): boolean {
242
- let parent: ts.Node = node.parent
243
- while (ts.isTypeLiteralNode(parent)) parent = parent.parent
244
- return ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)
245
- }
246
-
247
- /** Whether the sole production triple-slash reference is the generated browser Vite contract. */
248
- 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)
249
159
  return (
250
- path.replaceAll('\\', '/') === 'app/browser/env.d.ts' &&
251
- source.referencedFiles.length === 0 &&
252
- source.libReferenceDirectives.length === 0 &&
253
- source.typeReferenceDirectives.length === 1 &&
254
- 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)
255
165
  )
256
166
  }
257
167
 
258
168
  /**
259
- * Whether a source is self-contained around a positively identified Node runtime dependency.
169
+ * Whether a variable initializer is directly a function expression.
260
170
  *
261
- * @param source - The parsed source file to inspect.
262
- * @returns `true` when at least one value import names a real `node:` builtin and no sibling,
263
- * re-exported, or dynamic runtime dependency is present; type-only imports are erased.
264
- *
265
- * @example
266
- * ```ts
267
- * const source = ts.createSourceFile(
268
- * 'serve.ts',
269
- * "import { parentPort } from 'node:worker_threads'",
270
- * ts.ScriptTarget.Latest,
271
- * true,
272
- * )
273
- * isSelfContained(source) // true
274
- * ```
171
+ * @param initializer - The initializer to inspect.
172
+ * @returns `true` for a direct arrow or function expression.
275
173
  */
276
- export function isSelfContained(source: ts.SourceFile): boolean {
277
- const pending: ts.Node[] = [source]
278
- while (pending.length > 0) {
279
- const node = pending.pop()
280
- if (node === undefined) continue
281
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
282
- return false
283
- }
284
- ts.forEachChild(node, (child) => {
285
- pending.push(child)
286
- })
287
- }
288
-
289
- let builtin = false
290
- for (const statement of source.statements) {
291
- if (ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
292
- return false
293
- }
294
- if (ts.isImportDeclaration(statement)) {
295
- const clause = statement.importClause
296
- const named = clause?.namedBindings
297
- const erased =
298
- clause?.phaseModifier === ts.SyntaxKind.TypeKeyword ||
299
- (clause !== undefined &&
300
- clause.name === undefined &&
301
- named !== undefined &&
302
- ts.isNamedImports(named) &&
303
- named.elements.length > 0 &&
304
- named.elements.every((element) => element.isTypeOnly))
305
- if (erased) continue
306
- const moduleSpecifier = statement.moduleSpecifier
307
- if (!ts.isStringLiteral(moduleSpecifier)) return false
308
- const specifier = moduleSpecifier.text
309
- if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
310
- builtin = true
311
- }
312
- if (ts.isImportEqualsDeclaration(statement)) {
313
- if (statement.isTypeOnly) continue
314
- const reference = statement.moduleReference
315
- if (
316
- !ts.isExternalModuleReference(reference) ||
317
- reference.expression === undefined ||
318
- !ts.isStringLiteral(reference.expression)
319
- ) {
320
- return false
321
- }
322
- const specifier = reference.expression.text
323
- if (!specifier.startsWith('node:') || !isBuiltin(specifier)) return false
324
- builtin = true
325
- }
174
+ export function isPolicyFunctionInitializer(initializer: ts.Expression | undefined): boolean {
175
+ let expression = initializer
176
+ while (expression !== undefined && ts.isParenthesizedExpression(expression)) {
177
+ expression = expression.expression
326
178
  }
327
- return builtin
179
+ return (
180
+ expression !== undefined &&
181
+ (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
182
+ )
328
183
  }
329
184
 
330
185
  /**
331
- * Whether the policy compiler can read one source path.
186
+ * Whether an expression contains module-level function syntax.
332
187
  *
333
- * @param path - The source path to inspect
334
- * @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.
335
190
  */
336
- export function hasPolicySource(path: string): boolean {
337
- 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
338
200
  }
339
201
 
340
202
  /**
341
- * Read one virtual or physical policy source.
203
+ * Create one stable violation for an inspection result.
342
204
  *
343
- * @param path - The source path to read
344
- * @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.
345
210
  */
346
- export function readPolicySource(path: string): string | undefined {
347
- 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
+ }
348
223
  }
349
224
 
350
225
  /**
351
- * Parse one virtual or physical policy source for the compiler host.
226
+ * Inspect a registered function module's exact declaration shape.
352
227
  *
353
- * @param path - The source path to parse
354
- * @param language - The requested TypeScript language target
355
- * @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.
356
231
  */
357
- export function createPolicySource(
232
+ export function inspectFunctionDomain(
358
233
  path: string,
359
- language: ts.ScriptTarget | ts.CreateSourceFileOptions,
360
- ): ts.SourceFile | undefined {
361
- const content = readPolicySource(path)
362
- 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
+ ]
363
261
  }
364
262
 
365
263
  /**
366
- * Bind one policy-inspected module without loading ambient host declarations.
264
+ * Inspect parser and factory function names without inferring their meaning.
367
265
  *
368
- * @param path - The source path used in diagnostics
369
- * @param content - The TypeScript source text to bind
370
- * @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.
371
271
  */
372
- export function createPolicyProgram(path: string, content: string): ts.Program {
373
- const options: ts.CompilerOptions = {
374
- allowJs: true,
375
- noLib: true,
376
- noResolve: true,
377
- target: ts.ScriptTarget.Latest,
378
- 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)]
379
283
  }
380
- POLICY_SOURCE_TEXTS.set(path, content)
381
- const host = ts.createCompilerHost(options)
382
- host.fileExists = hasPolicySource
383
- host.readFile = readPolicySource
384
- host.getSourceFile = createPolicySource
385
- const program = ts.createProgram([path], options, host)
386
- POLICY_SOURCE_TEXTS.delete(path)
387
- return program
284
+ return []
388
285
  }
389
286
 
390
287
  /**
391
- * Whether an identifier is a standalone runtime value reference.
288
+ * Inspect one variable statement for data and function placement.
392
289
  *
393
- * @param node - The identifier occurrence to classify
394
- * @param checker - The binder used to distinguish lexical values from ambient globals
395
- * @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.
396
295
  */
397
- export function isValueReferenceIdentifier(node: ts.Identifier, checker: ts.TypeChecker): boolean {
398
- if (ts.isPartOfTypeNode(node)) return false
399
-
400
- let ancestor: ts.Node = node.parent
401
- while (!ts.isSourceFile(ancestor) && !ts.isStatement(ancestor)) {
402
- if (ts.isTypeQueryNode(ancestor)) return false
403
- if (ts.isComputedPropertyName(ancestor) && ts.isTypeElement(ancestor.parent)) return false
404
- ancestor = ancestor.parent
405
- }
406
-
407
- const parent = node.parent
408
- if (
409
- (ts.isPropertyAccessExpression(parent) && parent.name === node) ||
410
- (ts.isPropertyAssignment(parent) && parent.name === node) ||
411
- (ts.isPropertyDeclaration(parent) && parent.name === node) ||
412
- (ts.isPropertySignature(parent) && parent.name === node) ||
413
- (ts.isMethodDeclaration(parent) && parent.name === node) ||
414
- (ts.isMethodSignature(parent) && parent.name === node) ||
415
- (ts.isGetAccessorDeclaration(parent) && parent.name === node) ||
416
- (ts.isSetAccessorDeclaration(parent) && parent.name === node) ||
417
- (ts.isVariableDeclaration(parent) && parent.name === node) ||
418
- (ts.isParameter(parent) && parent.name === node) ||
419
- (ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
420
- (ts.isFunctionDeclaration(parent) && parent.name === node) ||
421
- (ts.isFunctionExpression(parent) && parent.name === node) ||
422
- (ts.isClassDeclaration(parent) && parent.name === node) ||
423
- (ts.isClassExpression(parent) && parent.name === node) ||
424
- (ts.isInterfaceDeclaration(parent) && parent.name === node) ||
425
- (ts.isTypeAliasDeclaration(parent) && parent.name === node) ||
426
- (ts.isTypeParameterDeclaration(parent) && parent.name === node) ||
427
- (ts.isEnumDeclaration(parent) && parent.name === node) ||
428
- (ts.isEnumMember(parent) && parent.name === node) ||
429
- (ts.isModuleDeclaration(parent) && parent.name === node) ||
430
- ts.isImportClause(parent) ||
431
- ts.isImportSpecifier(parent) ||
432
- ts.isNamespaceImport(parent) ||
433
- ts.isImportEqualsDeclaration(parent) ||
434
- ts.isExportSpecifier(parent) ||
435
- ts.isNamespaceExport(parent) ||
436
- ts.isNamespaceExportDeclaration(parent) ||
437
- (ts.isLabeledStatement(parent) && parent.label === node) ||
438
- (ts.isBreakOrContinueStatement(parent) && parent.label === node) ||
439
- (ts.isJsxAttribute(parent) && parent.name === node)
440
- ) {
441
- return false
442
- }
443
- return (
444
- (ts.isShorthandPropertyAssignment(parent)
445
- ? checker.getShorthandAssignmentValueSymbol(parent)
446
- : checker.getSymbolAtLocation(node)) === undefined
447
- )
448
- }
449
-
450
- /** Inspect a Vue single-file component for syntax that can bypass declared import policy. */
451
- export function inspectVueCodingLaw(
296
+ export function inspectPolicyVariables(
452
297
  path: string,
453
- scripts: readonly VueScriptBlockInterface[] = [],
454
- ): readonly string[] {
455
- const violations: string[] = []
456
- for (const [index, script] of scripts.entries()) {
457
- if (
458
- script.lang !== 'ts' &&
459
- script.lang !== 'tsx' &&
460
- script.lang !== 'mts' &&
461
- script.lang !== 'cts'
462
- ) {
463
- 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))
464
323
  }
465
- const extension =
466
- script.lang === 'tsx' || script.lang === 'mts' || script.lang === 'cts' ? script.lang : 'ts'
467
- violations.push(
468
- ...inspectCodingLaw(`${path}.script-${String(index)}.${extension}`, script.content),
469
- )
470
- }
471
- return violations
472
- }
473
-
474
- /** Inspect one production source through the shared coding-law route. */
475
- export function inspectCodingSource(
476
- path: string,
477
- content: string,
478
- vueScripts?: VueScriptExtractorInterface,
479
- ): readonly string[] {
480
- const normalizedPath = normalizePolicyPath(path)
481
- if (!normalizedPath.endsWith('.vue')) return inspectCodingLaw(normalizedPath, content)
482
- const violations: string[] = []
483
- if (!normalizedPath.startsWith('app/browser/')) {
484
- violations.push(`${normalizedPath} Vue components belong in app/browser`)
485
- }
486
- // A missing extractor is reported, never absorbed. Returning [] here would make
487
- // every SFC's script block silently unchecked while the surrounding sweep still
488
- // reported success — an instrument claiming a coverage it does not have.
489
- if (vueScripts === undefined) {
490
- violations.push(
491
- `${normalizedPath} requires a Vue script extractor; its script blocks were not inspected`,
492
- )
493
- return violations
494
324
  }
495
- violations.push(...inspectVueCodingLaw(normalizedPath, vueScripts(normalizedPath, content)))
496
325
  return violations
497
326
  }
498
327
 
499
- /** Add syntax-wide coding-law violations while traversing one source tree. */
500
- 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(
501
336
  path: string,
502
- node: ts.Node,
503
- violations: string[],
504
- checker: ts.TypeChecker,
505
- ): void {
506
- if (
507
- /^(?:app|src)[\\/]core[\\/]/u.test(path) &&
508
- ts.isIdentifier(node) &&
509
- WORKER_SCOPE_VALUE_GLOBALS.includes(node.text) &&
510
- isValueReferenceIdentifier(node, checker)
511
- ) {
512
- violations.push(
513
- `${path}:${formatPolicyPosition(node)} forbids worker-scope global ${node.text} in core`,
514
- )
515
- }
516
- if (
517
- ts.isAsExpression(node) ||
518
- ts.isTypeAssertionExpression(node) ||
519
- ts.isNonNullExpression(node)
520
- ) {
521
- violations.push(`${path}:${formatPolicyPosition(node)} forbids type/non-null assertions`)
522
- }
523
- if (node.kind === ts.SyntaxKind.AnyKeyword) {
524
- violations.push(`${path}:${formatPolicyPosition(node)} forbids any`)
525
- }
526
- if (
527
- (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
528
- node.moduleSpecifier !== undefined &&
529
- ts.isStringLiteral(node.moduleSpecifier) &&
530
- isUnsupportedModuleSpecifier(node.moduleSpecifier.text)
531
- ) {
532
- violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
533
- }
534
- if (
535
- ts.isPropertySignature(node) &&
536
- isContractProperty(node) &&
537
- !hasModifier(node, ts.SyntaxKind.ReadonlyKeyword)
538
- ) {
539
- violations.push(`${path}:${formatPolicyPosition(node)} requires readonly contract properties`)
540
- }
541
- if (
542
- ts.isCallExpression(node) &&
543
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
544
- (node.arguments.length !== 1 || !node.arguments.every(ts.isStringLiteral))
545
- ) {
337
+ statement: ts.VariableStatement,
338
+ ): readonly PolicyViolation[] {
339
+ const violations: PolicyViolation[] = []
340
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
546
341
  violations.push(
547
- `${path}:${formatPolicyPosition(node)} requires dynamic imports to use string literals so import policy remains enforceable`,
342
+ createPolicyViolation(
343
+ 'constant',
344
+ path,
345
+ 'constants.ts permits only const declarations',
346
+ statement,
347
+ ),
548
348
  )
549
349
  }
550
- if (
551
- ts.isCallExpression(node) &&
552
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
553
- node.arguments.length === 1 &&
554
- node.arguments.every(ts.isStringLiteral) &&
555
- isUnsupportedModuleSpecifier(node.arguments[0]?.text ?? '')
556
- ) {
557
- violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
558
- }
559
- if (ts.isFunctionDeclaration(node) && !ts.isSourceFile(node.parent)) {
560
- violations.push(`${path}:${formatPolicyPosition(node)} forbids nested function declarations`)
561
- }
562
- if (
563
- (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
564
- !isDirectCallback(node) &&
565
- !isDirectReturn(node) &&
566
- !isModuleFunction(node)
567
- ) {
568
- 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
+ }
569
375
  }
570
- ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations, checker))
376
+ return violations
571
377
  }
572
378
 
573
379
  /**
574
- * Inspect one eligible function-domain module for its required declaration shape.
380
+ * Whether a top-level statement declares a centralized symbol.
575
381
  *
576
- * @param path - The source path used in diagnostics
577
- * @param source - The parsed source file to inspect
578
- * @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.
579
384
  */
580
- export function inspectFunctionModule(path: string, source: ts.SourceFile): readonly string[] {
581
- const file = basename(normalizePolicyPath(path))
582
- const declarations = source.statements.filter(ts.isFunctionDeclaration)
583
- const functions = declarations.filter((declaration) => declaration.body !== undefined)
584
- const invalid = source.statements.filter(
585
- (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)
586
394
  )
587
- const declaration = functions[0]
588
- if (
589
- functions.length === 1 &&
590
- invalid.length === 0 &&
591
- declarations.every((candidate) => candidate.name?.text === file.slice(0, -3)) &&
592
- declaration !== undefined &&
593
- hasExportModifier(declaration) &&
594
- !hasModifier(declaration, ts.SyntaxKind.DefaultKeyword)
595
- ) {
596
- return []
597
- }
598
- return [`${path} declarations do not form one matching exported function implementation`]
599
395
  }
600
396
 
601
- /** Inspect one TypeScript source module for repository coding-law violations. */
602
- export function inspectCodingLaw(path: string, content: string): readonly string[] {
603
- const violations: string[] = []
604
- const program = createPolicyProgram(path, content)
605
- const source = program.getSourceFile(path)
606
- if (source === undefined) throw new Error(`Policy source was not bound at ${path}`)
607
- 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)
608
405
  const file = basename(path)
609
- const stem = basename(file, extname(file))
610
- const functionModule = isFunctionDomainPath(path)
611
- const placementExempt =
612
- !CENTRAL_SOURCE_FILES.includes(file) &&
613
- !FUNCTION_SOURCE_FILES.includes(file) &&
614
- !DATA_SOURCE_FILES.includes(file) &&
615
- isSelfContained(source)
616
-
617
- if (/\.[cm]?jsx?$/u.test(path)) {
618
- violations.push(`${path} production modules use TypeScript source extensions`)
619
- }
620
- if (FUNCTION_DOMAIN_FOLDERS.some((folder) => basename(folder) === stem)) {
621
- violations.push(`${path} names a function domain, which belongs in a folder rather than a file`)
622
- }
623
- if (/@ts-(?:expect-error|ignore|nocheck)|eslint-disable|oxlint-disable/u.test(content)) {
624
- violations.push(`${path} forbids suppression directives`)
625
- }
626
- if (
627
- (source.referencedFiles.length > 0 ||
628
- source.libReferenceDirectives.length > 0 ||
629
- source.typeReferenceDirectives.length > 0) &&
630
- !hasAllowedTripleSlashReference(path, source)
631
- ) {
632
- violations.push(`${path} forbids triple-slash references outside app/browser/env.d.ts`)
633
- }
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))
634
410
 
635
- if (file === 'index.ts') {
636
- for (const statement of source.statements) {
637
- if (
638
- !ts.isExportDeclaration(statement) ||
639
- statement.exportClause !== undefined ||
640
- statement.isTypeOnly ||
641
- statement.moduleSpecifier === undefined
642
- ) {
643
- violations.push(`${path} barrels contain only export * declarations`)
644
- }
645
- }
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
+ )
646
419
  }
647
420
 
648
- for (const statement of source.statements) {
649
- if (
650
- (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
651
- file !== 'types.ts'
652
- ) {
653
- violations.push(`${path} centralizes interfaces and type aliases in types.ts`)
654
- }
421
+ for (const statement of syntax.statements) {
655
422
  if (
656
423
  CENTRAL_SOURCE_FILES.includes(file) &&
657
- (ts.isClassDeclaration(statement) ||
658
- ts.isFunctionDeclaration(statement) ||
659
- ts.isInterfaceDeclaration(statement) ||
660
- ts.isTypeAliasDeclaration(statement) ||
661
- ts.isVariableStatement(statement)) &&
662
- !hasExportModifier(statement)
663
- ) {
664
- violations.push(`${path} exports every centralized declaration`)
665
- }
666
- if (
667
- !placementExempt &&
668
- !functionModule &&
669
- ts.isFunctionDeclaration(statement) &&
670
- !FUNCTION_SOURCE_FILES.includes(file)
424
+ isPolicyDeclaration(statement) &&
425
+ !hasPolicyModifier(statement, ts.SyntaxKind.ExportKeyword)
671
426
  ) {
672
- violations.push(`${path} places module functions in their centralized kind file`)
427
+ violations.push(
428
+ createPolicyViolation(
429
+ 'export',
430
+ path,
431
+ 'every centralized declaration is exported',
432
+ statement,
433
+ ),
434
+ )
673
435
  }
436
+
674
437
  if (
675
- !placementExempt &&
676
- !functionModule &&
677
- ts.isVariableStatement(statement) &&
678
- !DATA_SOURCE_FILES.includes(file)
438
+ (ts.isEnumDeclaration(statement) ||
439
+ ts.isInterfaceDeclaration(statement) ||
440
+ ts.isModuleDeclaration(statement) ||
441
+ ts.isTypeAliasDeclaration(statement)) &&
442
+ file !== 'types.ts'
679
443
  ) {
680
- violations.push(`${path} places module data in its centralized kind file`)
444
+ violations.push(
445
+ createPolicyViolation('type', path, 'type declarations sit in types.ts', statement),
446
+ )
681
447
  }
682
- if (
683
- ts.isClassDeclaration(statement) &&
684
- file !== 'errors.ts' &&
685
- !/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)
686
- ) {
687
- violations.push(`${path} places each class in its matching implementation or errors file`)
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
+ )
459
+ }
460
+ violations.push(...inspectPolicyFunctionName(path, file, statement.name?.text, statement))
688
461
  }
689
- if (ts.isEnumDeclaration(statement) && file !== 'types.ts') {
690
- violations.push(`${path} centralizes enum contracts in types.ts`)
462
+
463
+ if (ts.isVariableStatement(statement)) {
464
+ violations.push(...inspectPolicyVariables(path, file, statement, functionDomain))
465
+ if (file === 'constants.ts') violations.push(...inspectPolicyConstants(path, statement))
691
466
  }
692
- if (file === 'constants.ts' && ts.isVariableStatement(statement)) {
693
- if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
694
- violations.push(`${path} constants.ts permits only const declarations`)
695
- }
696
- for (const declaration of statement.declarationList.declarations) {
697
- if (
698
- !ts.isIdentifier(declaration.name) ||
699
- !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)
700
- ) {
701
- violations.push(
702
- `${path}:${formatPolicyPosition(declaration)} requires UPPER_SNAKE_CASE constants`,
703
- )
704
- }
705
- if (
706
- declaration.initializer !== undefined &&
707
- (ts.isArrayLiteralExpression(declaration.initializer) ||
708
- ts.isObjectLiteralExpression(declaration.initializer))
709
- ) {
710
- violations.push(
711
- `${path}:${formatPolicyPosition(declaration)} freezes collection constants`,
712
- )
713
- }
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
+ )
714
482
  }
715
483
  }
716
484
  }
717
485
 
718
- if (functionModule) {
719
- violations.push(...inspectFunctionModule(path, source))
720
- }
486
+ if (functionDomain) violations.push(...inspectFunctionDomain(path, syntax))
487
+ return violations
488
+ }
721
489
 
722
- if (/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)) {
723
- const classes = source.statements.filter(ts.isClassDeclaration)
724
- const invalid = source.statements.filter(
725
- (statement) => !ts.isImportDeclaration(statement) && !ts.isClassDeclaration(statement),
726
- )
727
- if (
728
- classes.length !== 1 ||
729
- invalid.length !== 0 ||
730
- classes[0]?.name?.text !== file.slice(0, -3) ||
731
- classes[0] === undefined ||
732
- !hasExportModifier(classes[0])
733
- ) {
490
+ /**
491
+ * Inspect an explicit source population through the same per-file route as the workspace sweep.
492
+ *
493
+ * @param sources - The TypeScript files to inspect.
494
+ * @returns Every syntactic placement violation in source order.
495
+ */
496
+ export function inspectPolicySources(sources: readonly PolicySource[]): readonly PolicyViolation[] {
497
+ const violations: PolicyViolation[] = []
498
+ for (const source of sources) violations.push(...inspectPolicySource(source))
499
+ return violations
500
+ }
501
+
502
+ /**
503
+ * Read the complete TypeScript source population beneath one workspace.
504
+ *
505
+ * @param root - The workspace root to read.
506
+ * @returns Every TypeScript source under the src and app axes, sorted by path.
507
+ */
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
+ }))
515
+ }
516
+
517
+ /**
518
+ * Derive the required source module for one mirrored module test.
519
+ *
520
+ * @param path - The workspace-relative test path.
521
+ * @returns The required TypeScript source path, or `undefined` for a reserved scope test.
522
+ */
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`
528
+ }
529
+
530
+ /**
531
+ * Inspect mirrored test paths against an explicit source-path population.
532
+ *
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.
536
+ */
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)) {
734
546
  violations.push(
735
- `${path} implementation modules contain imports and one matching exported class`,
547
+ createPolicyViolation('mirror', path, `module test requires matching source ${source}`),
736
548
  )
737
549
  }
738
- for (const member of classes[0]?.members ?? []) {
739
- if (hasModifier(member, ts.SyntaxKind.PrivateKeyword)) {
740
- violations.push(`${path}:${formatPolicyPosition(member)} uses runtime # privacy`)
741
- }
742
- }
743
550
  }
744
-
745
- inspectCodingNode(path, source, violations, checker)
746
551
  return violations
747
552
  }
748
553
 
749
554
  /**
750
- * Inspect every production source under one workspace.
555
+ * Inspect every mirrored module test beneath one workspace.
556
+ *
557
+ * @param root - The workspace root to inspect.
558
+ * @returns Every missing mirror violation in test-path order.
559
+ */
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)]
576
+ }
577
+
578
+ /**
579
+ * Write a control to a real temporary workspace and run the production sweep over it.
751
580
  *
752
- * @remarks
753
- * `vueScripts` is required whenever the workspace contains a `.vue` file: script
754
- * blocks cannot be read without it, and omitting it is reported as a violation
755
- * against each SFC rather than passing silently.
581
+ * @param control - The physical fixture and expected rule boundary.
582
+ * @returns Every violation reported through the production workspace route.
756
583
  */
757
- export function inspectCodingWorkspace(
758
- root: string,
759
- vueScripts?: VueScriptExtractorInterface,
760
- ): readonly string[] {
761
- const violations: string[] = []
762
- for (const path of globSync(CODING_SOURCE_GLOB, {
763
- cwd: root,
764
- })) {
765
- const content = readFileSync(join(root, path), 'utf8')
766
- violations.push(...inspectCodingSource(path, content, vueScripts))
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 })
767
595
  }
768
- return violations
769
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
+ ])