@orkestrel/scaffold 0.0.63 → 0.0.64
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 +18 -103
- package/dist/bin/main.js +95 -27
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +2 -2
- package/dist/host/CLAUDE.md +6 -0
- package/dist/host/agents/orchestration.md +23 -15
- package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
- package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +6 -6
- package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +4 -4
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +5 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +15 -15
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +43 -17
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +41 -16
- package/dist/host/claude/agents/orkestrel.md +56 -56
- package/dist/host/claude/agents/reviewer.md +13 -0
- package/dist/host/claude/rules/architecture.md +51 -45
- package/dist/host/claude/rules/documentation.md +18 -1
- package/dist/host/claude/rules/portability.md +2 -0
- package/dist/host/claude/rules/quality.md +1 -1
- package/dist/host/claude/rules/tests.md +12 -11
- package/dist/host/claude/rules/typescript.md +5 -0
- package/dist/host/claude/rules/workspace.md +23 -18
- package/dist/host/claude/rules/writing.md +4 -0
- package/dist/host/codex/agents/orkestrel.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -2
- package/dist/host/configs/helpers.ts +311 -2
- package/dist/host/configs/policy.ts +1100 -51
- package/dist/host/dotfiles/oxlintrc.json +72 -1
- package/dist/host/guides/guide.md +749 -222
- package/dist/host/guides/scaffold.md +472 -378
- package/dist/host/manifest.json +34 -33
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/config.test.ts +1200 -16
- package/dist/host/tests/policy.test.ts +157 -173
- package/dist/host/tests/setupPolicy.ts +522 -1007
- package/dist/src/core/index.cjs +371 -277
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +130 -120
- package/dist/src/core/index.d.ts +130 -120
- package/dist/src/core/index.js +371 -276
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +28 -21
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +38 -33
- package/dist/src/server/index.d.ts +38 -33
- package/dist/src/server/index.js +28 -21
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -16
|
@@ -8,34 +8,27 @@ import {
|
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from 'node:fs'
|
|
10
10
|
import { tmpdir } from 'node:os'
|
|
11
|
-
import { basename, dirname, join, matchesGlob } from 'node:path'
|
|
12
|
-
import
|
|
11
|
+
import { basename, dirname, join, matchesGlob, relative as relativePath, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { stripPolicyCode, textToPolicyHits } from '../configs/policy.js'
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
+
/** Names a rule the fleet sweep decides from workspace text and paths. */
|
|
15
16
|
export type PolicyRule =
|
|
16
17
|
| 'bridge'
|
|
17
|
-
| 'class'
|
|
18
|
-
| 'constant'
|
|
19
|
-
| 'data'
|
|
20
|
-
| 'domain'
|
|
21
|
-
| 'export'
|
|
22
|
-
| 'factory'
|
|
23
|
-
| 'function'
|
|
24
18
|
| 'mirror'
|
|
25
|
-
| 'parser'
|
|
26
19
|
| 'portability'
|
|
20
|
+
| 'prose'
|
|
27
21
|
| 'rules'
|
|
28
22
|
| 'skill'
|
|
29
23
|
| 'suppression'
|
|
30
|
-
| 'type'
|
|
31
24
|
|
|
32
|
-
/**
|
|
25
|
+
/** Describes one workspace file a physical control writes. */
|
|
33
26
|
export interface PolicySource {
|
|
34
27
|
readonly path: string
|
|
35
28
|
readonly content: string
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
/**
|
|
31
|
+
/** Describes one policy failure the sweep reports. */
|
|
39
32
|
export interface PolicyViolation {
|
|
40
33
|
readonly rule: PolicyRule
|
|
41
34
|
readonly path: string
|
|
@@ -43,7 +36,7 @@ export interface PolicyViolation {
|
|
|
43
36
|
readonly message: string
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
/**
|
|
39
|
+
/** Describes one physical negative control, including the population boundary it attacks. */
|
|
47
40
|
export interface PolicyControl {
|
|
48
41
|
readonly label: string
|
|
49
42
|
readonly membership: string
|
|
@@ -91,7 +84,7 @@ export function createPolicyScratch(options: { readonly prefix: string }): Polic
|
|
|
91
84
|
}
|
|
92
85
|
}
|
|
93
86
|
|
|
94
|
-
/**
|
|
87
|
+
/** Holds parsed skill frontmatter and the exact scalar source used for bridge comparison. */
|
|
95
88
|
export interface SkillFrontmatter {
|
|
96
89
|
readonly keys: readonly string[]
|
|
97
90
|
readonly name: string | undefined
|
|
@@ -102,113 +95,29 @@ export interface SkillFrontmatter {
|
|
|
102
95
|
}
|
|
103
96
|
}
|
|
104
97
|
|
|
105
|
-
/**
|
|
98
|
+
/** Names the directory whose immediate child directories form the complete skill family. */
|
|
106
99
|
export const SKILL_FAMILY_ROOT = '.agents/skills'
|
|
107
100
|
|
|
108
|
-
/**
|
|
101
|
+
/** Names the directory whose immediate child directories form the Claude skill bridge family. */
|
|
109
102
|
export const SKILL_BRIDGE_ROOT = '.claude/skills'
|
|
110
103
|
|
|
111
|
-
/**
|
|
104
|
+
/** Holds the minimal valid skill text for physical family controls. */
|
|
112
105
|
export const SKILL_POLICY_TEXT =
|
|
113
106
|
'---\nname: sample\ndescription: Use this skill for a policy fixture.\n---\n\n# Skill\n'
|
|
114
107
|
|
|
115
|
-
/**
|
|
108
|
+
/** Holds skill text naming one reference for physical family controls. */
|
|
116
109
|
export const SKILL_REFERENCE_TEXT = `${SKILL_POLICY_TEXT}\nRead references/example.md.\n`
|
|
117
110
|
|
|
118
|
-
/**
|
|
111
|
+
/** Holds the minimal valid provider bridge text for physical bridge controls. */
|
|
119
112
|
export const SKILL_BRIDGE_TEXT = `${SKILL_POLICY_TEXT}\nRead \`.agents/skills/sample/SKILL.md\`.\n`
|
|
120
113
|
|
|
121
|
-
/**
|
|
114
|
+
/** Holds canonical skill metadata whose values each carry YAML's escaped apostrophe. */
|
|
122
115
|
export const SKILL_APOSTROPHE_METADATA =
|
|
123
116
|
"interface:\n display_name: 'Owner''s Fixture'\n" +
|
|
124
117
|
" short_description: 'Exercise the family''s apostrophe rule'\n" +
|
|
125
118
|
" default_prompt: 'Use $sample for this fixture''s value.'\n"
|
|
126
119
|
|
|
127
|
-
/**
|
|
128
|
-
export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
129
|
-
'cloners.ts',
|
|
130
|
-
'combinators.ts',
|
|
131
|
-
'compilers.ts',
|
|
132
|
-
'constants.ts',
|
|
133
|
-
'contracts.ts',
|
|
134
|
-
'errors.ts',
|
|
135
|
-
'factories.ts',
|
|
136
|
-
'handlers.ts',
|
|
137
|
-
'helpers.ts',
|
|
138
|
-
'index.ts',
|
|
139
|
-
'inferers.ts',
|
|
140
|
-
'middlewares.ts',
|
|
141
|
-
'parsers.ts',
|
|
142
|
-
'relations.ts',
|
|
143
|
-
'routes.ts',
|
|
144
|
-
'schemas.ts',
|
|
145
|
-
'seeders.ts',
|
|
146
|
-
'shapers.ts',
|
|
147
|
-
'templates.ts',
|
|
148
|
-
'types.ts',
|
|
149
|
-
'validators.ts',
|
|
150
|
-
])
|
|
151
|
-
|
|
152
|
-
/** The exhaustive centralized-file set that permits module functions. */
|
|
153
|
-
export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
154
|
-
'cloners.ts',
|
|
155
|
-
'combinators.ts',
|
|
156
|
-
'compilers.ts',
|
|
157
|
-
'errors.ts',
|
|
158
|
-
'factories.ts',
|
|
159
|
-
'handlers.ts',
|
|
160
|
-
'helpers.ts',
|
|
161
|
-
'inferers.ts',
|
|
162
|
-
'middlewares.ts',
|
|
163
|
-
'parsers.ts',
|
|
164
|
-
'relations.ts',
|
|
165
|
-
'schemas.ts',
|
|
166
|
-
'seeders.ts',
|
|
167
|
-
'shapers.ts',
|
|
168
|
-
'validators.ts',
|
|
169
|
-
])
|
|
170
|
-
|
|
171
|
-
/** Centralized files that permit module data by declaration syntax. */
|
|
172
|
-
export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
173
|
-
'combinators.ts',
|
|
174
|
-
'constants.ts',
|
|
175
|
-
'contracts.ts',
|
|
176
|
-
'relations.ts',
|
|
177
|
-
'routes.ts',
|
|
178
|
-
'schemas.ts',
|
|
179
|
-
'shapers.ts',
|
|
180
|
-
'templates.ts',
|
|
181
|
-
'validators.ts',
|
|
182
|
-
])
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Files excluded from the module-data rule because their namespace values hold helper behavior.
|
|
186
|
-
* This exclusion also permits unrelated module data such as `export const RETRIES = 3`.
|
|
187
|
-
*/
|
|
188
|
-
export const DATA_EXEMPT_FILES: readonly string[] = Object.freeze(['helpers.ts'])
|
|
189
|
-
|
|
190
|
-
/** Fleet-registered folders whose direct modules each contain one named function. */
|
|
191
|
-
export const FUNCTION_DOMAIN_FOLDERS: readonly string[] = Object.freeze([
|
|
192
|
-
'app/browser/composables',
|
|
193
|
-
'src/server/execution',
|
|
194
|
-
])
|
|
195
|
-
|
|
196
|
-
/** Every ambient declaration suffix the source glob collects and the parsed population excludes. */
|
|
197
|
-
export const POLICY_AMBIENT_SUFFIXES: readonly string[] = Object.freeze([
|
|
198
|
-
'.d.cts',
|
|
199
|
-
'.d.mts',
|
|
200
|
-
'.d.ts',
|
|
201
|
-
])
|
|
202
|
-
|
|
203
|
-
/** TypeScript source extensions whose declaration syntax the sweep reads. */
|
|
204
|
-
export const POLICY_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
|
|
205
|
-
'cts',
|
|
206
|
-
'mts',
|
|
207
|
-
'ts',
|
|
208
|
-
'tsx',
|
|
209
|
-
])
|
|
210
|
-
|
|
211
|
-
/** Every extension through which a mirrored test can name a module. */
|
|
120
|
+
/** Lists every extension through which a mirrored test can name a module. */
|
|
212
121
|
export const POLICY_MODULE_EXTENSIONS: readonly string[] = Object.freeze([
|
|
213
122
|
'cts',
|
|
214
123
|
'mts',
|
|
@@ -219,42 +128,45 @@ export const POLICY_MODULE_EXTENSIONS: readonly string[] = Object.freeze([
|
|
|
219
128
|
'css',
|
|
220
129
|
])
|
|
221
130
|
|
|
222
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Lists the module extensions whose extensionless stem can resolve a leading-underscore partial.
|
|
133
|
+
*/
|
|
223
134
|
export const POLICY_PARTIAL_EXTENSIONS: readonly string[] = Object.freeze(['scss', 'css'])
|
|
224
135
|
|
|
225
|
-
/**
|
|
136
|
+
/** Names the reserved stem prefix whose mirrored module can resolve inside the tests axis. */
|
|
226
137
|
export const POLICY_TESTS_MODULE_PREFIX = 'setup'
|
|
227
138
|
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
/** The complete module population available to mirrored tests under either workspace axis. */
|
|
139
|
+
/**
|
|
140
|
+
* Matches the complete module population available to mirrored tests under either workspace axis.
|
|
141
|
+
*/
|
|
232
142
|
export const POLICY_MODULE_GLOB = `{app,src}/**/*.{${POLICY_MODULE_EXTENSIONS.join(',')}}`
|
|
233
143
|
|
|
234
|
-
/**
|
|
144
|
+
/** Matches the tests-axis setup module population available to mirrored tests. */
|
|
235
145
|
export const POLICY_TESTS_MODULE_GLOB = `tests/**/${POLICY_TESTS_MODULE_PREFIX}*.ts`
|
|
236
146
|
|
|
237
|
-
/**
|
|
147
|
+
/** Matches the mirrored module-test population inspected under either workspace axis. */
|
|
238
148
|
export const POLICY_TEST_GLOB = 'tests/{app,src}/**/*.test.ts'
|
|
239
149
|
|
|
240
150
|
// Compose suppression tokens so the instrument does not report its own definitions or controls.
|
|
241
151
|
export const POLICY_SUPPRESSION_DIRECTIVE = ['oxlint', '-disable'].join('')
|
|
242
152
|
|
|
243
|
-
/**
|
|
153
|
+
/** Matches the source, test, config, and script files inspected for lint suppression directives. */
|
|
244
154
|
export const POLICY_SUPPRESSION_GLOB: readonly string[] = Object.freeze([
|
|
245
155
|
'{src,app,tests,configs,scripts}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
|
|
246
156
|
'*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
|
|
247
157
|
])
|
|
248
158
|
|
|
249
|
-
/**
|
|
159
|
+
/** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
|
|
250
160
|
export const POLICY_WIRING_RULES: readonly string[] = Object.freeze([
|
|
251
161
|
'policy/no-mocking',
|
|
252
162
|
'policy/no-keyword-privacy',
|
|
163
|
+
'policy/no-malformed-summary',
|
|
164
|
+
'policy/no-banned-term',
|
|
253
165
|
'typescript/parameter-properties',
|
|
254
166
|
'typescript/explicit-member-accessibility',
|
|
255
167
|
])
|
|
256
168
|
|
|
257
|
-
/**
|
|
169
|
+
/** Lists the linted workspace roots that ignore patterns must not reach. */
|
|
258
170
|
export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
|
|
259
171
|
'src',
|
|
260
172
|
'app',
|
|
@@ -262,13 +174,16 @@ export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
|
|
|
262
174
|
'configs',
|
|
263
175
|
])
|
|
264
176
|
|
|
265
|
-
/**
|
|
177
|
+
/** Matches either lint suppression token the text sweep refuses. */
|
|
266
178
|
export const POLICY_SUPPRESSION_PATTERN = new RegExp(
|
|
267
179
|
[['eslint', '-disable'].join(''), POLICY_SUPPRESSION_DIRECTIVE].join('|'),
|
|
268
180
|
'u',
|
|
269
181
|
)
|
|
270
182
|
|
|
271
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Matches the workspace-authored path population inspected for a name a Windows checkout cannot
|
|
185
|
+
* hold.
|
|
186
|
+
*/
|
|
272
187
|
export const POLICY_PORTABILITY_GLOB: readonly string[] = Object.freeze([
|
|
273
188
|
'{src,app,configs,tests,scripts,guides}/**/*',
|
|
274
189
|
'{.agents,.claude,.codex,.cursor,.github}/**/*',
|
|
@@ -276,10 +191,7 @@ export const POLICY_PORTABILITY_GLOB: readonly string[] = Object.freeze([
|
|
|
276
191
|
'.*',
|
|
277
192
|
])
|
|
278
193
|
|
|
279
|
-
/**
|
|
280
|
-
export const POLICY_PORTABILITY_SOURCE_GLOB = '{src,app,configs}/**/*.ts'
|
|
281
|
-
|
|
282
|
-
/** Every device name Windows reserves, whatever extension the segment carries. */
|
|
194
|
+
/** Lists every device name Windows reserves, whatever extension the segment carries. */
|
|
283
195
|
export const POLICY_RESERVED_NAMES: readonly string[] = Object.freeze([
|
|
284
196
|
'aux',
|
|
285
197
|
'com1',
|
|
@@ -305,513 +217,109 @@ export const POLICY_RESERVED_NAMES: readonly string[] = Object.freeze([
|
|
|
305
217
|
'prn',
|
|
306
218
|
])
|
|
307
219
|
|
|
308
|
-
/**
|
|
220
|
+
/** Matches every character Windows refuses inside a path segment. */
|
|
309
221
|
export const POLICY_RESERVED_PATTERN = /[<>:"|?*]/u
|
|
310
222
|
|
|
311
|
-
/**
|
|
223
|
+
/** Matches a shell script named as a complete path token inside a manifest script. */
|
|
312
224
|
export const POLICY_SHELL_PATTERN = /\.sh\b/u
|
|
313
225
|
|
|
314
|
-
/**
|
|
226
|
+
/** Names the directory whose direct Markdown files form the complete rule family. */
|
|
315
227
|
export const POLICY_RULE_ROOT = '.claude/rules'
|
|
316
228
|
|
|
317
|
-
/**
|
|
229
|
+
/** Names the root instruction file whose rule map registers the rule family. */
|
|
318
230
|
export const POLICY_RULE_MAP_FILE = 'AGENTS.md'
|
|
319
231
|
|
|
320
|
-
/**
|
|
232
|
+
/** Names the heading that opens the root instruction file's rule map table. */
|
|
321
233
|
export const POLICY_RULE_MAP_HEADING = '## Rule map'
|
|
322
234
|
|
|
323
|
-
/**
|
|
235
|
+
/** Names the workspace manifest whose scripts run on every supported host. */
|
|
324
236
|
export const POLICY_MANIFEST_FILE = 'package.json'
|
|
325
237
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
238
|
+
/** Lists the directory names the prose sweep never descends into. */
|
|
239
|
+
export const POLICY_PROSE_EXCLUSIONS: readonly string[] = Object.freeze([
|
|
240
|
+
'.git',
|
|
241
|
+
'.orkestrel',
|
|
242
|
+
'dist',
|
|
243
|
+
'node_modules',
|
|
244
|
+
'tmp',
|
|
245
|
+
])
|
|
335
246
|
|
|
336
|
-
/**
|
|
337
|
-
|
|
338
|
-
*
|
|
339
|
-
* @param node - The declaration to inspect.
|
|
340
|
-
* @param modifier - The modifier syntax to find.
|
|
341
|
-
* @returns `true` when the declaration carries the modifier.
|
|
342
|
-
*/
|
|
343
|
-
export function hasPolicyModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean {
|
|
344
|
-
return (
|
|
345
|
-
ts.canHaveModifiers(node) &&
|
|
346
|
-
ts.getModifiers(node)?.some((candidate) => candidate.kind === modifier) === true
|
|
347
|
-
)
|
|
348
|
-
}
|
|
247
|
+
/** Matches a top-level guide path and captures the package short name it is written for. */
|
|
248
|
+
export const POLICY_MIRROR_PATTERN = /^guides\/([^/]+)\.md$/u
|
|
349
249
|
|
|
350
|
-
/**
|
|
351
|
-
|
|
352
|
-
*
|
|
353
|
-
* @param node - The syntax node to locate.
|
|
354
|
-
* @returns Its one-based source line.
|
|
355
|
-
*/
|
|
356
|
-
export function getPolicyLine(node: ts.Node): number {
|
|
357
|
-
const position = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart())
|
|
358
|
-
return position.line + 1
|
|
359
|
-
}
|
|
250
|
+
/** Names the guide a workspace holds as its own index rather than as a mirror. */
|
|
251
|
+
export const POLICY_GUIDE_MAP = 'README'
|
|
360
252
|
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
*
|
|
364
|
-
* @param path - The workspace-relative source path to inspect.
|
|
365
|
-
* @returns `true` when the path has the registered function-module shape.
|
|
366
|
-
*/
|
|
367
|
-
export function isFunctionDomainPath(path: string): boolean {
|
|
368
|
-
const normalized = normalizePolicyPath(path)
|
|
369
|
-
const file = basename(normalized)
|
|
370
|
-
return (
|
|
371
|
-
FUNCTION_DOMAIN_FOLDERS.includes(dirname(normalized).replaceAll('\\', '/')) &&
|
|
372
|
-
/^[a-z][A-Za-z0-9]*\.ts$/u.test(file) &&
|
|
373
|
-
file !== 'index.ts' &&
|
|
374
|
-
file !== 'main.ts' &&
|
|
375
|
-
!CENTRAL_SOURCE_FILES.includes(file)
|
|
376
|
-
)
|
|
377
|
-
}
|
|
253
|
+
/** Names the rule file whose substitution table is the denylist's source. */
|
|
254
|
+
export const POLICY_TERM_FILE = '.claude/rules/writing.md'
|
|
378
255
|
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
*
|
|
382
|
-
* @param expression - The expression to unwrap.
|
|
383
|
-
* @returns The arrow or function expression it holds directly, or `undefined` for anything else.
|
|
384
|
-
*/
|
|
385
|
-
export function expressionToPolicyFunction(
|
|
386
|
-
expression: ts.Expression | undefined,
|
|
387
|
-
): ts.ArrowFunction | ts.FunctionExpression | undefined {
|
|
388
|
-
let current = expression
|
|
389
|
-
while (current !== undefined && ts.isParenthesizedExpression(current)) {
|
|
390
|
-
current = current.expression
|
|
391
|
-
}
|
|
392
|
-
if (current === undefined) return undefined
|
|
393
|
-
return ts.isArrowFunction(current) || ts.isFunctionExpression(current) ? current : undefined
|
|
394
|
-
}
|
|
256
|
+
/** Names the heading that opens the substitution table. */
|
|
257
|
+
export const POLICY_TERM_HEADING = '## Substitutions'
|
|
395
258
|
|
|
396
259
|
/**
|
|
397
|
-
*
|
|
260
|
+
* Names the catalog agent file whose table registers every fleet package.
|
|
398
261
|
*
|
|
399
|
-
* @
|
|
400
|
-
*
|
|
262
|
+
* @remarks
|
|
263
|
+
* The path is written here rather than read from the scaffold constant that plans it, because this
|
|
264
|
+
* module is vendored byte-identical into every workspace and imports nothing from the package.
|
|
401
265
|
*/
|
|
402
|
-
export
|
|
403
|
-
|
|
404
|
-
|
|
266
|
+
export const POLICY_CATALOG_FILE = '.claude/agents/orkestrel.md'
|
|
267
|
+
|
|
268
|
+
/** Names the heading that opens the package catalog. */
|
|
269
|
+
export const POLICY_CATALOG_HEADING = '## Package catalog'
|
|
405
270
|
|
|
406
271
|
/**
|
|
407
|
-
*
|
|
272
|
+
* Normalizes platform separators for stable matching and diagnostics.
|
|
408
273
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* those positions nothing extra.
|
|
412
|
-
*
|
|
413
|
-
* @param node - The module region to inspect.
|
|
414
|
-
* @returns `true` when the region holds an arrow or function expression the law does not permit.
|
|
415
|
-
* An arrow or function expression passed directly as a call or `new` argument is exempt itself, and
|
|
416
|
-
* reports only for what its parameters and body nest. A class expression is never function syntax.
|
|
274
|
+
* @param path - The workspace-relative path to normalize.
|
|
275
|
+
* @returns The path with forward slashes and no duplicate separators.
|
|
417
276
|
*/
|
|
418
|
-
export function
|
|
419
|
-
|
|
420
|
-
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return true
|
|
421
|
-
if (ts.isClassExpression(node)) return false
|
|
422
|
-
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
423
|
-
if (hasModulePolicyFunction(node.expression)) return true
|
|
424
|
-
if (node.arguments !== undefined) {
|
|
425
|
-
for (const argument of node.arguments) {
|
|
426
|
-
const permitted = expressionToPolicyFunction(argument)
|
|
427
|
-
if (permitted === undefined) {
|
|
428
|
-
if (hasModulePolicyFunction(argument)) return true
|
|
429
|
-
} else if (nestsPolicyFunction(permitted)) return true
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return false
|
|
433
|
-
}
|
|
434
|
-
let found = false
|
|
435
|
-
ts.forEachChild(node, (child) => {
|
|
436
|
-
if (!found && hasModulePolicyFunction(child)) found = true
|
|
437
|
-
})
|
|
438
|
-
return found
|
|
277
|
+
export function normalizePolicyPath(path: string): string {
|
|
278
|
+
return path.replaceAll('\\', '/').replace(/\/+/gu, '/')
|
|
439
279
|
}
|
|
440
280
|
|
|
441
281
|
/**
|
|
442
|
-
*
|
|
282
|
+
* Normalizes a native path or `file:` URI for workspace-relative policy comparisons.
|
|
443
283
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* @param node - The permitted callback or returned function to inspect.
|
|
449
|
-
* @returns `true` when its parameters or body nest function syntax the law does not permit.
|
|
284
|
+
* @param root - The host path used as the comparison root.
|
|
285
|
+
* @param filename - The diagnostic filename as a native path or `file:` URI.
|
|
286
|
+
* @returns The normalized path relative to the resolved comparison root.
|
|
287
|
+
* @throws Thrown when a `file:` filename is not a valid file URI.
|
|
450
288
|
*/
|
|
451
|
-
export function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (!ts.isBlock(node.body)) {
|
|
456
|
-
const returned = expressionToPolicyFunction(node.body)
|
|
457
|
-
if (returned !== undefined) return nestsPolicyFunction(returned)
|
|
458
|
-
}
|
|
459
|
-
return hasNestedPolicyFunction(node.body)
|
|
289
|
+
export function normalizePolicyFilename(root: string, filename: string): string {
|
|
290
|
+
const base = resolve(root)
|
|
291
|
+
const path = filename.startsWith('file:') ? fileURLToPath(filename) : resolve(base, filename)
|
|
292
|
+
return normalizePolicyPath(relativePath(base, path))
|
|
460
293
|
}
|
|
461
294
|
|
|
462
295
|
/**
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* A direct return keeps its permission through control flow. Every other nested function reports
|
|
466
|
-
* unless it independently qualifies as a direct callback. Class-expression members stay outside
|
|
467
|
-
* the instrument's reach.
|
|
468
|
-
*
|
|
469
|
-
* @param node - The region inside a permitted function to inspect.
|
|
470
|
-
* @returns `true` when the region holds function syntax the law does not permit.
|
|
296
|
+
* Reports whether a parsed configuration value is a plain record rather than an array or a
|
|
297
|
+
* primitive.
|
|
471
298
|
*/
|
|
472
|
-
export function
|
|
473
|
-
|
|
474
|
-
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return true
|
|
475
|
-
if (ts.isClassExpression(node)) return false
|
|
476
|
-
if (ts.isReturnStatement(node)) {
|
|
477
|
-
const returned = expressionToPolicyFunction(node.expression)
|
|
478
|
-
return returned === undefined
|
|
479
|
-
? hasModulePolicyFunction(node.expression)
|
|
480
|
-
: nestsPolicyFunction(returned)
|
|
481
|
-
}
|
|
482
|
-
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
483
|
-
return hasModulePolicyFunction(node)
|
|
484
|
-
}
|
|
485
|
-
let found = false
|
|
486
|
-
ts.forEachChild(node, (child) => {
|
|
487
|
-
if (!found && hasNestedPolicyFunction(child)) found = true
|
|
488
|
-
})
|
|
489
|
-
return found
|
|
299
|
+
export function isPolicyRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
300
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
490
301
|
}
|
|
491
302
|
|
|
492
303
|
/**
|
|
493
|
-
*
|
|
304
|
+
* Creates one stable violation for an inspection result.
|
|
494
305
|
*
|
|
495
306
|
* @param rule - The rule that failed.
|
|
496
307
|
* @param path - The workspace-relative source path.
|
|
497
308
|
* @param message - The failure text.
|
|
498
|
-
* @param
|
|
309
|
+
* @param line - The one-based source line the violation occurred on, when known.
|
|
499
310
|
* @returns The stable violation record.
|
|
500
311
|
*/
|
|
501
312
|
export function createPolicyViolation(
|
|
502
313
|
rule: PolicyRule,
|
|
503
314
|
path: string,
|
|
504
315
|
message: string,
|
|
505
|
-
|
|
316
|
+
line?: number,
|
|
506
317
|
): PolicyViolation {
|
|
507
|
-
return {
|
|
508
|
-
rule,
|
|
509
|
-
path,
|
|
510
|
-
...(node === undefined ? {} : { line: getPolicyLine(node) }),
|
|
511
|
-
message,
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Inspect a registered function module's exact declaration shape.
|
|
517
|
-
*
|
|
518
|
-
* @param path - The workspace-relative source path.
|
|
519
|
-
* @param source - The parsed TypeScript source.
|
|
520
|
-
* @returns A domain-shape violation when the module is malformed.
|
|
521
|
-
*/
|
|
522
|
-
export function inspectFunctionDomain(
|
|
523
|
-
path: string,
|
|
524
|
-
source: ts.SourceFile,
|
|
525
|
-
): readonly PolicyViolation[] {
|
|
526
|
-
const expected = basename(path, '.ts')
|
|
527
|
-
const declarations = source.statements.filter(ts.isFunctionDeclaration)
|
|
528
|
-
const implementations = declarations.filter((declaration) => declaration.body !== undefined)
|
|
529
|
-
const invalid = source.statements.filter(
|
|
530
|
-
(statement) => !ts.isImportDeclaration(statement) && !ts.isFunctionDeclaration(statement),
|
|
531
|
-
)
|
|
532
|
-
const valid =
|
|
533
|
-
implementations.length === 1 &&
|
|
534
|
-
invalid.length === 0 &&
|
|
535
|
-
declarations.length > 0 &&
|
|
536
|
-
declarations.every(
|
|
537
|
-
(declaration) =>
|
|
538
|
-
declaration.name?.text === expected &&
|
|
539
|
-
hasPolicyModifier(declaration, ts.SyntaxKind.ExportKeyword) &&
|
|
540
|
-
!hasPolicyModifier(declaration, ts.SyntaxKind.DefaultKeyword),
|
|
541
|
-
)
|
|
542
|
-
return valid
|
|
543
|
-
? []
|
|
544
|
-
: [
|
|
545
|
-
createPolicyViolation(
|
|
546
|
-
'domain',
|
|
547
|
-
path,
|
|
548
|
-
'registered function modules contain imports and one matching named export',
|
|
549
|
-
),
|
|
550
|
-
]
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Inspect parser and factory function names without inferring their meaning.
|
|
555
|
-
*
|
|
556
|
-
* @param path - The workspace-relative source path.
|
|
557
|
-
* @param file - The source filename.
|
|
558
|
-
* @param name - The declared function name, when present.
|
|
559
|
-
* @param node - The function declaration or binding.
|
|
560
|
-
* @returns A parser or factory name violation when the prefix is wrong.
|
|
561
|
-
*/
|
|
562
|
-
export function inspectPolicyFunctionName(
|
|
563
|
-
path: string,
|
|
564
|
-
file: string,
|
|
565
|
-
name: string | undefined,
|
|
566
|
-
node: ts.Node,
|
|
567
|
-
): readonly PolicyViolation[] {
|
|
568
|
-
if (file === 'parsers.ts' && (name === undefined || !name.startsWith('parse'))) {
|
|
569
|
-
return [createPolicyViolation('parser', path, 'parser functions use the parse prefix', node)]
|
|
570
|
-
}
|
|
571
|
-
if (file === 'factories.ts' && (name === undefined || !name.startsWith('create'))) {
|
|
572
|
-
return [createPolicyViolation('factory', path, 'factory functions use the create prefix', node)]
|
|
573
|
-
}
|
|
574
|
-
return []
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Inspect one variable statement for data and function placement.
|
|
579
|
-
*
|
|
580
|
-
* @param path - The workspace-relative source path.
|
|
581
|
-
* @param file - The source filename.
|
|
582
|
-
* @param statement - The variable statement to inspect.
|
|
583
|
-
* @param functionDomain - Whether the path is a registered function module.
|
|
584
|
-
* @returns Every data, function, parser, and factory violation in declaration order.
|
|
585
|
-
*/
|
|
586
|
-
export function inspectPolicyVariables(
|
|
587
|
-
path: string,
|
|
588
|
-
file: string,
|
|
589
|
-
statement: ts.VariableStatement,
|
|
590
|
-
functionDomain: boolean,
|
|
591
|
-
): readonly PolicyViolation[] {
|
|
592
|
-
const violations: PolicyViolation[] = []
|
|
593
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
594
|
-
const directFunction = isPolicyFunctionInitializer(declaration.initializer)
|
|
595
|
-
const containsFunction = hasModulePolicyFunction(declaration.initializer)
|
|
596
|
-
if (!directFunction && !DATA_SOURCE_FILES.includes(file) && !DATA_EXEMPT_FILES.includes(file)) {
|
|
597
|
-
violations.push(
|
|
598
|
-
createPolicyViolation('data', path, 'module data sits in a data-kind file', declaration),
|
|
599
|
-
)
|
|
600
|
-
}
|
|
601
|
-
if (containsFunction && !functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
|
|
602
|
-
violations.push(
|
|
603
|
-
createPolicyViolation(
|
|
604
|
-
'function',
|
|
605
|
-
path,
|
|
606
|
-
'module function syntax sits in a function-kind file',
|
|
607
|
-
declaration,
|
|
608
|
-
),
|
|
609
|
-
)
|
|
610
|
-
}
|
|
611
|
-
if (directFunction && ts.isIdentifier(declaration.name)) {
|
|
612
|
-
violations.push(...inspectPolicyFunctionName(path, file, declaration.name.text, declaration))
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return violations
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Inspect the const, name, and bare-collection rules for constants.ts.
|
|
620
|
-
*
|
|
621
|
-
* @param path - The workspace-relative source path.
|
|
622
|
-
* @param statement - The variable statement to inspect.
|
|
623
|
-
* @returns Every constants.ts syntax violation in declaration order.
|
|
624
|
-
*/
|
|
625
|
-
export function inspectPolicyConstants(
|
|
626
|
-
path: string,
|
|
627
|
-
statement: ts.VariableStatement,
|
|
628
|
-
): readonly PolicyViolation[] {
|
|
629
|
-
const violations: PolicyViolation[] = []
|
|
630
|
-
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
631
|
-
violations.push(
|
|
632
|
-
createPolicyViolation(
|
|
633
|
-
'constant',
|
|
634
|
-
path,
|
|
635
|
-
'constants.ts permits only const declarations',
|
|
636
|
-
statement,
|
|
637
|
-
),
|
|
638
|
-
)
|
|
639
|
-
}
|
|
640
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
641
|
-
if (!ts.isIdentifier(declaration.name) || !/^[A-Z][A-Z0-9_]*$/u.test(declaration.name.text)) {
|
|
642
|
-
violations.push(
|
|
643
|
-
createPolicyViolation(
|
|
644
|
-
'constant',
|
|
645
|
-
path,
|
|
646
|
-
'constants.ts names declarations in UPPER_SNAKE_CASE',
|
|
647
|
-
declaration,
|
|
648
|
-
),
|
|
649
|
-
)
|
|
650
|
-
}
|
|
651
|
-
if (
|
|
652
|
-
declaration.initializer !== undefined &&
|
|
653
|
-
(ts.isArrayLiteralExpression(declaration.initializer) ||
|
|
654
|
-
ts.isObjectLiteralExpression(declaration.initializer))
|
|
655
|
-
) {
|
|
656
|
-
violations.push(
|
|
657
|
-
createPolicyViolation(
|
|
658
|
-
'constant',
|
|
659
|
-
path,
|
|
660
|
-
'constants.ts forbids bare collection literals',
|
|
661
|
-
declaration,
|
|
662
|
-
),
|
|
663
|
-
)
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
return violations
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* Whether a top-level statement declares a centralized symbol.
|
|
671
|
-
*
|
|
672
|
-
* @param statement - The top-level statement to classify.
|
|
673
|
-
* @returns `true` when the statement declares a symbol governed by the export rule.
|
|
674
|
-
*/
|
|
675
|
-
export function isPolicyDeclaration(statement: ts.Statement): boolean {
|
|
676
|
-
return (
|
|
677
|
-
ts.isClassDeclaration(statement) ||
|
|
678
|
-
ts.isEnumDeclaration(statement) ||
|
|
679
|
-
ts.isFunctionDeclaration(statement) ||
|
|
680
|
-
ts.isInterfaceDeclaration(statement) ||
|
|
681
|
-
ts.isModuleDeclaration(statement) ||
|
|
682
|
-
ts.isTypeAliasDeclaration(statement) ||
|
|
683
|
-
ts.isVariableStatement(statement)
|
|
684
|
-
)
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Inspect one source file against the fleet's syntactic placement register.
|
|
689
|
-
*
|
|
690
|
-
* @param source - The path and TypeScript text to inspect.
|
|
691
|
-
* @returns Every syntactic placement violation in source order.
|
|
692
|
-
*/
|
|
693
|
-
export function inspectPolicySource(source: PolicySource): readonly PolicyViolation[] {
|
|
694
|
-
const path = normalizePolicyPath(source.path)
|
|
695
|
-
const file = basename(path)
|
|
696
|
-
const syntax = ts.createSourceFile(path, source.content, ts.ScriptTarget.Latest, true)
|
|
697
|
-
const violations: PolicyViolation[] = []
|
|
698
|
-
const functionDomain = isFunctionDomainPath(path)
|
|
699
|
-
const domainNames = FUNCTION_DOMAIN_FOLDERS.map((folder) => basename(folder))
|
|
700
|
-
|
|
701
|
-
if (domainNames.includes(basename(file, '.ts'))) {
|
|
702
|
-
violations.push(
|
|
703
|
-
createPolicyViolation(
|
|
704
|
-
'domain',
|
|
705
|
-
path,
|
|
706
|
-
'a registered function domain is a folder rather than a source file',
|
|
707
|
-
),
|
|
708
|
-
)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
for (const statement of syntax.statements) {
|
|
712
|
-
if (
|
|
713
|
-
CENTRAL_SOURCE_FILES.includes(file) &&
|
|
714
|
-
isPolicyDeclaration(statement) &&
|
|
715
|
-
!hasPolicyModifier(statement, ts.SyntaxKind.ExportKeyword)
|
|
716
|
-
) {
|
|
717
|
-
violations.push(
|
|
718
|
-
createPolicyViolation(
|
|
719
|
-
'export',
|
|
720
|
-
path,
|
|
721
|
-
'every centralized declaration is exported',
|
|
722
|
-
statement,
|
|
723
|
-
),
|
|
724
|
-
)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (
|
|
728
|
-
(ts.isEnumDeclaration(statement) ||
|
|
729
|
-
ts.isInterfaceDeclaration(statement) ||
|
|
730
|
-
ts.isModuleDeclaration(statement) ||
|
|
731
|
-
ts.isTypeAliasDeclaration(statement)) &&
|
|
732
|
-
file !== 'types.ts'
|
|
733
|
-
) {
|
|
734
|
-
violations.push(
|
|
735
|
-
createPolicyViolation('type', path, 'type declarations sit in types.ts', statement),
|
|
736
|
-
)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (ts.isFunctionDeclaration(statement)) {
|
|
740
|
-
if (!functionDomain && !FUNCTION_SOURCE_FILES.includes(file)) {
|
|
741
|
-
violations.push(
|
|
742
|
-
createPolicyViolation(
|
|
743
|
-
'function',
|
|
744
|
-
path,
|
|
745
|
-
'module functions sit in a function-kind file',
|
|
746
|
-
statement,
|
|
747
|
-
),
|
|
748
|
-
)
|
|
749
|
-
}
|
|
750
|
-
violations.push(...inspectPolicyFunctionName(path, file, statement.name?.text, statement))
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if (ts.isVariableStatement(statement)) {
|
|
754
|
-
violations.push(...inspectPolicyVariables(path, file, statement, functionDomain))
|
|
755
|
-
if (file === 'constants.ts') violations.push(...inspectPolicyConstants(path, statement))
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if (ts.isClassDeclaration(statement)) {
|
|
759
|
-
const expected = basename(file, '.ts')
|
|
760
|
-
if (
|
|
761
|
-
file !== 'errors.ts' &&
|
|
762
|
-
(!/^[A-Z][A-Za-z0-9]*\.ts$/u.test(file) || statement.name?.text !== expected)
|
|
763
|
-
) {
|
|
764
|
-
violations.push(
|
|
765
|
-
createPolicyViolation(
|
|
766
|
-
'class',
|
|
767
|
-
path,
|
|
768
|
-
'classes sit in their matching implementation or errors file',
|
|
769
|
-
statement,
|
|
770
|
-
),
|
|
771
|
-
)
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (functionDomain) violations.push(...inspectFunctionDomain(path, syntax))
|
|
777
|
-
return violations
|
|
318
|
+
return line === undefined ? { rule, path, message } : { rule, path, line, message }
|
|
778
319
|
}
|
|
779
320
|
|
|
780
321
|
/**
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* @param sources - The TypeScript files to inspect.
|
|
784
|
-
* @returns Every syntactic placement violation in source order.
|
|
785
|
-
*/
|
|
786
|
-
export function inspectPolicySources(sources: readonly PolicySource[]): readonly PolicyViolation[] {
|
|
787
|
-
const violations: PolicyViolation[] = []
|
|
788
|
-
for (const source of sources) violations.push(...inspectPolicySource(source))
|
|
789
|
-
return violations
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Read the parsed TypeScript source population beneath one workspace.
|
|
794
|
-
*
|
|
795
|
-
* @param root - The workspace root to read.
|
|
796
|
-
* @param glob - The population to read. Default: the src and app placement population.
|
|
797
|
-
* @returns Every non-ambient TypeScript source the population names, sorted by path.
|
|
798
|
-
*/
|
|
799
|
-
export function readPolicySources(
|
|
800
|
-
root: string,
|
|
801
|
-
glob: string | readonly string[] = POLICY_SOURCE_GLOB,
|
|
802
|
-
): readonly PolicySource[] {
|
|
803
|
-
return globSync(glob, { cwd: root })
|
|
804
|
-
.map(normalizePolicyPath)
|
|
805
|
-
.filter((path) => !POLICY_AMBIENT_SUFFIXES.some((suffix) => basename(path).endsWith(suffix)))
|
|
806
|
-
.sort()
|
|
807
|
-
.map((path) => ({
|
|
808
|
-
path,
|
|
809
|
-
content: readFileSync(join(root, path), 'utf8'),
|
|
810
|
-
}))
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Derive the extensionless module stem for one mirrored module test.
|
|
322
|
+
* Derives the extensionless module stem for one mirrored module test.
|
|
815
323
|
*
|
|
816
324
|
* @param path - The workspace-relative test path.
|
|
817
325
|
* @returns The extensionless module stem, or `undefined` for a reserved scope test.
|
|
@@ -824,7 +332,7 @@ export function testToPolicyStem(path: string): string | undefined {
|
|
|
824
332
|
}
|
|
825
333
|
|
|
826
334
|
/**
|
|
827
|
-
*
|
|
335
|
+
* Lists every module name a registered language resolves for a stem.
|
|
828
336
|
*
|
|
829
337
|
* @param stem - The extensionless workspace-relative module stem.
|
|
830
338
|
* @returns Direct modules, partial modules, then a matching tests-axis setup module.
|
|
@@ -842,7 +350,7 @@ export function stemToPolicyCandidates(stem: string): readonly string[] {
|
|
|
842
350
|
}
|
|
843
351
|
|
|
844
352
|
/**
|
|
845
|
-
*
|
|
353
|
+
* Inspects mirrored test paths against an explicit module-path population.
|
|
846
354
|
*
|
|
847
355
|
* @param tests - The module-test paths to inspect.
|
|
848
356
|
* @param modules - The existing module paths in every registered language.
|
|
@@ -872,7 +380,7 @@ export function inspectPolicyMirrorPaths(
|
|
|
872
380
|
}
|
|
873
381
|
|
|
874
382
|
/**
|
|
875
|
-
*
|
|
383
|
+
* Inspects every mirrored module test beneath one workspace.
|
|
876
384
|
*
|
|
877
385
|
* @param root - The workspace root to inspect.
|
|
878
386
|
* @returns Every missing mirror violation in test-path order.
|
|
@@ -887,7 +395,7 @@ export function inspectPolicyMirrors(root: string): readonly PolicyViolation[] {
|
|
|
887
395
|
}
|
|
888
396
|
|
|
889
397
|
/**
|
|
890
|
-
*
|
|
398
|
+
* Inspects code-shaped workspace files for lint suppression directives.
|
|
891
399
|
*
|
|
892
400
|
* @param root - The workspace root to inspect.
|
|
893
401
|
* @returns Every suppression occurrence in path and line order.
|
|
@@ -900,12 +408,14 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
|
|
|
900
408
|
for (let index = 0; index < lines.length; index += 1) {
|
|
901
409
|
const line = lines[index]
|
|
902
410
|
if (line !== undefined && POLICY_SUPPRESSION_PATTERN.test(line)) {
|
|
903
|
-
violations.push(
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
411
|
+
violations.push(
|
|
412
|
+
createPolicyViolation(
|
|
413
|
+
'suppression',
|
|
414
|
+
path,
|
|
415
|
+
'file carries a lint suppression directive',
|
|
416
|
+
index + 1,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
909
419
|
}
|
|
910
420
|
}
|
|
911
421
|
}
|
|
@@ -913,23 +423,22 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
|
|
|
913
423
|
}
|
|
914
424
|
|
|
915
425
|
/**
|
|
916
|
-
*
|
|
426
|
+
* Inspects the lint configuration that keeps policy rules active across the workspace.
|
|
917
427
|
*
|
|
918
428
|
* @param configuration - The parsed Oxlint configuration to inspect.
|
|
919
429
|
* @returns Every wiring violation in rule and configuration order.
|
|
920
430
|
*/
|
|
921
431
|
export function inspectPolicyConfiguration(configuration: unknown): readonly string[] {
|
|
922
432
|
const violations: string[] = []
|
|
923
|
-
if (
|
|
433
|
+
if (!isPolicyRecord(configuration)) {
|
|
924
434
|
return ['Oxlint configuration must be a record']
|
|
925
435
|
}
|
|
926
436
|
|
|
927
437
|
const rules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
|
|
928
438
|
for (const rule of POLICY_WIRING_RULES) {
|
|
929
|
-
const setting =
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
: undefined
|
|
439
|
+
const setting = isPolicyRecord(rules)
|
|
440
|
+
? Object.getOwnPropertyDescriptor(rules, rule)?.value
|
|
441
|
+
: undefined
|
|
933
442
|
const severity = Array.isArray(setting) ? setting[0] : setting
|
|
934
443
|
if (severity !== 'error') violations.push(`${rule} must have top-level error severity`)
|
|
935
444
|
}
|
|
@@ -960,17 +469,12 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
|
|
|
960
469
|
violations.push('overrides must be an array when declared')
|
|
961
470
|
} else if (Array.isArray(overrides)) {
|
|
962
471
|
for (const override of overrides) {
|
|
963
|
-
if (
|
|
472
|
+
if (!isPolicyRecord(override)) {
|
|
964
473
|
violations.push('override entries must be records')
|
|
965
474
|
continue
|
|
966
475
|
}
|
|
967
476
|
const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
|
|
968
|
-
if (
|
|
969
|
-
overrideRules === undefined ||
|
|
970
|
-
typeof overrideRules !== 'object' ||
|
|
971
|
-
overrideRules === null ||
|
|
972
|
-
Array.isArray(overrideRules)
|
|
973
|
-
) {
|
|
477
|
+
if (!isPolicyRecord(overrideRules)) {
|
|
974
478
|
continue
|
|
975
479
|
}
|
|
976
480
|
for (const rule of POLICY_WIRING_RULES) {
|
|
@@ -985,7 +489,56 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
|
|
|
985
489
|
}
|
|
986
490
|
|
|
987
491
|
/**
|
|
988
|
-
*
|
|
492
|
+
* Inspects the lint configuration that keeps every named rule and population wired.
|
|
493
|
+
*
|
|
494
|
+
* @param configuration - The parsed Oxlint configuration to inspect.
|
|
495
|
+
* @param rules - Every rule id that some top-level or override rules record must enable.
|
|
496
|
+
* @param populations - Every glob population some override's files list must declare exactly.
|
|
497
|
+
* @returns One violation line per unenabled rule, then one per undeclared population.
|
|
498
|
+
*/
|
|
499
|
+
export function inspectPolicyWiring(
|
|
500
|
+
configuration: unknown,
|
|
501
|
+
rules: readonly string[],
|
|
502
|
+
populations: ReadonlyArray<readonly string[]>,
|
|
503
|
+
): readonly string[] {
|
|
504
|
+
if (!isPolicyRecord(configuration)) {
|
|
505
|
+
return ['Oxlint configuration must be a record']
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const records: unknown[] = [Object.getOwnPropertyDescriptor(configuration, 'rules')?.value]
|
|
509
|
+
const declaredPopulations: string[] = []
|
|
510
|
+
const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
|
|
511
|
+
if (Array.isArray(overrides)) {
|
|
512
|
+
for (const entry of overrides) {
|
|
513
|
+
if (!isPolicyRecord(entry)) continue
|
|
514
|
+
records.push(Object.getOwnPropertyDescriptor(entry, 'rules')?.value)
|
|
515
|
+
const files: unknown = Object.getOwnPropertyDescriptor(entry, 'files')?.value
|
|
516
|
+
if (Array.isArray(files)) declaredPopulations.push(files.join(' '))
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const enabled = new Set<string>()
|
|
521
|
+
for (const record of records) {
|
|
522
|
+
if (!isPolicyRecord(record)) continue
|
|
523
|
+
for (const name of Object.getOwnPropertyNames(record)) enabled.add(name)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const violations: string[] = []
|
|
527
|
+
for (const rule of rules) {
|
|
528
|
+
if (!enabled.has(rule))
|
|
529
|
+
violations.push(`${rule} is enabled by no top-level or override rules record`)
|
|
530
|
+
}
|
|
531
|
+
for (const population of populations) {
|
|
532
|
+
const joined = population.join(' ')
|
|
533
|
+
if (!declaredPopulations.includes(joined)) {
|
|
534
|
+
violations.push(`no override files list declares the population exactly: ${joined}`)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return violations
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Resolves an exact-case directory beneath a physical root.
|
|
989
542
|
*
|
|
990
543
|
* @param root - The physical directory from which resolution starts.
|
|
991
544
|
* @param path - The relative directory path to resolve.
|
|
@@ -1006,7 +559,7 @@ export function resolvePolicyDirectory(root: string, path: string): string | und
|
|
|
1006
559
|
}
|
|
1007
560
|
|
|
1008
561
|
/**
|
|
1009
|
-
*
|
|
562
|
+
* Reports whether an exact-case path resolves to a regular file beneath a physical root.
|
|
1010
563
|
*
|
|
1011
564
|
* @param root - The physical directory from which resolution starts.
|
|
1012
565
|
* @param path - The relative file path to inspect.
|
|
@@ -1023,7 +576,7 @@ export function isPolicyFile(root: string, path: string): boolean {
|
|
|
1023
576
|
}
|
|
1024
577
|
|
|
1025
578
|
/**
|
|
1026
|
-
*
|
|
579
|
+
* Reads the immediate child directories beneath one workspace-relative path.
|
|
1027
580
|
*
|
|
1028
581
|
* @param root - The workspace root to inspect.
|
|
1029
582
|
* @param path - The workspace-relative parent directory.
|
|
@@ -1039,7 +592,7 @@ export function readPolicyDirectories(root: string, path: string): readonly stri
|
|
|
1039
592
|
}
|
|
1040
593
|
|
|
1041
594
|
/**
|
|
1042
|
-
*
|
|
595
|
+
* Discovers the skill family from immediate directories in the workspace tree.
|
|
1043
596
|
*
|
|
1044
597
|
* @param root - The workspace root to inspect.
|
|
1045
598
|
* @returns The sorted directory names that belong to the skill family.
|
|
@@ -1049,7 +602,7 @@ export function readSkillFamily(root: string): readonly string[] {
|
|
|
1049
602
|
}
|
|
1050
603
|
|
|
1051
604
|
/**
|
|
1052
|
-
*
|
|
605
|
+
* Parses one skill document's frontmatter without interpreting arbitrary body lines as keys.
|
|
1053
606
|
*
|
|
1054
607
|
* @param content - The raw SKILL.md text.
|
|
1055
608
|
* @returns The parsed fields and exact scalar source, or `undefined` for an unsupported shape.
|
|
@@ -1134,7 +687,7 @@ export function parseSkillFrontmatter(content: string): SkillFrontmatter | undef
|
|
|
1134
687
|
}
|
|
1135
688
|
|
|
1136
689
|
/**
|
|
1137
|
-
*
|
|
690
|
+
* Reports whether a description carries a sentence that begins with the case-sensitive word `Use`.
|
|
1138
691
|
*
|
|
1139
692
|
* @param description - The parsed skill description.
|
|
1140
693
|
* @returns True when the description contains the canonical trigger sentence.
|
|
@@ -1144,7 +697,7 @@ export function matchesSkillTrigger(description: string): boolean {
|
|
|
1144
697
|
}
|
|
1145
698
|
|
|
1146
699
|
/**
|
|
1147
|
-
*
|
|
700
|
+
* Reads the direct Markdown files owned by one skill's references directory.
|
|
1148
701
|
*
|
|
1149
702
|
* @param root - The workspace root to inspect.
|
|
1150
703
|
* @param name - The discovered skill directory name.
|
|
@@ -1160,7 +713,7 @@ export function readSkillReferences(root: string, name: string): readonly string
|
|
|
1160
713
|
}
|
|
1161
714
|
|
|
1162
715
|
/**
|
|
1163
|
-
*
|
|
716
|
+
* Creates metadata in the canonical skill interface shape.
|
|
1164
717
|
*
|
|
1165
718
|
* @param name - The skill token the default prompt invokes.
|
|
1166
719
|
* @returns Canonical skill interface metadata ending in one newline.
|
|
@@ -1177,7 +730,7 @@ export function createSkillMetadata(name: string): string {
|
|
|
1177
730
|
}
|
|
1178
731
|
|
|
1179
732
|
/**
|
|
1180
|
-
*
|
|
733
|
+
* Parses the default prompt from the canonical skill interface shape.
|
|
1181
734
|
*
|
|
1182
735
|
* Each value is a non-empty single-quoted scalar in which `''` carries an apostrophe.
|
|
1183
736
|
*
|
|
@@ -1198,7 +751,7 @@ export function parseSkillPrompt(content: string): string | undefined {
|
|
|
1198
751
|
}
|
|
1199
752
|
|
|
1200
753
|
/**
|
|
1201
|
-
*
|
|
754
|
+
* Reports whether a default prompt names one skill's token in complete form.
|
|
1202
755
|
*
|
|
1203
756
|
* A skill directory name is lowercase letters and hyphens, so a match followed by either continues
|
|
1204
757
|
* a longer name and names a different skill.
|
|
@@ -1217,7 +770,7 @@ export function matchesSkillToken(prompt: string, name: string): boolean {
|
|
|
1217
770
|
}
|
|
1218
771
|
|
|
1219
772
|
/**
|
|
1220
|
-
*
|
|
773
|
+
* Extracts the direct Markdown reference paths named in one skill document.
|
|
1221
774
|
*
|
|
1222
775
|
* @param content - The raw SKILL.md text.
|
|
1223
776
|
* @returns Each distinct references/name.md token in sorted order.
|
|
@@ -1278,12 +831,9 @@ export function inspectSkillTemplateTODOs(
|
|
|
1278
831
|
todo !== -1 && todo < end;
|
|
1279
832
|
todo = line.indexOf('TODO', todo + 4)
|
|
1280
833
|
) {
|
|
1281
|
-
violations.push(
|
|
1282
|
-
rule,
|
|
1283
|
-
|
|
1284
|
-
line: index + 1,
|
|
1285
|
-
message: 'skill documents contain no template TODOs',
|
|
1286
|
-
})
|
|
834
|
+
violations.push(
|
|
835
|
+
createPolicyViolation(rule, path, 'skill documents contain no template TODOs', index + 1),
|
|
836
|
+
)
|
|
1287
837
|
}
|
|
1288
838
|
if (openingBacktick === -1) break
|
|
1289
839
|
const closingBacktick = line.indexOf('`', openingBacktick + 1)
|
|
@@ -1294,7 +844,7 @@ export function inspectSkillTemplateTODOs(
|
|
|
1294
844
|
}
|
|
1295
845
|
|
|
1296
846
|
/**
|
|
1297
|
-
*
|
|
847
|
+
* Inspects one discovered skill's required files, metadata, token, and references.
|
|
1298
848
|
*
|
|
1299
849
|
* @param root - The workspace root to inspect.
|
|
1300
850
|
* @param name - The discovered skill directory name.
|
|
@@ -1484,7 +1034,7 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1484
1034
|
}
|
|
1485
1035
|
|
|
1486
1036
|
/**
|
|
1487
|
-
*
|
|
1037
|
+
* Inspects every immediate member of the discovered skill family.
|
|
1488
1038
|
*
|
|
1489
1039
|
* @param root - The workspace root to inspect.
|
|
1490
1040
|
* @returns Every skill-family violation in directory and invariant order.
|
|
@@ -1496,7 +1046,7 @@ export function inspectSkillFamily(root: string): readonly PolicyViolation[] {
|
|
|
1496
1046
|
}
|
|
1497
1047
|
|
|
1498
1048
|
/**
|
|
1499
|
-
*
|
|
1049
|
+
* Inspects one provider bridge against its canonical skill twin.
|
|
1500
1050
|
*
|
|
1501
1051
|
* @param root - The workspace root to inspect.
|
|
1502
1052
|
* @param name - The shared canonical and bridge directory name.
|
|
@@ -1584,7 +1134,7 @@ export function inspectBridge(root: string, name: string): readonly PolicyViolat
|
|
|
1584
1134
|
}
|
|
1585
1135
|
|
|
1586
1136
|
/**
|
|
1587
|
-
*
|
|
1137
|
+
* Inspects the provider bridge set and every bridge shared with the canonical skill family.
|
|
1588
1138
|
*
|
|
1589
1139
|
* @param root - The workspace root to inspect.
|
|
1590
1140
|
* @returns Every bridge-set and bridge-content violation in directory order.
|
|
@@ -1623,7 +1173,7 @@ export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
|
|
|
1623
1173
|
}
|
|
1624
1174
|
|
|
1625
1175
|
/**
|
|
1626
|
-
*
|
|
1176
|
+
* Reads every rule path the root instruction file's rule map registers.
|
|
1627
1177
|
*
|
|
1628
1178
|
* @param content - The raw root instruction text.
|
|
1629
1179
|
* @returns Each backticked first cell beneath the rule map heading, in table order.
|
|
@@ -1643,7 +1193,7 @@ export function readPolicyRuleMap(content: string): readonly string[] {
|
|
|
1643
1193
|
}
|
|
1644
1194
|
|
|
1645
1195
|
/**
|
|
1646
|
-
*
|
|
1196
|
+
* Inspects the discovered rule family against the root instruction file's rule map.
|
|
1647
1197
|
*
|
|
1648
1198
|
* A workspace with no rule file has no rule map to keep, so the population is empty there.
|
|
1649
1199
|
*
|
|
@@ -1679,7 +1229,7 @@ export function inspectPolicyRuleMap(root: string): readonly PolicyViolation[] {
|
|
|
1679
1229
|
}
|
|
1680
1230
|
|
|
1681
1231
|
/**
|
|
1682
|
-
*
|
|
1232
|
+
* Inspects an explicit path population for a name a Windows checkout cannot hold.
|
|
1683
1233
|
*
|
|
1684
1234
|
* Each path is read through its own final segment, because the population lists every directory as
|
|
1685
1235
|
* its own entry. A Windows host refuses the reserved characters and folds a case collision into one
|
|
@@ -1736,7 +1286,7 @@ export function inspectPolicyFilenamePaths(paths: readonly string[]): readonly P
|
|
|
1736
1286
|
}
|
|
1737
1287
|
|
|
1738
1288
|
/**
|
|
1739
|
-
*
|
|
1289
|
+
* Reads the workspace-authored path population, directories included.
|
|
1740
1290
|
*
|
|
1741
1291
|
* @param root - The workspace root to read.
|
|
1742
1292
|
* @returns Every authored path, sorted by path.
|
|
@@ -1746,7 +1296,7 @@ export function readPolicyPaths(root: string): readonly string[] {
|
|
|
1746
1296
|
}
|
|
1747
1297
|
|
|
1748
1298
|
/**
|
|
1749
|
-
*
|
|
1299
|
+
* Inspects the workspace-authored path population for a name a Windows checkout cannot hold.
|
|
1750
1300
|
*
|
|
1751
1301
|
* @param root - The workspace root to inspect.
|
|
1752
1302
|
* @returns Every unusable-name and case-collision violation in path order.
|
|
@@ -1756,7 +1306,7 @@ export function inspectPolicyFilenames(root: string): readonly PolicyViolation[]
|
|
|
1756
1306
|
}
|
|
1757
1307
|
|
|
1758
1308
|
/**
|
|
1759
|
-
*
|
|
1309
|
+
* Parses the manifest's script record without interpreting any other manifest field.
|
|
1760
1310
|
*
|
|
1761
1311
|
* @param content - The raw package.json text.
|
|
1762
1312
|
* @returns Each script name and its command, in manifest order.
|
|
@@ -1769,9 +1319,9 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
|
|
|
1769
1319
|
} catch {
|
|
1770
1320
|
return scripts
|
|
1771
1321
|
}
|
|
1772
|
-
if (
|
|
1322
|
+
if (!isPolicyRecord(manifest)) return scripts
|
|
1773
1323
|
const record: unknown = Object.getOwnPropertyDescriptor(manifest, 'scripts')?.value
|
|
1774
|
-
if (
|
|
1324
|
+
if (!isPolicyRecord(record)) return scripts
|
|
1775
1325
|
for (const name of Object.getOwnPropertyNames(record)) {
|
|
1776
1326
|
const command: unknown = Object.getOwnPropertyDescriptor(record, name)?.value
|
|
1777
1327
|
if (typeof command === 'string') scripts.set(name, command)
|
|
@@ -1780,7 +1330,7 @@ export function parsePolicyScripts(content: string): ReadonlyMap<string, string>
|
|
|
1780
1330
|
}
|
|
1781
1331
|
|
|
1782
1332
|
/**
|
|
1783
|
-
*
|
|
1333
|
+
* Inspects every manifest script for a shell file no Windows host runs.
|
|
1784
1334
|
*
|
|
1785
1335
|
* @param root - The workspace root to inspect.
|
|
1786
1336
|
* @returns Every shell-script violation in manifest order.
|
|
@@ -1804,155 +1354,236 @@ export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
|
|
|
1804
1354
|
}
|
|
1805
1355
|
|
|
1806
1356
|
/**
|
|
1807
|
-
*
|
|
1357
|
+
* Reads every authored Markdown path in one workspace, sorted by path.
|
|
1808
1358
|
*
|
|
1809
|
-
* @
|
|
1810
|
-
*
|
|
1811
|
-
*
|
|
1359
|
+
* @remarks
|
|
1360
|
+
* The walk descends the whole tree apart from the directory names
|
|
1361
|
+
* {@link POLICY_PROSE_EXCLUSIONS} lists, which hold installed packages, built output, scratch
|
|
1362
|
+
* work, and campaign records rather than prose this workspace authors.
|
|
1363
|
+
*
|
|
1364
|
+
* @param root - The workspace root to read.
|
|
1365
|
+
* @returns Every workspace-relative Markdown path, sorted by path.
|
|
1812
1366
|
*/
|
|
1813
|
-
export function
|
|
1814
|
-
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
)
|
|
1367
|
+
export function readPolicyProse(root: string): readonly string[] {
|
|
1368
|
+
const paths: string[] = []
|
|
1369
|
+
const pending: string[] = ['']
|
|
1370
|
+
while (pending.length > 0) {
|
|
1371
|
+
const relative = pending.pop() ?? ''
|
|
1372
|
+
for (const entry of readdirSync(join(root, relative), { withFileTypes: true })) {
|
|
1373
|
+
const path = relative === '' ? entry.name : `${relative}/${entry.name}`
|
|
1374
|
+
if (entry.isDirectory()) {
|
|
1375
|
+
if (!POLICY_PROSE_EXCLUSIONS.includes(entry.name)) pending.push(path)
|
|
1376
|
+
continue
|
|
1377
|
+
}
|
|
1378
|
+
if (entry.name.endsWith('.md')) paths.push(normalizePolicyPath(path))
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return paths.sort()
|
|
1828
1382
|
}
|
|
1829
1383
|
|
|
1830
1384
|
/**
|
|
1831
|
-
*
|
|
1385
|
+
* Reads the short name one workspace manifest declares, its scope removed.
|
|
1832
1386
|
*
|
|
1833
|
-
* @param
|
|
1834
|
-
* @returns
|
|
1387
|
+
* @param root - The workspace root to read.
|
|
1388
|
+
* @returns The manifest name after its scope, or undefined where no manifest declares one.
|
|
1835
1389
|
*/
|
|
1836
|
-
export function
|
|
1837
|
-
return
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1390
|
+
export function readPolicyPackage(root: string): string | undefined {
|
|
1391
|
+
if (!isPolicyFile(root, POLICY_MANIFEST_FILE)) return undefined
|
|
1392
|
+
let manifest: unknown
|
|
1393
|
+
try {
|
|
1394
|
+
manifest = JSON.parse(readFileSync(join(root, POLICY_MANIFEST_FILE), 'utf8'))
|
|
1395
|
+
} catch {
|
|
1396
|
+
return undefined
|
|
1397
|
+
}
|
|
1398
|
+
if (!isPolicyRecord(manifest)) return undefined
|
|
1399
|
+
const name: unknown = Object.getOwnPropertyDescriptor(manifest, 'name')?.value
|
|
1400
|
+
if (typeof name !== 'string') return undefined
|
|
1401
|
+
return name.slice(name.lastIndexOf('/') + 1)
|
|
1843
1402
|
}
|
|
1844
1403
|
|
|
1845
1404
|
/**
|
|
1846
|
-
*
|
|
1405
|
+
* Reads every package short name the catalog table registers.
|
|
1406
|
+
*
|
|
1407
|
+
* @remarks
|
|
1408
|
+
* A workspace holds this file because the `catalog` verb refuses a target that lacks it, and a
|
|
1409
|
+
* workspace that has not received one yet registers no package, so the read yields an empty list
|
|
1410
|
+
* there rather than failing.
|
|
1847
1411
|
*
|
|
1848
|
-
* @param
|
|
1849
|
-
* @returns
|
|
1412
|
+
* @param root - The workspace root to read.
|
|
1413
|
+
* @returns Each catalog row's package short name, its scope removed, in table order.
|
|
1850
1414
|
*/
|
|
1851
|
-
export function
|
|
1852
|
-
if (!
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
1415
|
+
export function readPolicyCatalog(root: string): readonly string[] {
|
|
1416
|
+
if (!isPolicyFile(root, POLICY_CATALOG_FILE)) return []
|
|
1417
|
+
const lines = readFileSync(join(root, POLICY_CATALOG_FILE), 'utf8')
|
|
1418
|
+
.replaceAll('\r\n', '\n')
|
|
1419
|
+
.split('\n')
|
|
1420
|
+
const heading = lines.indexOf(POLICY_CATALOG_HEADING)
|
|
1421
|
+
if (heading === -1) return []
|
|
1422
|
+
const names: string[] = []
|
|
1423
|
+
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
1424
|
+
const line = lines[index]
|
|
1425
|
+
if (line === undefined || line.startsWith('## ')) break
|
|
1426
|
+
const cell = line.match(/^\|\s*`([^`]+)`\s*\|/u)?.[1]
|
|
1427
|
+
if (cell !== undefined) names.push(cell.slice(cell.lastIndexOf('/') + 1))
|
|
1428
|
+
}
|
|
1429
|
+
return names
|
|
1859
1430
|
}
|
|
1860
1431
|
|
|
1861
1432
|
/**
|
|
1862
|
-
*
|
|
1433
|
+
* Reads the guide name one prose path carries when that guide is another package's to account for.
|
|
1863
1434
|
*
|
|
1864
|
-
* @
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1435
|
+
* @remarks
|
|
1436
|
+
* A top-level `guides/<name>.md` path yields its name unless the name is the guide index or this
|
|
1437
|
+
* workspace's own package; every other path yields undefined. {@link isPolicyMirror} and
|
|
1438
|
+
* {@link isPolicyStray} split that name by catalog membership.
|
|
1439
|
+
*
|
|
1440
|
+
* @param root - The workspace root the path belongs to.
|
|
1441
|
+
* @param path - The workspace-relative prose path to read.
|
|
1442
|
+
* @returns The guide's name, or undefined where the path is not a top-level guide another package
|
|
1443
|
+
* could own.
|
|
1867
1444
|
*/
|
|
1868
|
-
export function
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1871
|
-
|
|
1872
|
-
createPolicyViolation(
|
|
1873
|
-
'portability',
|
|
1874
|
-
path,
|
|
1875
|
-
'sources split arrived text before trimming each line',
|
|
1876
|
-
node,
|
|
1877
|
-
),
|
|
1878
|
-
)
|
|
1879
|
-
}
|
|
1880
|
-
if (matchesPolicyTerminator(node) || importsPolicyTerminator(node)) {
|
|
1881
|
-
violations.push(
|
|
1882
|
-
createPolicyViolation(
|
|
1883
|
-
'portability',
|
|
1884
|
-
path,
|
|
1885
|
-
'sources emit a line feed rather than the host line ending',
|
|
1886
|
-
node,
|
|
1887
|
-
),
|
|
1888
|
-
)
|
|
1889
|
-
}
|
|
1890
|
-
ts.forEachChild(node, (child) => {
|
|
1891
|
-
violations.push(...inspectPolicyEndingNode(path, child))
|
|
1892
|
-
})
|
|
1893
|
-
return violations
|
|
1445
|
+
export function readPolicyGuide(root: string, path: string): string | undefined {
|
|
1446
|
+
const name = normalizePolicyPath(path).match(POLICY_MIRROR_PATTERN)?.[1]
|
|
1447
|
+
if (name === undefined || name === POLICY_GUIDE_MAP) return undefined
|
|
1448
|
+
return name === readPolicyPackage(root) ? undefined : name
|
|
1894
1449
|
}
|
|
1895
1450
|
|
|
1896
1451
|
/**
|
|
1897
|
-
*
|
|
1452
|
+
* Reports whether one prose path is a top-level guide the catalog registers to another package.
|
|
1453
|
+
*
|
|
1454
|
+
* @remarks
|
|
1455
|
+
* A mirror is fetched bytes rather than authored prose, so the term sweep leaves it to the package
|
|
1456
|
+
* that wrote it. The catalog table is the evidence, and it is the only evidence: the workspace's own
|
|
1457
|
+
* guide and the guide index are authored here, and a top-level guide the catalog does not register
|
|
1458
|
+
* is a finding rather than a silent exclusion.
|
|
1459
|
+
*
|
|
1460
|
+
* @param root - The workspace root the path belongs to.
|
|
1461
|
+
* @param path - The workspace-relative prose path to judge.
|
|
1462
|
+
* @returns True if the path is a top-level guide the catalog registers to a package other than this
|
|
1463
|
+
* one; false otherwise.
|
|
1464
|
+
*/
|
|
1465
|
+
export function isPolicyMirror(root: string, path: string): boolean {
|
|
1466
|
+
const name = readPolicyGuide(root, path)
|
|
1467
|
+
return name !== undefined && readPolicyCatalog(root).includes(name)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Reports whether one prose path is a top-level guide no evidence accounts for.
|
|
1898
1472
|
*
|
|
1899
|
-
* @param
|
|
1900
|
-
* @
|
|
1473
|
+
* @param root - The workspace root the path belongs to.
|
|
1474
|
+
* @param path - The workspace-relative prose path to judge.
|
|
1475
|
+
* @returns True if the path is a top-level guide that is neither this package's own, nor the index,
|
|
1476
|
+
* nor a catalog row; false otherwise.
|
|
1901
1477
|
*/
|
|
1902
|
-
export function
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
return inspectPolicyEndingNode(path, syntax)
|
|
1478
|
+
export function isPolicyStray(root: string, path: string): boolean {
|
|
1479
|
+
const name = readPolicyGuide(root, path)
|
|
1480
|
+
return name !== undefined && !readPolicyCatalog(root).includes(name)
|
|
1906
1481
|
}
|
|
1907
1482
|
|
|
1908
1483
|
/**
|
|
1909
|
-
*
|
|
1484
|
+
* Inspects every authored Markdown file for a term the substitution table bans unconditionally.
|
|
1485
|
+
*
|
|
1486
|
+
* @remarks
|
|
1487
|
+
* Each file is stripped of its fenced blocks, its inline code spans, its link tags, and its URLs
|
|
1488
|
+
* before the match, by the same reader the comment rule uses, so a term inside one of those regions
|
|
1489
|
+
* is not prose. Stripping holds every offset, so the reported line is the line in the file.
|
|
1490
|
+
*
|
|
1491
|
+
* A top-level guide the catalog registers to another package is a mirror and is skipped, and a
|
|
1492
|
+
* top-level guide no evidence accounts for reports instead, so an exclusion is never silent.
|
|
1910
1493
|
*
|
|
1911
1494
|
* @param root - The workspace root to inspect.
|
|
1912
|
-
* @returns Every
|
|
1495
|
+
* @returns Every unaccounted-guide violation, then every banned-term violation in path and offset
|
|
1496
|
+
* order.
|
|
1913
1497
|
*/
|
|
1914
|
-
export function
|
|
1498
|
+
export function inspectPolicyProse(root: string): readonly PolicyViolation[] {
|
|
1915
1499
|
const violations: PolicyViolation[] = []
|
|
1916
|
-
for (const
|
|
1917
|
-
|
|
1500
|
+
for (const path of readPolicyProse(root)) {
|
|
1501
|
+
if (isPolicyStray(root, path)) {
|
|
1502
|
+
violations.push(
|
|
1503
|
+
createPolicyViolation(
|
|
1504
|
+
'prose',
|
|
1505
|
+
path,
|
|
1506
|
+
"guide is the package's own, the map, or a catalog row",
|
|
1507
|
+
),
|
|
1508
|
+
)
|
|
1509
|
+
}
|
|
1510
|
+
if (isPolicyMirror(root, path)) continue
|
|
1511
|
+
const prose = stripPolicyCode(readFileSync(join(root, path), 'utf8'))
|
|
1512
|
+
for (const hit of textToPolicyHits(prose)) {
|
|
1513
|
+
violations.push(
|
|
1514
|
+
createPolicyViolation(
|
|
1515
|
+
'prose',
|
|
1516
|
+
path,
|
|
1517
|
+
`prose carries no banned term: ${hit.term.term} (${hit.term.replacement})`,
|
|
1518
|
+
prose.slice(0, hit.index).split('\n').length,
|
|
1519
|
+
),
|
|
1520
|
+
)
|
|
1521
|
+
}
|
|
1918
1522
|
}
|
|
1919
1523
|
return violations
|
|
1920
1524
|
}
|
|
1921
1525
|
|
|
1922
1526
|
/**
|
|
1923
|
-
*
|
|
1527
|
+
* Reads every term the substitution table's first column registers.
|
|
1528
|
+
*
|
|
1529
|
+
* @remarks
|
|
1530
|
+
* Each row's first cell carries its terms as code spans, so the read takes the backticked tokens
|
|
1531
|
+
* and drops the parenthetical qualifier a row writes beside one.
|
|
1532
|
+
*
|
|
1533
|
+
* @param content - The raw rule text carrying the substitution table.
|
|
1534
|
+
* @returns Each registered term, in table order.
|
|
1535
|
+
*/
|
|
1536
|
+
export function readPolicyTerms(content: string): readonly string[] {
|
|
1537
|
+
const lines = content.replaceAll('\r\n', '\n').split('\n')
|
|
1538
|
+
const heading = lines.indexOf(POLICY_TERM_HEADING)
|
|
1539
|
+
if (heading === -1) return []
|
|
1540
|
+
const terms: string[] = []
|
|
1541
|
+
for (let index = heading + 1; index < lines.length; index += 1) {
|
|
1542
|
+
const line = lines[index]
|
|
1543
|
+
if (line === undefined || line.startsWith('## ')) break
|
|
1544
|
+
const cell = line.match(/^\|([^|]*)\|/u)?.[1]
|
|
1545
|
+
if (cell === undefined) continue
|
|
1546
|
+
for (const match of cell.matchAll(/`([^`]+)`/gu)) {
|
|
1547
|
+
const term = match[1]
|
|
1548
|
+
if (term !== undefined) terms.push(term)
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return terms
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Inspects every host portability rule across one workspace.
|
|
1924
1556
|
*
|
|
1925
1557
|
* @param root - The workspace root to inspect.
|
|
1926
|
-
* @returns Every rule-map, filename, manifest-script
|
|
1558
|
+
* @returns Every rule-map, filename, and manifest-script violation.
|
|
1927
1559
|
*/
|
|
1928
1560
|
export function inspectPolicyPortability(root: string): readonly PolicyViolation[] {
|
|
1929
1561
|
return [
|
|
1930
1562
|
...inspectPolicyRuleMap(root),
|
|
1931
1563
|
...inspectPolicyFilenames(root),
|
|
1932
1564
|
...inspectPolicyScripts(root),
|
|
1933
|
-
...inspectPolicyEndings(root),
|
|
1934
1565
|
]
|
|
1935
1566
|
}
|
|
1936
1567
|
|
|
1937
1568
|
/**
|
|
1938
|
-
*
|
|
1569
|
+
* Inspects every policy rule across one workspace.
|
|
1939
1570
|
*
|
|
1940
1571
|
* @param root - The workspace root to inspect.
|
|
1941
|
-
* @returns Every
|
|
1572
|
+
* @returns Every mirror, suppression, skill, bridge, portability, and prose violation.
|
|
1942
1573
|
*/
|
|
1943
1574
|
export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
|
|
1944
1575
|
return [
|
|
1945
|
-
...inspectPolicySources(readPolicySources(root)),
|
|
1946
1576
|
...inspectPolicyMirrors(root),
|
|
1947
1577
|
...inspectPolicySuppressions(root),
|
|
1948
1578
|
...inspectSkillFamily(root),
|
|
1949
1579
|
...inspectSkillBridges(root),
|
|
1950
1580
|
...inspectPolicyPortability(root),
|
|
1581
|
+
...inspectPolicyProse(root),
|
|
1951
1582
|
]
|
|
1952
1583
|
}
|
|
1953
1584
|
|
|
1954
1585
|
/**
|
|
1955
|
-
*
|
|
1586
|
+
* Writes a control to a real temporary workspace and runs the production sweep over it.
|
|
1956
1587
|
*
|
|
1957
1588
|
* The control's rule selects the sweep: `skill` inspects the canonical family, `bridge` inspects
|
|
1958
1589
|
* provider bridges, and every other rule inspects the whole workspace route.
|
|
@@ -1979,7 +1610,7 @@ export function inspectPolicyControl(control: PolicyControl): readonly PolicyVio
|
|
|
1979
1610
|
}
|
|
1980
1611
|
}
|
|
1981
1612
|
|
|
1982
|
-
/**
|
|
1613
|
+
/** Lists the physical negative controls, one for each rule the sweep claims to enforce. */
|
|
1983
1614
|
export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
1984
1615
|
{
|
|
1985
1616
|
label: 'rejects a suppression directive in a scanned source file',
|
|
@@ -2003,96 +1634,6 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2003
1634
|
},
|
|
2004
1635
|
],
|
|
2005
1636
|
},
|
|
2006
|
-
{
|
|
2007
|
-
label: 'rejects a type outside types.ts',
|
|
2008
|
-
membership: 'top-level type declarations whose filename is not types.ts',
|
|
2009
|
-
rule: 'type',
|
|
2010
|
-
files: [{ path: 'src/mobile/helpers.ts', content: 'export interface ValueInterface {}\n' }],
|
|
2011
|
-
},
|
|
2012
|
-
{
|
|
2013
|
-
label: 'rejects an inline function in routes.ts',
|
|
2014
|
-
membership: 'module-level function syntax whose filename is absent from the function register',
|
|
2015
|
-
rule: 'function',
|
|
2016
|
-
files: [
|
|
2017
|
-
{
|
|
2018
|
-
path: 'src/worker/routes.ts',
|
|
2019
|
-
content: 'export const ROUTES = Object.freeze([{ handler: () => undefined }])\n',
|
|
2020
|
-
},
|
|
2021
|
-
],
|
|
2022
|
-
},
|
|
2023
|
-
{
|
|
2024
|
-
label: 'rejects data in handlers.ts',
|
|
2025
|
-
membership: 'module data whose filename is absent from the data register',
|
|
2026
|
-
rule: 'data',
|
|
2027
|
-
files: [{ path: 'app/edge/handlers.ts', content: "export const STATUS = 'ready'\n" }],
|
|
2028
|
-
},
|
|
2029
|
-
{
|
|
2030
|
-
label: 'rejects a hidden centralized declaration',
|
|
2031
|
-
membership: 'centralized declarations without an export modifier',
|
|
2032
|
-
rule: 'export',
|
|
2033
|
-
files: [{ path: 'src/worker/helpers.ts', content: 'function buildValue(): void {}\n' }],
|
|
2034
|
-
},
|
|
2035
|
-
{
|
|
2036
|
-
label: 'rejects a class that differs from its file',
|
|
2037
|
-
membership: 'class declarations outside errors.ts whose names differ from their filename',
|
|
2038
|
-
rule: 'class',
|
|
2039
|
-
files: [{ path: 'app/desktop/Widget.ts', content: 'export class Other {}\n' }],
|
|
2040
|
-
},
|
|
2041
|
-
{
|
|
2042
|
-
label: 'rejects mutable constants',
|
|
2043
|
-
membership: 'variable statements in constants.ts that are not const',
|
|
2044
|
-
rule: 'constant',
|
|
2045
|
-
files: [{ path: 'src/worker/constants.ts', content: 'export let COUNT = 1\n' }],
|
|
2046
|
-
},
|
|
2047
|
-
{
|
|
2048
|
-
label: 'rejects lower-case constants',
|
|
2049
|
-
membership: 'declarations in constants.ts whose names are not UPPER_SNAKE_CASE',
|
|
2050
|
-
rule: 'constant',
|
|
2051
|
-
files: [{ path: 'src/worker/constants.ts', content: 'export const count = 1\n' }],
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
label: 'rejects bare collection constants',
|
|
2055
|
-
membership: 'declarations in constants.ts with direct array or object literal initializers',
|
|
2056
|
-
rule: 'constant',
|
|
2057
|
-
files: [{ path: 'src/worker/constants.ts', content: 'export const VALUES = []\n' }],
|
|
2058
|
-
},
|
|
2059
|
-
{
|
|
2060
|
-
label: 'rejects a parser without the parse prefix',
|
|
2061
|
-
membership: 'function declarations in parsers.ts whose names do not start with parse',
|
|
2062
|
-
rule: 'parser',
|
|
2063
|
-
files: [{ path: 'app/edge/parsers.ts', content: 'export function coerceValue(): void {}\n' }],
|
|
2064
|
-
},
|
|
2065
|
-
{
|
|
2066
|
-
label: 'rejects a factory without the create prefix',
|
|
2067
|
-
membership: 'function declarations in factories.ts whose names do not start with create',
|
|
2068
|
-
rule: 'factory',
|
|
2069
|
-
files: [{ path: 'app/edge/factories.ts', content: 'export function buildValue(): void {}\n' }],
|
|
2070
|
-
},
|
|
2071
|
-
{
|
|
2072
|
-
label: 'rejects a malformed registered function module',
|
|
2073
|
-
membership: 'direct camelCase modules in a registered function-domain folder',
|
|
2074
|
-
rule: 'domain',
|
|
2075
|
-
files: [
|
|
2076
|
-
{
|
|
2077
|
-
path: 'app/browser/composables/useTheme.ts',
|
|
2078
|
-
content: 'export function useMode(): void {}\n',
|
|
2079
|
-
},
|
|
2080
|
-
],
|
|
2081
|
-
},
|
|
2082
|
-
{
|
|
2083
|
-
label: 'rejects a file named for a function domain',
|
|
2084
|
-
membership: 'source files whose stem is registered as a function-domain folder name',
|
|
2085
|
-
rule: 'domain',
|
|
2086
|
-
files: [{ path: 'app/edge/composables.ts', content: '' }],
|
|
2087
|
-
},
|
|
2088
|
-
{
|
|
2089
|
-
label: 'rejects a function in an unregistered domain',
|
|
2090
|
-
membership: 'function modules whose parent path is absent from the domain register',
|
|
2091
|
-
rule: 'function',
|
|
2092
|
-
files: [
|
|
2093
|
-
{ path: 'src/worker/jobs/runTask.ts', content: 'export function runTask(): void {}\n' },
|
|
2094
|
-
],
|
|
2095
|
-
},
|
|
2096
1637
|
{
|
|
2097
1638
|
label: 'rejects an unmirrored module test',
|
|
2098
1639
|
membership: 'module tests below tests/src or tests/app except integration.test.ts',
|
|
@@ -2137,97 +1678,9 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2137
1678
|
{ path: 'tests/app/core/widget.test.ts', content: '' },
|
|
2138
1679
|
],
|
|
2139
1680
|
},
|
|
2140
|
-
{
|
|
2141
|
-
label: 'rejects a type in a non-ambient env module',
|
|
2142
|
-
membership: 'non-ambient TypeScript modules whose filename is not types.ts',
|
|
2143
|
-
rule: 'type',
|
|
2144
|
-
files: [{ path: 'app/browser/env.ts', content: 'export interface EnvironmentInterface {}\n' }],
|
|
2145
|
-
},
|
|
2146
|
-
{
|
|
2147
|
-
label: 'rejects a property-held arrow in constants.ts',
|
|
2148
|
-
membership: 'module-level function syntax not passed directly as an argument',
|
|
2149
|
-
rule: 'function',
|
|
2150
|
-
files: [
|
|
2151
|
-
{
|
|
2152
|
-
path: 'app/edge/constants.ts',
|
|
2153
|
-
content: 'export const HANDLERS = Object.freeze({ run: () => undefined })\n',
|
|
2154
|
-
},
|
|
2155
|
-
],
|
|
2156
|
-
},
|
|
2157
|
-
{
|
|
2158
|
-
label: 'rejects a callback parameter default function',
|
|
2159
|
-
membership: 'function expressions in callback parameter defaults',
|
|
2160
|
-
rule: 'function',
|
|
2161
|
-
files: [
|
|
2162
|
-
{
|
|
2163
|
-
path: 'app/edge/constants.ts',
|
|
2164
|
-
content: 'export const VALUES = Object.freeze(C.map((c = () => 1) => c))\n',
|
|
2165
|
-
},
|
|
2166
|
-
],
|
|
2167
|
-
},
|
|
2168
|
-
{
|
|
2169
|
-
label: 'rejects a destructured callback parameter default function',
|
|
2170
|
-
membership: 'function expressions in destructured callback parameter defaults',
|
|
2171
|
-
rule: 'function',
|
|
2172
|
-
files: [
|
|
2173
|
-
{
|
|
2174
|
-
path: 'app/edge/constants.ts',
|
|
2175
|
-
content: 'export const VALUES = Object.freeze(C.map(({ f = () => 1 }) => f))\n',
|
|
2176
|
-
},
|
|
2177
|
-
],
|
|
2178
|
-
},
|
|
2179
|
-
{
|
|
2180
|
-
label: 'rejects an assignment inside callback control flow',
|
|
2181
|
-
membership: 'function assignments inside callback control-flow branches',
|
|
2182
|
-
rule: 'function',
|
|
2183
|
-
files: [
|
|
2184
|
-
{
|
|
2185
|
-
path: 'app/edge/constants.ts',
|
|
2186
|
-
content:
|
|
2187
|
-
'export const VALUES = Object.freeze(C.map((c) => { if (c) { const f = () => 1; return f() } return 2 }))\n',
|
|
2188
|
-
},
|
|
2189
|
-
],
|
|
2190
|
-
},
|
|
2191
|
-
{
|
|
2192
|
-
label: 'rejects an assignment inside a direct callback',
|
|
2193
|
-
membership: 'function assignments inside the body of a callback passed directly as an argument',
|
|
2194
|
-
rule: 'function',
|
|
2195
|
-
files: [
|
|
2196
|
-
{
|
|
2197
|
-
path: 'app/edge/constants.ts',
|
|
2198
|
-
content:
|
|
2199
|
-
'export const VALUES = Object.freeze(C.map((c) => { const f = () => c; return f() }))\n',
|
|
2200
|
-
},
|
|
2201
|
-
],
|
|
2202
|
-
},
|
|
2203
|
-
{
|
|
2204
|
-
label: 'rejects a declaration inside a direct callback',
|
|
2205
|
-
membership:
|
|
2206
|
-
'function declarations inside the body of a callback passed directly as an argument',
|
|
2207
|
-
rule: 'function',
|
|
2208
|
-
files: [
|
|
2209
|
-
{
|
|
2210
|
-
path: 'app/edge/constants.ts',
|
|
2211
|
-
content:
|
|
2212
|
-
'export const LABELS = Object.freeze(COLUMNS.map((column) => { function format() { return column.label } return format() }))\n',
|
|
2213
|
-
},
|
|
2214
|
-
],
|
|
2215
|
-
},
|
|
2216
|
-
{
|
|
2217
|
-
label: 'rejects an assignment two direct callbacks down',
|
|
2218
|
-
membership: 'function assignments inside a callback the outer callback passes directly',
|
|
2219
|
-
rule: 'function',
|
|
2220
|
-
files: [
|
|
2221
|
-
{
|
|
2222
|
-
path: 'app/edge/constants.ts',
|
|
2223
|
-
content:
|
|
2224
|
-
'export const VALUES = Object.freeze(C.map((c) => wrap((d) => { const g = () => d; return g() })))\n',
|
|
2225
|
-
},
|
|
2226
|
-
],
|
|
2227
|
-
},
|
|
2228
1681
|
])
|
|
2229
1682
|
|
|
2230
|
-
/**
|
|
1683
|
+
/** Lists the physical in-family controls for every skill-family assertion class. */
|
|
2231
1684
|
export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2232
1685
|
{
|
|
2233
1686
|
label: 'rejects a SKILL.md without frontmatter',
|
|
@@ -2503,7 +1956,7 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2503
1956
|
},
|
|
2504
1957
|
])
|
|
2505
1958
|
|
|
2506
|
-
/**
|
|
1959
|
+
/** Lists the physical controls for provider-bridge assertions. */
|
|
2507
1960
|
export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2508
1961
|
{
|
|
2509
1962
|
label: 'rejects a canonical skill without a provider bridge',
|
|
@@ -2632,7 +2085,9 @@ export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2632
2085
|
},
|
|
2633
2086
|
])
|
|
2634
2087
|
|
|
2635
|
-
/**
|
|
2088
|
+
/**
|
|
2089
|
+
* Describes an in-family skill whose metadata values carry escaped apostrophes, proving they parse.
|
|
2090
|
+
*/
|
|
2636
2091
|
export const SKILL_POLICY_APOSTROPHE: PolicyControl = Object.freeze({
|
|
2637
2092
|
label: 'accepts escaped apostrophes in agents/openai.yaml values',
|
|
2638
2093
|
membership: 'immediate directories beneath .agents/skills',
|
|
@@ -2643,7 +2098,9 @@ export const SKILL_POLICY_APOSTROPHE: PolicyControl = Object.freeze({
|
|
|
2643
2098
|
],
|
|
2644
2099
|
})
|
|
2645
2100
|
|
|
2646
|
-
/**
|
|
2101
|
+
/**
|
|
2102
|
+
* Describes a folded description containing a colon, proving continuation lines do not become keys.
|
|
2103
|
+
*/
|
|
2647
2104
|
export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
|
|
2648
2105
|
label: 'accepts a folded description containing a colon',
|
|
2649
2106
|
membership: 'folded description scalars in discovered skill frontmatter',
|
|
@@ -2658,7 +2115,7 @@ export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
|
|
|
2658
2115
|
],
|
|
2659
2116
|
})
|
|
2660
2117
|
|
|
2661
|
-
/**
|
|
2118
|
+
/** Describes a healthy skill reference whose prose carries the documented backticked TODO form. */
|
|
2662
2119
|
export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
|
|
2663
2120
|
label: 'accepts a backticked TODO in skill prose',
|
|
2664
2121
|
membership: 'TODO occurrences inside matched inline backtick spans in discovered skill documents',
|
|
@@ -2673,7 +2130,7 @@ export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
|
|
|
2673
2130
|
],
|
|
2674
2131
|
})
|
|
2675
2132
|
|
|
2676
|
-
/**
|
|
2133
|
+
/** Describes a healthy skill whose fenced example carries a template-TODO spelling. */
|
|
2677
2134
|
export const SKILL_POLICY_FENCED: PolicyControl = Object.freeze({
|
|
2678
2135
|
label: 'accepts a TODO in a three-space-indented fenced skill example',
|
|
2679
2136
|
membership:
|
|
@@ -2690,7 +2147,7 @@ export const SKILL_POLICY_FENCED: PolicyControl = Object.freeze({
|
|
|
2690
2147
|
],
|
|
2691
2148
|
})
|
|
2692
2149
|
|
|
2693
|
-
/**
|
|
2150
|
+
/** Describes a folded description whose blank scalar line separates its paragraphs. */
|
|
2694
2151
|
export const SKILL_POLICY_PARAGRAPHS: PolicyControl = Object.freeze({
|
|
2695
2152
|
label: 'accepts a folded description containing two paragraphs',
|
|
2696
2153
|
membership: 'folded description scalars in discovered skill frontmatter',
|
|
@@ -2705,7 +2162,9 @@ export const SKILL_POLICY_PARAGRAPHS: PolicyControl = Object.freeze({
|
|
|
2705
2162
|
],
|
|
2706
2163
|
})
|
|
2707
2164
|
|
|
2708
|
-
/**
|
|
2165
|
+
/**
|
|
2166
|
+
* Describes a bridge skill outside the discovered family, used to prove the membership boundary.
|
|
2167
|
+
*/
|
|
2709
2168
|
export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
|
|
2710
2169
|
label: 'excludes .claude/skills from the skill family',
|
|
2711
2170
|
membership: 'directories outside .agents/skills',
|
|
@@ -2714,7 +2173,7 @@ export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
|
|
|
2714
2173
|
})
|
|
2715
2174
|
|
|
2716
2175
|
/**
|
|
2717
|
-
*
|
|
2176
|
+
* Creates root instruction text whose rule map names an explicit rule set.
|
|
2718
2177
|
*
|
|
2719
2178
|
* @param rules - The workspace-relative rule paths the map registers.
|
|
2720
2179
|
* @returns Root instruction text carrying one rule map table.
|
|
@@ -2733,7 +2192,27 @@ export function createPolicyRuleMap(rules: readonly string[]): string {
|
|
|
2733
2192
|
)
|
|
2734
2193
|
}
|
|
2735
2194
|
|
|
2736
|
-
/**
|
|
2195
|
+
/**
|
|
2196
|
+
* Creates catalog agent text whose package table names an explicit package set.
|
|
2197
|
+
*
|
|
2198
|
+
* @param names - The package short names the catalog registers.
|
|
2199
|
+
* @returns Catalog agent text carrying one package table.
|
|
2200
|
+
*/
|
|
2201
|
+
export function createPolicyCatalog(names: readonly string[]): string {
|
|
2202
|
+
return (
|
|
2203
|
+
[
|
|
2204
|
+
'# Orkestrel',
|
|
2205
|
+
'',
|
|
2206
|
+
POLICY_CATALOG_HEADING,
|
|
2207
|
+
'',
|
|
2208
|
+
'| Package | Version |',
|
|
2209
|
+
'| ------- | ------- |',
|
|
2210
|
+
...names.map((name) => `| \`@orkestrel/${name}\` | \`0.0.1\` |`),
|
|
2211
|
+
].join('\n') + '\n'
|
|
2212
|
+
)
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/** Lists the physical controls for every rule-map parity assertion the workspace route reaches. */
|
|
2737
2216
|
export const RULES_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2738
2217
|
{
|
|
2739
2218
|
label: 'rejects a rule file the rule map omits',
|
|
@@ -2763,137 +2242,173 @@ export const RULES_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2763
2242
|
},
|
|
2764
2243
|
])
|
|
2765
2244
|
|
|
2766
|
-
/**
|
|
2767
|
-
export const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2245
|
+
/** Holds the manifest one prose control writes, naming the package its own guide belongs to. */
|
|
2246
|
+
export const PROSE_POLICY_MANIFEST = '{\n\t"name": "@orkestrel/sample"\n}\n'
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Lists the physical controls for every prose-population boundary the workspace route reaches.
|
|
2250
|
+
*
|
|
2251
|
+
* @remarks
|
|
2252
|
+
* Each control that attacks an exclusion also writes the arrival file, whose different term proves
|
|
2253
|
+
* the sweep ran over the control workspace rather than reporting nothing because it found nothing.
|
|
2254
|
+
*/
|
|
2255
|
+
export const PROSE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2775
2256
|
{
|
|
2776
|
-
label: 'rejects a
|
|
2777
|
-
membership: '
|
|
2778
|
-
rule: '
|
|
2779
|
-
|
|
2780
|
-
|
|
2257
|
+
label: 'rejects a banned term in the workspace front page',
|
|
2258
|
+
membership: 'authored Markdown outside the excluded directories and the guide mirrors',
|
|
2259
|
+
rule: 'prose',
|
|
2260
|
+
line: 3,
|
|
2261
|
+
message: 'prose carries no banned term: should (must, can, might, or the imperative)',
|
|
2262
|
+
files: [
|
|
2263
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2264
|
+
{ path: 'README.md', content: '# Front page\n\nA reader should meet this term.\n' },
|
|
2265
|
+
],
|
|
2781
2266
|
},
|
|
2782
2267
|
{
|
|
2783
|
-
label: 'rejects a
|
|
2784
|
-
membership: '
|
|
2785
|
-
rule: '
|
|
2786
|
-
|
|
2787
|
-
|
|
2268
|
+
label: 'rejects a banned term in the package guide',
|
|
2269
|
+
membership: 'the top-level guide whose name matches the manifest name',
|
|
2270
|
+
rule: 'prose',
|
|
2271
|
+
line: 3,
|
|
2272
|
+
message: 'prose carries no banned term: should (must, can, might, or the imperative)',
|
|
2273
|
+
files: [
|
|
2274
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2275
|
+
{ path: 'guides/sample.md', content: '# Sample\n\nA reader should meet this term.\n' },
|
|
2276
|
+
],
|
|
2788
2277
|
},
|
|
2789
2278
|
{
|
|
2790
|
-
label: 'rejects a
|
|
2791
|
-
membership: '
|
|
2792
|
-
rule: '
|
|
2793
|
-
|
|
2279
|
+
label: 'rejects a banned term in a rule file',
|
|
2280
|
+
membership: 'authored Markdown below a dot directory the sweep descends into',
|
|
2281
|
+
rule: 'prose',
|
|
2282
|
+
line: 3,
|
|
2283
|
+
message: 'prose carries no banned term: should (must, can, might, or the imperative)',
|
|
2794
2284
|
files: [
|
|
2285
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2795
2286
|
{
|
|
2796
|
-
path:
|
|
2797
|
-
content: '
|
|
2287
|
+
path: `${POLICY_RULE_ROOT}/sample.md`,
|
|
2288
|
+
content: '# Sample\n\nA reader should meet this term.\n',
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
path: POLICY_RULE_MAP_FILE,
|
|
2292
|
+
content: createPolicyRuleMap([`${POLICY_RULE_ROOT}/sample.md`]),
|
|
2798
2293
|
},
|
|
2799
2294
|
],
|
|
2800
2295
|
},
|
|
2801
2296
|
{
|
|
2802
|
-
label: '
|
|
2803
|
-
membership: '
|
|
2804
|
-
rule: '
|
|
2805
|
-
|
|
2297
|
+
label: 'accepts a banned term inside a fenced block',
|
|
2298
|
+
membership: 'fenced regions, whose lines are code rather than prose',
|
|
2299
|
+
rule: 'prose',
|
|
2300
|
+
line: 3,
|
|
2301
|
+
message: 'prose carries no banned term: via (through, by using)',
|
|
2806
2302
|
files: [
|
|
2303
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2304
|
+
{ path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
|
|
2807
2305
|
{
|
|
2808
|
-
path: '
|
|
2809
|
-
content:
|
|
2810
|
-
"export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
|
|
2306
|
+
path: 'guides/README.md',
|
|
2307
|
+
content: '# Index\n\n```text\nshould inside a fence\n```\n',
|
|
2811
2308
|
},
|
|
2812
2309
|
],
|
|
2813
2310
|
},
|
|
2814
2311
|
{
|
|
2815
|
-
label: '
|
|
2816
|
-
membership: '
|
|
2817
|
-
rule: '
|
|
2818
|
-
|
|
2312
|
+
label: 'accepts a banned term inside a code span a line break runs through',
|
|
2313
|
+
membership: 'inline code spans, whose text is a token rather than prose',
|
|
2314
|
+
rule: 'prose',
|
|
2315
|
+
line: 3,
|
|
2316
|
+
message: 'prose carries no banned term: via (through, by using)',
|
|
2819
2317
|
files: [
|
|
2318
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2319
|
+
{ path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
|
|
2820
2320
|
{
|
|
2821
|
-
path: '
|
|
2822
|
-
content:
|
|
2823
|
-
"import * as os from 'node:os'\nexport function endLine(): string {\n\treturn os.EOL\n}\n",
|
|
2321
|
+
path: 'guides/README.md',
|
|
2322
|
+
content: '# Index\n\nA span `that\nspans lines with should` here.\n',
|
|
2824
2323
|
},
|
|
2825
2324
|
],
|
|
2826
2325
|
},
|
|
2827
2326
|
{
|
|
2828
|
-
label: '
|
|
2829
|
-
membership: '
|
|
2830
|
-
rule: '
|
|
2831
|
-
|
|
2327
|
+
label: 'accepts a banned term in a guide the catalog registers to another package',
|
|
2328
|
+
membership: 'top-level guides the catalog registers to a package other than this one',
|
|
2329
|
+
rule: 'prose',
|
|
2330
|
+
line: 3,
|
|
2331
|
+
message: 'prose carries no banned term: via (through, by using)',
|
|
2332
|
+
files: [
|
|
2333
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2334
|
+
{ path: POLICY_CATALOG_FILE, content: createPolicyCatalog(['other', 'sample']) },
|
|
2335
|
+
{ path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
|
|
2336
|
+
{ path: 'guides/other.md', content: '# Other\n\nA reader should meet this term.\n' },
|
|
2337
|
+
],
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
label: 'rejects a top-level guide the catalog does not register',
|
|
2341
|
+
membership: 'top-level guides that are neither this package, nor the index, nor a catalog row',
|
|
2342
|
+
rule: 'prose',
|
|
2343
|
+
message: "guide is the package's own, the map, or a catalog row",
|
|
2832
2344
|
files: [
|
|
2345
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2346
|
+
{ path: POLICY_CATALOG_FILE, content: createPolicyCatalog(['other', 'sample']) },
|
|
2347
|
+
{ path: 'guides/stray.md', content: '# Stray\n\nA reader reads this guide.\n' },
|
|
2348
|
+
],
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
label: 'accepts a banned term inside an installed package',
|
|
2352
|
+
membership: 'Markdown below a directory name the sweep never descends into',
|
|
2353
|
+
rule: 'prose',
|
|
2354
|
+
line: 3,
|
|
2355
|
+
message: 'prose carries no banned term: via (through, by using)',
|
|
2356
|
+
files: [
|
|
2357
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2358
|
+
{ path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
|
|
2833
2359
|
{
|
|
2834
|
-
path: '
|
|
2835
|
-
content:
|
|
2836
|
-
"import { EOL } from 'node:os'\nexport function endLine(): string {\n\treturn EOL\n}\n",
|
|
2360
|
+
path: 'node_modules/sample/README.md',
|
|
2361
|
+
content: '# Installed\n\nA reader should meet this term.\n',
|
|
2837
2362
|
},
|
|
2838
2363
|
],
|
|
2839
2364
|
},
|
|
2365
|
+
{
|
|
2366
|
+
label: 'accepts a banned term inside scratch work',
|
|
2367
|
+
membership: 'Markdown below a directory name the sweep never descends into',
|
|
2368
|
+
rule: 'prose',
|
|
2369
|
+
line: 3,
|
|
2370
|
+
message: 'prose carries no banned term: via (through, by using)',
|
|
2371
|
+
files: [
|
|
2372
|
+
{ path: POLICY_MANIFEST_FILE, content: PROSE_POLICY_MANIFEST },
|
|
2373
|
+
{ path: 'README.md', content: '# Front page\n\nA reader arrives via this term.\n' },
|
|
2374
|
+
{ path: 'tmp/notes.md', content: '# Notes\n\nA reader should meet this term.\n' },
|
|
2375
|
+
],
|
|
2376
|
+
},
|
|
2840
2377
|
])
|
|
2841
2378
|
|
|
2842
|
-
/**
|
|
2843
|
-
export const
|
|
2844
|
-
label: 'excludes a script module from the parsed source axes',
|
|
2845
|
-
membership: 'TypeScript modules outside the src, app, and configs axes',
|
|
2846
|
-
rule: 'portability',
|
|
2847
|
-
files: [
|
|
2848
|
-
{
|
|
2849
|
-
path: 'scripts/read.ts',
|
|
2850
|
-
content:
|
|
2851
|
-
"export function readLines(text: string): readonly string[] {\n\treturn text.trim().split('\\n')\n}\n",
|
|
2852
|
-
},
|
|
2853
|
-
],
|
|
2854
|
-
})
|
|
2855
|
-
|
|
2856
|
-
/** A locally declared line-ending constant, which the host line-ending rule leaves legal. */
|
|
2857
|
-
export const PORTABILITY_POLICY_LOCAL: PolicyControl = Object.freeze({
|
|
2858
|
-
label: 'accepts a locally declared EOL constant',
|
|
2859
|
-
membership: 'EOL identifiers that neither read a binding named os nor import from node:os',
|
|
2860
|
-
rule: 'portability',
|
|
2861
|
-
files: [{ path: 'src/worker/constants.ts', content: "export const EOL = '\\n'\n" }],
|
|
2862
|
-
})
|
|
2863
|
-
|
|
2864
|
-
/** A split on the host-independent line-ending pattern, which the split rule leaves legal. */
|
|
2865
|
-
export const PORTABILITY_POLICY_SPLIT: PolicyControl = Object.freeze({
|
|
2866
|
-
label: 'accepts a split on the line-ending pattern',
|
|
2867
|
-
membership: 'split calls whose argument is not a line-feed string literal',
|
|
2868
|
-
rule: 'portability',
|
|
2869
|
-
files: [
|
|
2870
|
-
{
|
|
2871
|
-
path: 'src/worker/helpers.ts',
|
|
2872
|
-
content:
|
|
2873
|
-
'export function readLines(text: string): readonly string[] {\n\treturn text.trim().split(/\\r\\n|\\n/u)\n}\n',
|
|
2874
|
-
},
|
|
2875
|
-
],
|
|
2876
|
-
})
|
|
2877
|
-
|
|
2878
|
-
/** A differently shaped workspace with app, browser, and worker environments but no core. */
|
|
2879
|
-
export const GENERIC_POLICY_SOURCES: readonly PolicySource[] = Object.freeze([
|
|
2379
|
+
/** Lists the physical controls for every portability assertion the workspace route reaches. */
|
|
2380
|
+
export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2880
2381
|
{
|
|
2881
|
-
|
|
2882
|
-
|
|
2382
|
+
label: 'rejects a reserved device name',
|
|
2383
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2384
|
+
rule: 'portability',
|
|
2385
|
+
message: 'path segments avoid the names Windows reserves',
|
|
2386
|
+
files: [{ path: 'src/worker/con.ts', content: '' }],
|
|
2883
2387
|
},
|
|
2884
|
-
{ path: 'src/worker/Worker.ts', content: 'export class Worker {}\n' },
|
|
2885
2388
|
{
|
|
2886
|
-
|
|
2887
|
-
|
|
2389
|
+
label: 'rejects a segment that ends with a dot',
|
|
2390
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2391
|
+
rule: 'portability',
|
|
2392
|
+
message: 'path segments end with neither a dot nor a space',
|
|
2393
|
+
files: [{ path: 'src/worker/helpers.ts.', content: '' }],
|
|
2888
2394
|
},
|
|
2889
|
-
{ path: 'app/browser/handlers.ts', content: 'export function open(): void {}\n' },
|
|
2890
2395
|
{
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2396
|
+
label: 'rejects a segment that ends with a space',
|
|
2397
|
+
membership: 'path segments in the workspace-authored path population',
|
|
2398
|
+
rule: 'portability',
|
|
2399
|
+
message: 'path segments end with neither a dot nor a space',
|
|
2400
|
+
files: [{ path: 'src/worker/helpers.ts ', content: '' }],
|
|
2894
2401
|
},
|
|
2895
2402
|
{
|
|
2896
|
-
|
|
2897
|
-
|
|
2403
|
+
label: 'rejects a shell script named by a manifest script',
|
|
2404
|
+
membership: 'string values beneath the manifest scripts record',
|
|
2405
|
+
rule: 'portability',
|
|
2406
|
+
message: 'manifest scripts name no .sh file: prepare',
|
|
2407
|
+
files: [
|
|
2408
|
+
{
|
|
2409
|
+
path: POLICY_MANIFEST_FILE,
|
|
2410
|
+
content: '{\n\t"scripts": {\n\t\t"prepare": "bash scripts/prepare.sh"\n\t}\n}\n',
|
|
2411
|
+
},
|
|
2412
|
+
],
|
|
2898
2413
|
},
|
|
2899
2414
|
])
|