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