@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.
- package/README.md +84 -99
- package/dist/bin/main.js +1094 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/host/CLAUDE.md +3 -1
- package/dist/host/agents/orchestration.md +61 -4
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +7 -5
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +9 -9
- package/dist/host/claude/rules/architecture.md +45 -3
- package/dist/host/claude/rules/quality.md +4 -0
- package/dist/host/claude/rules/tests.md +57 -1
- package/dist/host/claude/rules/workspace.md +50 -17
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/configs/helpers.ts +762 -0
- package/dist/host/dotfiles/oxlintrc.json +2 -1
- package/dist/host/guides/scaffold.md +862 -0
- package/dist/host/manifest.json +40 -33
- package/dist/host/tests/config.test.ts +544 -0
- package/dist/host/tests/policy.test.ts +46 -0
- package/dist/host/tests/setupPolicy.ts +557 -602
- package/dist/src/core/index.cjs +3569 -10510
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +2361 -2789
- package/dist/src/core/index.d.ts +2361 -2789
- package/dist/src/core/index.js +3513 -10374
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2855 -3765
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1920 -1335
- package/dist/src/server/index.d.ts +1920 -1335
- package/dist/src/server/index.js +2812 -3680
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -23
- package/dist/bin/scaffold.js +0 -1896
- package/dist/bin/scaffold.js.map +0 -1
- package/dist/host/guides/src/scaffold.md +0 -2886
- /package/dist/host/guides/{src/guide.md → guide.md} +0 -0
|
@@ -1,19 +1,55 @@
|
|
|
1
|
-
import { globSync, readFileSync } from 'node:fs'
|
|
2
|
-
import {
|
|
3
|
-
import { basename,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
67
|
-
export const
|
|
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
|
-
/**
|
|
113
|
-
export const
|
|
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
|
-
/**
|
|
119
|
-
export
|
|
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
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
126
|
+
* Whether a declaration carries a specified TypeScript modifier.
|
|
147
127
|
*
|
|
148
|
-
* @param
|
|
149
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
140
|
+
* Return the one-based line where a syntax node begins.
|
|
200
141
|
*
|
|
201
|
-
* @param node - The
|
|
202
|
-
* @returns
|
|
142
|
+
* @param node - The syntax node to locate.
|
|
143
|
+
* @returns Its one-based source line.
|
|
203
144
|
*/
|
|
204
|
-
export function
|
|
205
|
-
const
|
|
206
|
-
|
|
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
|
-
*
|
|
151
|
+
* Whether a path is a direct module of a fleet-registered function domain.
|
|
230
152
|
*
|
|
231
|
-
* @param
|
|
232
|
-
* @returns
|
|
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
|
|
235
|
-
const
|
|
236
|
-
const
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
169
|
+
* Whether a variable initializer is directly a function expression.
|
|
260
170
|
*
|
|
261
|
-
* @param
|
|
262
|
-
* @returns `true`
|
|
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
|
|
277
|
-
|
|
278
|
-
while (
|
|
279
|
-
|
|
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
|
|
179
|
+
return (
|
|
180
|
+
expression !== undefined &&
|
|
181
|
+
(ts.isArrowFunction(expression) || ts.isFunctionExpression(expression))
|
|
182
|
+
)
|
|
328
183
|
}
|
|
329
184
|
|
|
330
185
|
/**
|
|
331
|
-
* Whether
|
|
186
|
+
* Whether an expression contains module-level function syntax.
|
|
332
187
|
*
|
|
333
|
-
* @param
|
|
334
|
-
* @returns `true` when the
|
|
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
|
|
337
|
-
|
|
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
|
-
*
|
|
203
|
+
* Create one stable violation for an inspection result.
|
|
342
204
|
*
|
|
343
|
-
* @param
|
|
344
|
-
* @
|
|
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
|
|
347
|
-
|
|
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
|
-
*
|
|
226
|
+
* Inspect a registered function module's exact declaration shape.
|
|
352
227
|
*
|
|
353
|
-
* @param path - The source path
|
|
354
|
-
* @param
|
|
355
|
-
* @returns
|
|
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
|
|
232
|
+
export function inspectFunctionDomain(
|
|
358
233
|
path: string,
|
|
359
|
-
|
|
360
|
-
):
|
|
361
|
-
const
|
|
362
|
-
|
|
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
|
-
*
|
|
264
|
+
* Inspect parser and factory function names without inferring their meaning.
|
|
367
265
|
*
|
|
368
|
-
* @param path - The source path
|
|
369
|
-
* @param
|
|
370
|
-
* @
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
288
|
+
* Inspect one variable statement for data and function placement.
|
|
392
289
|
*
|
|
393
|
-
* @param
|
|
394
|
-
* @param
|
|
395
|
-
* @
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
/**
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
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
|
-
|
|
342
|
+
createPolicyViolation(
|
|
343
|
+
'constant',
|
|
344
|
+
path,
|
|
345
|
+
'constants.ts permits only const declarations',
|
|
346
|
+
statement,
|
|
347
|
+
),
|
|
548
348
|
)
|
|
549
349
|
}
|
|
550
|
-
|
|
551
|
-
ts.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
376
|
+
return violations
|
|
571
377
|
}
|
|
572
378
|
|
|
573
379
|
/**
|
|
574
|
-
*
|
|
380
|
+
* Whether a top-level statement declares a centralized symbol.
|
|
575
381
|
*
|
|
576
|
-
* @param
|
|
577
|
-
* @
|
|
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
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
/**
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
|
|
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
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
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
|
-
(
|
|
658
|
-
|
|
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(
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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(
|
|
444
|
+
violations.push(
|
|
445
|
+
createPolicyViolation('type', path, 'type declarations sit in types.ts', statement),
|
|
446
|
+
)
|
|
681
447
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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 (
|
|
719
|
-
|
|
720
|
-
|
|
486
|
+
if (functionDomain) violations.push(...inspectFunctionDomain(path, syntax))
|
|
487
|
+
return violations
|
|
488
|
+
}
|
|
721
489
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
* @
|
|
753
|
-
*
|
|
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
|
|
758
|
-
root
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
])
|