@orkestrel/scaffold 0.0.2 → 0.0.4
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 +29 -15
- package/dist/bin/scaffold.js +1162 -1200
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +124 -925
- package/dist/host/CLAUDE.md +335 -495
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
- package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
- package/dist/host/claude/agents/application.md +30 -0
- package/dist/host/claude/agents/builder.md +10 -7
- package/dist/host/claude/agents/checker.md +14 -8
- package/dist/host/claude/agents/codex.md +94 -0
- package/dist/host/claude/agents/grok.md +37 -34
- package/dist/host/claude/agents/implementer.md +40 -0
- package/dist/host/claude/agents/orkestrel.md +88 -223
- package/dist/host/claude/agents/planner.md +13 -34
- package/dist/host/claude/agents/reviewer.md +38 -24
- package/dist/host/claude/agents/verifier.md +14 -10
- package/dist/host/claude/rules/application.md +57 -0
- package/dist/host/claude/rules/architecture.md +180 -0
- package/dist/host/claude/rules/browser.md +28 -0
- package/dist/host/claude/rules/documentation.md +64 -0
- package/dist/host/claude/rules/names.md +209 -0
- package/dist/host/claude/rules/patterns.md +130 -0
- package/dist/host/claude/rules/quality.md +45 -0
- package/dist/host/claude/rules/styles.md +64 -0
- package/dist/host/claude/rules/tests.md +123 -0
- package/dist/host/claude/rules/typescript.md +78 -0
- package/dist/host/claude/rules/workspace.md +180 -0
- package/dist/host/claude/settings.json +112 -0
- package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
- package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +15 -0
- package/dist/host/codex/agents/application.toml +25 -0
- package/dist/host/codex/agents/builder.toml +24 -0
- package/dist/host/codex/agents/checker.toml +15 -0
- package/dist/host/codex/agents/grok.toml +17 -0
- package/dist/host/codex/agents/implementer.toml +17 -0
- package/dist/host/codex/agents/opus.toml +25 -0
- package/dist/host/codex/agents/orkestrel.toml +16 -0
- package/dist/host/codex/agents/planner.toml +20 -0
- package/dist/host/codex/agents/reviewer.toml +18 -0
- package/dist/host/codex/agents/verifier.toml +14 -0
- package/dist/host/codex/config.toml +66 -0
- package/dist/host/cursor/mcp.json +12 -0
- package/dist/host/dotfiles/mcp.json +8 -0
- package/dist/host/dotfiles/oxlintrc.json +307 -1
- package/dist/host/guides/src/scaffold.md +2059 -2007
- package/dist/host/manifest.json +376 -137
- package/dist/host/scripts/codex.sh +49 -0
- package/dist/host/scripts/cursor.sh +33 -47
- package/dist/host/scripts/deps.sh +34 -16
- package/dist/host/scripts/ollama.sh +6 -149
- package/dist/host/tests/setupPolicy.ts +390 -0
- package/dist/src/core/index.cjs +6603 -1200
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +802 -267
- package/dist/src/core/index.d.ts +802 -267
- package/dist/src/core/index.js +6521 -1192
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3132 -593
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +643 -95
- package/dist/src/server/index.d.ts +643 -95
- package/dist/src/server/index.js +3054 -594
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -12
- package/dist/host/claude/agents/composer.md +0 -64
- package/dist/host/claude/agents/researcher.md +0 -38
- package/dist/host/claude/agents/scout.md +0 -35
- package/dist/host/github/workflows/ci.yml +0 -64
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { globSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import * as ts from 'typescript'
|
|
4
|
+
|
|
5
|
+
/** Centralized source modules whose top-level declarations must all be exported. */
|
|
6
|
+
export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
7
|
+
'combinators.ts',
|
|
8
|
+
'cloners.ts',
|
|
9
|
+
'compilers.ts',
|
|
10
|
+
'constants.ts',
|
|
11
|
+
'contracts.ts',
|
|
12
|
+
'errors.ts',
|
|
13
|
+
'factories.ts',
|
|
14
|
+
'helpers.ts',
|
|
15
|
+
'handlers.ts',
|
|
16
|
+
'inferers.ts',
|
|
17
|
+
'middlewares.ts',
|
|
18
|
+
'parsers.ts',
|
|
19
|
+
'relations.ts',
|
|
20
|
+
'schemas.ts',
|
|
21
|
+
'seeders.ts',
|
|
22
|
+
'shapers.ts',
|
|
23
|
+
'templates.ts',
|
|
24
|
+
'types.ts',
|
|
25
|
+
'validators.ts',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
/** Centralized files that own module-scope function declarations. */
|
|
29
|
+
export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
30
|
+
'combinators.ts',
|
|
31
|
+
'cloners.ts',
|
|
32
|
+
'compilers.ts',
|
|
33
|
+
'errors.ts',
|
|
34
|
+
'factories.ts',
|
|
35
|
+
'handlers.ts',
|
|
36
|
+
'helpers.ts',
|
|
37
|
+
'inferers.ts',
|
|
38
|
+
'middlewares.ts',
|
|
39
|
+
'parsers.ts',
|
|
40
|
+
'relations.ts',
|
|
41
|
+
'schemas.ts',
|
|
42
|
+
'seeders.ts',
|
|
43
|
+
'shapers.ts',
|
|
44
|
+
'validators.ts',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
/** Centralized files that own module-scope data declarations. */
|
|
48
|
+
export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
49
|
+
'combinators.ts',
|
|
50
|
+
'constants.ts',
|
|
51
|
+
'contracts.ts',
|
|
52
|
+
'relations.ts',
|
|
53
|
+
'schemas.ts',
|
|
54
|
+
'templates.ts',
|
|
55
|
+
'validators.ts',
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** One script block extracted from a Vue SFC by the official compiler. */
|
|
59
|
+
export interface VueScriptBlockInterface {
|
|
60
|
+
readonly content: string
|
|
61
|
+
readonly lang?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** An injected official Vue SFC script-block extractor. */
|
|
65
|
+
export interface VueScriptExtractorInterface {
|
|
66
|
+
(path: string, content: string): readonly VueScriptBlockInterface[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Normalize platform separators and duplicate glob segments for stable diagnostics. */
|
|
70
|
+
export function normalizePolicyPath(path: string): string {
|
|
71
|
+
return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether a declaration carries an explicit export modifier. */
|
|
75
|
+
export function hasExportModifier(node: ts.Node): boolean {
|
|
76
|
+
return (
|
|
77
|
+
ts.canHaveModifiers(node) &&
|
|
78
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
|
|
79
|
+
true
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether a declaration carries a specified modifier. */
|
|
84
|
+
export function hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
|
|
85
|
+
return (
|
|
86
|
+
ts.canHaveModifiers(node) &&
|
|
87
|
+
ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether a module specifier uses a URL scheme other than Node's builtin namespace. */
|
|
92
|
+
export function isUnsupportedModuleSpecifier(specifier: string): boolean {
|
|
93
|
+
return /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(specifier) && !specifier.startsWith('node:')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Whether an arrow/function expression is an anonymous callback passed directly as an argument. */
|
|
97
|
+
export function isDirectCallback(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
|
|
98
|
+
const parent = node.parent
|
|
99
|
+
return (
|
|
100
|
+
(ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
|
|
101
|
+
parent.arguments?.some((argument) => argument === node) === true
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether an arrow/function expression is returned directly as a factory or combinator result.
|
|
107
|
+
*
|
|
108
|
+
* @param node - The function expression to inspect
|
|
109
|
+
* @returns `true` when the node is a direct return value, including one parenthesized layer
|
|
110
|
+
*/
|
|
111
|
+
export function isDirectReturn(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
|
|
112
|
+
const parent = node.parent
|
|
113
|
+
if (ts.isReturnStatement(parent)) return true
|
|
114
|
+
if (ts.isArrowFunction(parent) && parent.body === node) return true
|
|
115
|
+
if (!ts.isParenthesizedExpression(parent)) return false
|
|
116
|
+
const container = parent.parent
|
|
117
|
+
return (
|
|
118
|
+
ts.isReturnStatement(container) || (ts.isArrowFunction(container) && container.body === parent)
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether a function expression is assigned by one module-scope variable declaration. */
|
|
123
|
+
export function isModuleFunction(node: ts.ArrowFunction | ts.FunctionExpression): boolean {
|
|
124
|
+
const declaration = node.parent
|
|
125
|
+
const list = declaration.parent
|
|
126
|
+
const statement = list.parent
|
|
127
|
+
return (
|
|
128
|
+
ts.isVariableDeclaration(declaration) &&
|
|
129
|
+
ts.isVariableDeclarationList(list) &&
|
|
130
|
+
ts.isVariableStatement(statement) &&
|
|
131
|
+
ts.isSourceFile(statement.parent)
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format a syntax node's source position as a one-based line and character.
|
|
137
|
+
*
|
|
138
|
+
* @param node - The syntax node whose starting position to format
|
|
139
|
+
* @returns The node's one-based `line:character` position
|
|
140
|
+
*/
|
|
141
|
+
export function formatPolicyPosition(node: ts.Node): string {
|
|
142
|
+
const source = node.getSourceFile()
|
|
143
|
+
const position = source.getLineAndCharacterOfPosition(node.getStart())
|
|
144
|
+
return `${String(position.line + 1)}:${String(position.character + 1)}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Whether a property signature belongs to a centralized interface or type alias contract. */
|
|
148
|
+
export function isContractProperty(node: ts.PropertySignature): boolean {
|
|
149
|
+
let parent: ts.Node = node.parent
|
|
150
|
+
while (ts.isTypeLiteralNode(parent)) parent = parent.parent
|
|
151
|
+
return ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether the sole production triple-slash reference is the generated browser Vite contract. */
|
|
155
|
+
export function hasAllowedTripleSlashReference(path: string, source: ts.SourceFile): boolean {
|
|
156
|
+
return (
|
|
157
|
+
path.replaceAll('\\', '/') === 'app/browser/env.d.ts' &&
|
|
158
|
+
source.referencedFiles.length === 0 &&
|
|
159
|
+
source.libReferenceDirectives.length === 0 &&
|
|
160
|
+
source.typeReferenceDirectives.length === 1 &&
|
|
161
|
+
source.typeReferenceDirectives[0]?.fileName === 'vite/client'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Inspect a Vue single-file component for syntax that can bypass declared import policy. */
|
|
166
|
+
export function inspectVueCodingLaw(
|
|
167
|
+
path: string,
|
|
168
|
+
scripts: readonly VueScriptBlockInterface[] = [],
|
|
169
|
+
): readonly string[] {
|
|
170
|
+
const violations: string[] = []
|
|
171
|
+
for (const [index, script] of scripts.entries()) {
|
|
172
|
+
if (
|
|
173
|
+
script.lang !== 'ts' &&
|
|
174
|
+
script.lang !== 'tsx' &&
|
|
175
|
+
script.lang !== 'mts' &&
|
|
176
|
+
script.lang !== 'cts'
|
|
177
|
+
) {
|
|
178
|
+
violations.push(`${path}.script-${String(index)} requires a TypeScript script language`)
|
|
179
|
+
}
|
|
180
|
+
const extension =
|
|
181
|
+
script.lang === 'tsx' || script.lang === 'mts' || script.lang === 'cts' ? script.lang : 'ts'
|
|
182
|
+
violations.push(
|
|
183
|
+
...inspectCodingLaw(`${path}.script-${String(index)}.${extension}`, script.content),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
return violations
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Add syntax-wide coding-law violations while traversing one source tree. */
|
|
190
|
+
export function inspectCodingNode(path: string, node: ts.Node, violations: string[]): void {
|
|
191
|
+
if (
|
|
192
|
+
ts.isAsExpression(node) ||
|
|
193
|
+
ts.isTypeAssertionExpression(node) ||
|
|
194
|
+
ts.isNonNullExpression(node)
|
|
195
|
+
) {
|
|
196
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids type/non-null assertions`)
|
|
197
|
+
}
|
|
198
|
+
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
199
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids any`)
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
203
|
+
node.moduleSpecifier !== undefined &&
|
|
204
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
205
|
+
isUnsupportedModuleSpecifier(node.moduleSpecifier.text)
|
|
206
|
+
) {
|
|
207
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
ts.isPropertySignature(node) &&
|
|
211
|
+
isContractProperty(node) &&
|
|
212
|
+
!hasModifier(node, ts.SyntaxKind.ReadonlyKeyword)
|
|
213
|
+
) {
|
|
214
|
+
violations.push(`${path}:${formatPolicyPosition(node)} requires readonly contract properties`)
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
ts.isCallExpression(node) &&
|
|
218
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
219
|
+
(node.arguments.length !== 1 || !node.arguments.every(ts.isStringLiteral))
|
|
220
|
+
) {
|
|
221
|
+
violations.push(
|
|
222
|
+
`${path}:${formatPolicyPosition(node)} requires dynamic imports to use string literals so import policy remains enforceable`,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
ts.isCallExpression(node) &&
|
|
227
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
228
|
+
node.arguments.length === 1 &&
|
|
229
|
+
node.arguments.every(ts.isStringLiteral) &&
|
|
230
|
+
isUnsupportedModuleSpecifier(node.arguments[0]?.text ?? '')
|
|
231
|
+
) {
|
|
232
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids non-Node URL module specifiers`)
|
|
233
|
+
}
|
|
234
|
+
if (ts.isFunctionDeclaration(node) && !ts.isSourceFile(node.parent)) {
|
|
235
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids nested function declarations`)
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
239
|
+
!isDirectCallback(node) &&
|
|
240
|
+
!isDirectReturn(node) &&
|
|
241
|
+
!isModuleFunction(node)
|
|
242
|
+
) {
|
|
243
|
+
violations.push(`${path}:${formatPolicyPosition(node)} forbids hidden function assignments`)
|
|
244
|
+
}
|
|
245
|
+
ts.forEachChild(node, (child) => inspectCodingNode(path, child, violations))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Inspect one TypeScript source module for repository coding-law violations. */
|
|
249
|
+
export function inspectCodingLaw(path: string, content: string): readonly string[] {
|
|
250
|
+
const violations: string[] = []
|
|
251
|
+
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true)
|
|
252
|
+
const file = basename(path)
|
|
253
|
+
|
|
254
|
+
if (/\.[cm]?jsx?$/u.test(path)) {
|
|
255
|
+
violations.push(`${path} production modules use TypeScript source extensions`)
|
|
256
|
+
}
|
|
257
|
+
if (/@ts-(?:expect-error|ignore|nocheck)|eslint-disable|oxlint-disable/u.test(content)) {
|
|
258
|
+
violations.push(`${path} forbids suppression directives`)
|
|
259
|
+
}
|
|
260
|
+
if (
|
|
261
|
+
(source.referencedFiles.length > 0 ||
|
|
262
|
+
source.libReferenceDirectives.length > 0 ||
|
|
263
|
+
source.typeReferenceDirectives.length > 0) &&
|
|
264
|
+
!hasAllowedTripleSlashReference(path, source)
|
|
265
|
+
) {
|
|
266
|
+
violations.push(`${path} forbids triple-slash references outside app/browser/env.d.ts`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (file === 'index.ts') {
|
|
270
|
+
for (const statement of source.statements) {
|
|
271
|
+
if (
|
|
272
|
+
!ts.isExportDeclaration(statement) ||
|
|
273
|
+
statement.exportClause !== undefined ||
|
|
274
|
+
statement.isTypeOnly ||
|
|
275
|
+
statement.moduleSpecifier === undefined
|
|
276
|
+
) {
|
|
277
|
+
violations.push(`${path} barrels contain only export * declarations`)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const statement of source.statements) {
|
|
283
|
+
if (
|
|
284
|
+
(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) &&
|
|
285
|
+
file !== 'types.ts'
|
|
286
|
+
) {
|
|
287
|
+
violations.push(`${path} centralizes interfaces and type aliases in types.ts`)
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
CENTRAL_SOURCE_FILES.includes(file) &&
|
|
291
|
+
(ts.isClassDeclaration(statement) ||
|
|
292
|
+
ts.isFunctionDeclaration(statement) ||
|
|
293
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
294
|
+
ts.isTypeAliasDeclaration(statement) ||
|
|
295
|
+
ts.isVariableStatement(statement)) &&
|
|
296
|
+
!hasExportModifier(statement)
|
|
297
|
+
) {
|
|
298
|
+
violations.push(`${path} exports every centralized declaration`)
|
|
299
|
+
}
|
|
300
|
+
if (ts.isFunctionDeclaration(statement) && !FUNCTION_SOURCE_FILES.includes(file)) {
|
|
301
|
+
violations.push(`${path} places module functions in their centralized kind file`)
|
|
302
|
+
}
|
|
303
|
+
if (ts.isVariableStatement(statement) && !DATA_SOURCE_FILES.includes(file)) {
|
|
304
|
+
violations.push(`${path} places module data in its centralized kind file`)
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
ts.isClassDeclaration(statement) &&
|
|
308
|
+
file !== 'errors.ts' &&
|
|
309
|
+
!/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)
|
|
310
|
+
) {
|
|
311
|
+
violations.push(`${path} places each class in its matching implementation or errors file`)
|
|
312
|
+
}
|
|
313
|
+
if (ts.isEnumDeclaration(statement) && file !== 'types.ts') {
|
|
314
|
+
violations.push(`${path} centralizes enum contracts in types.ts`)
|
|
315
|
+
}
|
|
316
|
+
if (file === 'constants.ts' && ts.isVariableStatement(statement)) {
|
|
317
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
318
|
+
violations.push(`${path} constants.ts permits only const declarations`)
|
|
319
|
+
}
|
|
320
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
321
|
+
if (
|
|
322
|
+
!ts.isIdentifier(declaration.name) ||
|
|
323
|
+
!/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)
|
|
324
|
+
) {
|
|
325
|
+
violations.push(
|
|
326
|
+
`${path}:${formatPolicyPosition(declaration)} requires UPPER_SNAKE_CASE constants`,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
declaration.initializer !== undefined &&
|
|
331
|
+
(ts.isArrayLiteralExpression(declaration.initializer) ||
|
|
332
|
+
ts.isObjectLiteralExpression(declaration.initializer))
|
|
333
|
+
) {
|
|
334
|
+
violations.push(
|
|
335
|
+
`${path}:${formatPolicyPosition(declaration)} freezes collection constants`,
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file)) {
|
|
343
|
+
const classes = source.statements.filter(ts.isClassDeclaration)
|
|
344
|
+
const invalid = source.statements.filter(
|
|
345
|
+
(statement) => !ts.isImportDeclaration(statement) && !ts.isClassDeclaration(statement),
|
|
346
|
+
)
|
|
347
|
+
if (
|
|
348
|
+
classes.length !== 1 ||
|
|
349
|
+
invalid.length !== 0 ||
|
|
350
|
+
classes[0]?.name?.text !== file.slice(0, -3) ||
|
|
351
|
+
classes[0] === undefined ||
|
|
352
|
+
!hasExportModifier(classes[0])
|
|
353
|
+
) {
|
|
354
|
+
violations.push(
|
|
355
|
+
`${path} implementation modules contain imports and one matching exported class`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
for (const member of classes[0]?.members ?? []) {
|
|
359
|
+
if (hasModifier(member, ts.SyntaxKind.PrivateKeyword)) {
|
|
360
|
+
violations.push(`${path}:${formatPolicyPosition(member)} uses runtime # privacy`)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
inspectCodingNode(path, source, violations)
|
|
366
|
+
return violations
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Inspect every production TypeScript module under one workspace. */
|
|
370
|
+
export function inspectCodingWorkspace(
|
|
371
|
+
root: string,
|
|
372
|
+
vueScripts?: VueScriptExtractorInterface,
|
|
373
|
+
): readonly string[] {
|
|
374
|
+
const violations: string[] = []
|
|
375
|
+
for (const path of globSync('{app,src}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}', {
|
|
376
|
+
cwd: root,
|
|
377
|
+
})) {
|
|
378
|
+
const content = readFileSync(join(root, path), 'utf8')
|
|
379
|
+
const normalizedPath = normalizePolicyPath(path)
|
|
380
|
+
if (path.endsWith('.vue') && !normalizedPath.startsWith('app/browser/')) {
|
|
381
|
+
violations.push(`${normalizedPath} Vue components belong in app/browser`)
|
|
382
|
+
}
|
|
383
|
+
violations.push(
|
|
384
|
+
...(path.endsWith('.vue')
|
|
385
|
+
? inspectVueCodingLaw(normalizedPath, vueScripts?.(normalizedPath, content))
|
|
386
|
+
: inspectCodingLaw(normalizedPath, content)),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
return violations
|
|
390
|
+
}
|