@orkestrel/scaffold 0.0.72 → 0.0.74
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/dist/bin/main.js +75 -34
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/skills/orkestrel-prove-journey/SKILL.md +175 -29
- package/dist/host/agents/skills/orkestrel-prove-journey/references/captures.md +42 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/decide.md +34 -15
- package/dist/host/agents/skills/orkestrel-prove-journey/references/layer.md +248 -52
- package/dist/host/agents/skills/orkestrel-prove-journey/references/statechart.md +255 -49
- package/dist/host/agents/skills/orkestrel-prove-journey/references/styles.md +111 -23
- package/dist/host/agents/transports/codex.md +7 -0
- package/dist/host/claude/agents/orkestrel.md +46 -46
- package/dist/host/claude/rules/documentation.md +2 -0
- package/dist/host/claude/rules/tests.md +5 -3
- package/dist/host/claude/rules/workspace.md +26 -15
- package/dist/host/dotfiles/gitignore +3 -0
- package/dist/host/guides/README.md +5 -0
- package/dist/host/guides/scaffold.md +97 -13
- package/dist/host/guides/test.md +788 -206
- package/dist/host/manifest.json +22 -32
- package/dist/host/tests/config.test.ts +272 -121
- package/dist/host/tests/policy.test.ts +11 -1
- package/dist/host/tests/setupPolicy.ts +571 -7
- package/dist/src/core/index.cjs +109 -18
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +19 -6
- package/dist/src/core/index.d.ts +19 -6
- package/dist/src/core/index.js +109 -19
- package/dist/src/core/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
hasCanonicalSegments,
|
|
5
5
|
resolveLink,
|
|
6
6
|
} from '@orkestrel/guide'
|
|
7
|
-
import { HOST_PATHS } from '@orkestrel/scaffold'
|
|
7
|
+
import { BASE_DEV_DEPENDENCIES, HOST_PATHS } from '@orkestrel/scaffold'
|
|
8
8
|
import {
|
|
9
9
|
existsSync,
|
|
10
10
|
globSync,
|
|
@@ -156,6 +156,7 @@ export interface PolicyControl {
|
|
|
156
156
|
readonly directories?: readonly string[]
|
|
157
157
|
readonly line?: number
|
|
158
158
|
readonly message?: string
|
|
159
|
+
readonly violations?: readonly PolicyViolation[]
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/** Describes a contained temporary directory owned by one vendored test. */
|
|
@@ -954,14 +955,423 @@ export function inspectSkillTemplateTODOs(
|
|
|
954
955
|
return violations
|
|
955
956
|
}
|
|
956
957
|
|
|
958
|
+
/** Names why a declaration reading produced no inventory. */
|
|
959
|
+
export type SkillDeclarationRefusal =
|
|
960
|
+
| 'entry'
|
|
961
|
+
| 'form'
|
|
962
|
+
| 'name'
|
|
963
|
+
| 'package'
|
|
964
|
+
| 'specifier'
|
|
965
|
+
| 'syntax'
|
|
966
|
+
| 'target'
|
|
967
|
+
|
|
968
|
+
/** Names what one declaration reading produced. */
|
|
969
|
+
export type SkillDeclarationOutcome = SkillDeclarationRefusal | 'cycle' | 'read'
|
|
970
|
+
|
|
971
|
+
/** Reports one declaration entry's exported names, or the cause that refused the reading. */
|
|
972
|
+
export interface SkillDeclarationResult {
|
|
973
|
+
readonly outcome: SkillDeclarationOutcome
|
|
974
|
+
readonly names: readonly string[]
|
|
975
|
+
readonly detail?: string
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/** Supplies the sentence each refused declaration reading reports, ahead of its own detail. */
|
|
979
|
+
export const SKILL_DECLARATION_MESSAGES: Readonly<Record<SkillDeclarationRefusal, string>> =
|
|
980
|
+
Object.freeze({
|
|
981
|
+
entry: 'has no declaration entry for',
|
|
982
|
+
form: 'has an unsupported declaration form:',
|
|
983
|
+
name: 're-exports a name its target does not declare:',
|
|
984
|
+
package: 'has no installed package',
|
|
985
|
+
specifier: 'is not a supported package entry specifier',
|
|
986
|
+
syntax: 'has a declaration syntax error:',
|
|
987
|
+
target: 'has no declaration file at',
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Reads one package manifest from the directory that holds it.
|
|
992
|
+
*
|
|
993
|
+
* @param directory - The package directory to read the manifest from.
|
|
994
|
+
* @returns The parsed manifest, or `undefined` when the directory holds none.
|
|
995
|
+
* @throws SyntaxError - Thrown when the manifest exists and holds no valid JSON.
|
|
996
|
+
*/
|
|
997
|
+
export function readSkillManifest(directory: string): unknown {
|
|
998
|
+
const path = join(directory, 'package.json')
|
|
999
|
+
if (!existsSync(path)) return undefined
|
|
1000
|
+
const manifest: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
1001
|
+
return manifest
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Reads a public entry's exported declaration names without loading its runtime.
|
|
1006
|
+
*
|
|
1007
|
+
* @param root - The workspace whose own manifest or installed packages supply the declarations.
|
|
1008
|
+
* @param specifier - The public package entry to resolve through its exports map.
|
|
1009
|
+
* @returns The exported names, or the outcome naming why the reading refused.
|
|
1010
|
+
* @remarks Resolves the workspace's own package against `root` when the root manifest carries that
|
|
1011
|
+
* name, and every other package under node_modules, so a checkout reads the package it publishes.
|
|
1012
|
+
* Reads exact exports-map keys through the types, import, and default conditions; a wildcard key,
|
|
1013
|
+
* an array, a source alias, and a runtime-only entry have no declaration entry. The Oxc parser
|
|
1014
|
+
* reads exported function, variable, class, enum, interface, and type declarations, local export
|
|
1015
|
+
* lists, and relative star and named re-exports, including aliases and type forms. Relative .js,
|
|
1016
|
+
* .mjs, and .cjs targets resolve to .d.ts, .d.mts, and .d.cts declarations. This is a name
|
|
1017
|
+
* inventory, not TypeScript semantic validation.
|
|
1018
|
+
*/
|
|
1019
|
+
export function readSkillExports(root: string, specifier: string): SkillDeclarationResult {
|
|
1020
|
+
if (!/^@orkestrel\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/u.test(specifier))
|
|
1021
|
+
return { outcome: 'specifier', names: [] }
|
|
1022
|
+
const segments = specifier.split('/')
|
|
1023
|
+
const key = segments.length === 2 ? '.' : `./${segments.slice(2).join('/')}`
|
|
1024
|
+
const own = readSkillManifest(root)
|
|
1025
|
+
const owned = isPolicyRecord(own) && own['name'] === segments.slice(0, 2).join('/')
|
|
1026
|
+
const directory = owned ? root : join(root, 'node_modules', ...segments.slice(0, 2))
|
|
1027
|
+
const manifest = owned ? own : readSkillManifest(directory)
|
|
1028
|
+
if (!isPolicyRecord(manifest)) return { outcome: 'package', names: [] }
|
|
1029
|
+
const exports = manifest['exports']
|
|
1030
|
+
const target = resolveSkillDeclaration(
|
|
1031
|
+
isPolicyRecord(exports) && Object.keys(exports).some((name) => name.startsWith('.'))
|
|
1032
|
+
? exports[key]
|
|
1033
|
+
: key === '.'
|
|
1034
|
+
? exports
|
|
1035
|
+
: undefined,
|
|
1036
|
+
)
|
|
1037
|
+
if (target === undefined || !target.startsWith('./') || !hasCanonicalSegments(target.slice(2)))
|
|
1038
|
+
return { outcome: 'entry', names: [], detail: key }
|
|
1039
|
+
return readSkillDeclarations(resolve(directory, target))
|
|
1040
|
+
}
|
|
1041
|
+
|
|
957
1042
|
/**
|
|
958
|
-
*
|
|
1043
|
+
* Resolves an explicit declaration target under supported exports-map conditions.
|
|
1044
|
+
*
|
|
1045
|
+
* @param target - The installed exports-map value.
|
|
1046
|
+
* @returns The declaration path, or `undefined` for an unsupported target.
|
|
1047
|
+
*/
|
|
1048
|
+
export function resolveSkillDeclaration(target: unknown): string | undefined {
|
|
1049
|
+
if (typeof target === 'string') return /\.d\.(?:ts|mts|cts)$/u.test(target) ? target : undefined
|
|
1050
|
+
if (!isPolicyRecord(target)) return undefined
|
|
1051
|
+
for (const condition of ['types', 'import', 'default']) {
|
|
1052
|
+
if (Object.hasOwn(target, condition)) return resolveSkillDeclaration(target[condition])
|
|
1053
|
+
}
|
|
1054
|
+
return undefined
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** Enumerates the refusals a declaration file can carry, each written as the form that raises it. */
|
|
1058
|
+
export const SKILL_DECLARATION_REFUSALS = Object.freeze([
|
|
1059
|
+
'export default function read(): void',
|
|
1060
|
+
'declare const value: string\nexport = value',
|
|
1061
|
+
'declare module "foreign" { export const value: string }',
|
|
1062
|
+
'export declare namespace Vocabulary { }',
|
|
1063
|
+
'export declare const value: string\nexport { value as default }',
|
|
1064
|
+
'export * as vocabulary from "./values.js"',
|
|
1065
|
+
'export as namespace Vocabulary',
|
|
1066
|
+
'export * from "foreign"',
|
|
1067
|
+
'export { absent } from "./values.js"',
|
|
1068
|
+
'export * from "./absent.js"',
|
|
1069
|
+
'export const =',
|
|
1070
|
+
])
|
|
1071
|
+
|
|
1072
|
+
/** Describes one planted package entry and the refusal sentence its fenced import reports. */
|
|
1073
|
+
export interface SkillRefusalCase {
|
|
1074
|
+
readonly label: string
|
|
1075
|
+
readonly specifier: string
|
|
1076
|
+
readonly files: readonly PolicySource[]
|
|
1077
|
+
readonly message: string
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Supplies one planted package for each refusal cause a fenced import reports by its own name. */
|
|
1081
|
+
export const SKILL_REFUSAL_CASES: readonly SkillRefusalCase[] = Object.freeze([
|
|
1082
|
+
{
|
|
1083
|
+
label: 'an entry segment outside the specifier grammar',
|
|
1084
|
+
specifier: '@orkestrel/test/Browser',
|
|
1085
|
+
files: [
|
|
1086
|
+
{
|
|
1087
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1088
|
+
content: '{"name":"@orkestrel/test","exports":{".":{"types":"./entry.d.ts"}}}',
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
path: 'node_modules/@orkestrel/test/entry.d.ts',
|
|
1092
|
+
content: 'export declare const VALUE: string\n',
|
|
1093
|
+
},
|
|
1094
|
+
],
|
|
1095
|
+
message:
|
|
1096
|
+
'skill fence import @orkestrel/test/Browser is not a supported package entry specifier',
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
label: 'a package the workspace does not hold',
|
|
1100
|
+
specifier: '@orkestrel/test',
|
|
1101
|
+
files: [],
|
|
1102
|
+
message: 'skill fence import @orkestrel/test has no installed package',
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
label: 'an exports key the map does not declare',
|
|
1106
|
+
specifier: '@orkestrel/test/missing',
|
|
1107
|
+
files: [
|
|
1108
|
+
{
|
|
1109
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1110
|
+
content: '{"name":"@orkestrel/test","exports":{".":{"types":"./entry.d.ts"}}}',
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
path: 'node_modules/@orkestrel/test/entry.d.ts',
|
|
1114
|
+
content: 'export declare const VALUE: string\n',
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
message: 'skill fence import @orkestrel/test/missing has no declaration entry for ./missing',
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
label: 'a wildcard exports key the reader does not expand',
|
|
1121
|
+
specifier: '@orkestrel/test/browser',
|
|
1122
|
+
files: [
|
|
1123
|
+
{
|
|
1124
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1125
|
+
content: '{"name":"@orkestrel/test","exports":{"./*":{"types":"./*.d.ts"}}}',
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
path: 'node_modules/@orkestrel/test/browser.d.ts',
|
|
1129
|
+
content: 'export declare const VALUE: string\n',
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
message: 'skill fence import @orkestrel/test/browser has no declaration entry for ./browser',
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
label: 'a declaration target the package does not hold',
|
|
1136
|
+
specifier: '@orkestrel/test',
|
|
1137
|
+
files: [
|
|
1138
|
+
{
|
|
1139
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1140
|
+
content: '{"name":"@orkestrel/test","exports":{".":{"types":"./entry.d.ts"}}}',
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
message: 'skill fence import @orkestrel/test has no declaration file at entry.d.ts',
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
label: 'a declaration form the reader refuses',
|
|
1147
|
+
specifier: '@orkestrel/test',
|
|
1148
|
+
files: [
|
|
1149
|
+
{
|
|
1150
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1151
|
+
content: '{"name":"@orkestrel/test","exports":{".":{"types":"./entry.d.ts"}}}',
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
path: 'node_modules/@orkestrel/test/entry.d.ts',
|
|
1155
|
+
content: 'export default function read(): void\n',
|
|
1156
|
+
},
|
|
1157
|
+
],
|
|
1158
|
+
message:
|
|
1159
|
+
'skill fence import @orkestrel/test has an unsupported declaration form: export default',
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
label: 'a re-exported name the target does not declare',
|
|
1163
|
+
specifier: '@orkestrel/test',
|
|
1164
|
+
files: [
|
|
1165
|
+
{
|
|
1166
|
+
path: 'node_modules/@orkestrel/test/package.json',
|
|
1167
|
+
content: '{"name":"@orkestrel/test","exports":{".":{"types":"./entry.d.ts"}}}',
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
path: 'node_modules/@orkestrel/test/entry.d.ts',
|
|
1171
|
+
content: "export { absent } from './values.js'\n",
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
path: 'node_modules/@orkestrel/test/values.d.ts',
|
|
1175
|
+
content: 'export declare const VALUE: string\n',
|
|
1176
|
+
},
|
|
1177
|
+
],
|
|
1178
|
+
message:
|
|
1179
|
+
'skill fence import @orkestrel/test re-exports a name its target does not declare: absent',
|
|
1180
|
+
},
|
|
1181
|
+
])
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Reads declaration names while following relative declaration re-exports without execution.
|
|
1185
|
+
*
|
|
1186
|
+
* @param path - The absolute declaration path.
|
|
1187
|
+
* @param ancestors - The declaration paths visited along this branch.
|
|
1188
|
+
* @returns The exported names, or the outcome naming why the reading refused.
|
|
1189
|
+
* @remarks Supports the declaration forms documented on {@link readSkillExports}. A file the branch
|
|
1190
|
+
* already visited reports the `cycle` outcome carrying the names that file declares itself: a named
|
|
1191
|
+
* re-export resolves against those declarations, and a star re-export of a visited file contributes
|
|
1192
|
+
* nothing, because the frame still reading that file contributes its names already. Local export
|
|
1193
|
+
* lists supply their written names; this parser does not typecheck their bindings or resolve
|
|
1194
|
+
* ambiguous star exports.
|
|
1195
|
+
*/
|
|
1196
|
+
export function readSkillDeclarations(
|
|
1197
|
+
path: string,
|
|
1198
|
+
ancestors: readonly string[] = [],
|
|
1199
|
+
): SkillDeclarationResult {
|
|
1200
|
+
if (!existsSync(path)) return { outcome: 'target', names: [], detail: basename(path) }
|
|
1201
|
+
const source = parseSync(path, readFileSync(path, 'utf8'))
|
|
1202
|
+
const [failure] = source.errors
|
|
1203
|
+
if (failure !== undefined) return { outcome: 'syntax', names: [], detail: failure.message }
|
|
1204
|
+
const names = new Set<string>()
|
|
1205
|
+
for (const statement of source.program.body) {
|
|
1206
|
+
if (statement.type === 'ExportDefaultDeclaration')
|
|
1207
|
+
return { outcome: 'form', names: [], detail: 'export default' }
|
|
1208
|
+
if (statement.type === 'TSExportAssignment')
|
|
1209
|
+
return { outcome: 'form', names: [], detail: 'export assignment' }
|
|
1210
|
+
if (statement.type === 'TSNamespaceExportDeclaration')
|
|
1211
|
+
return { outcome: 'form', names: [], detail: 'export as namespace' }
|
|
1212
|
+
if (statement.type === 'TSModuleDeclaration')
|
|
1213
|
+
return { outcome: 'form', names: [], detail: 'ambient module' }
|
|
1214
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.source !== null) continue
|
|
1215
|
+
for (const binding of statement.specifiers) {
|
|
1216
|
+
const name =
|
|
1217
|
+
binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value
|
|
1218
|
+
if (name === 'default') return { outcome: 'form', names: [], detail: 'default export list' }
|
|
1219
|
+
names.add(name)
|
|
1220
|
+
}
|
|
1221
|
+
const declaration = statement.declaration
|
|
1222
|
+
if (declaration === null) continue
|
|
1223
|
+
if (declaration.type === 'VariableDeclaration') {
|
|
1224
|
+
for (const variable of declaration.declarations) {
|
|
1225
|
+
if (variable.id.type !== 'Identifier')
|
|
1226
|
+
return { outcome: 'form', names: [], detail: 'destructured declaration' }
|
|
1227
|
+
names.add(variable.id.name)
|
|
1228
|
+
}
|
|
1229
|
+
} else if (declaration.type === 'TSModuleDeclaration') {
|
|
1230
|
+
return { outcome: 'form', names: [], detail: 'exported namespace' }
|
|
1231
|
+
} else if (
|
|
1232
|
+
declaration.type === 'TSDeclareFunction' ||
|
|
1233
|
+
declaration.type === 'ClassDeclaration' ||
|
|
1234
|
+
declaration.type === 'TSEnumDeclaration' ||
|
|
1235
|
+
declaration.type === 'TSInterfaceDeclaration' ||
|
|
1236
|
+
declaration.type === 'TSTypeAliasDeclaration'
|
|
1237
|
+
) {
|
|
1238
|
+
if (declaration.id === null)
|
|
1239
|
+
return { outcome: 'form', names: [], detail: 'anonymous declaration' }
|
|
1240
|
+
names.add(declaration.id.name)
|
|
1241
|
+
} else return { outcome: 'form', names: [], detail: 'unsupported exported declaration' }
|
|
1242
|
+
}
|
|
1243
|
+
if (ancestors.includes(path)) return { outcome: 'cycle', names: [...names] }
|
|
1244
|
+
for (const statement of source.program.body) {
|
|
1245
|
+
if (statement.type !== 'ExportAllDeclaration' && statement.type !== 'ExportNamedDeclaration')
|
|
1246
|
+
continue
|
|
1247
|
+
const origin = statement.source
|
|
1248
|
+
if (origin === null) continue
|
|
1249
|
+
if (statement.type === 'ExportAllDeclaration' && statement.exported !== null)
|
|
1250
|
+
return { outcome: 'form', names: [], detail: 'star export alias' }
|
|
1251
|
+
if (!origin.value.startsWith('./') && !origin.value.startsWith('../'))
|
|
1252
|
+
return { outcome: 'form', names: [], detail: 'non-relative re-export' }
|
|
1253
|
+
const declaration = /\.d\.(?:ts|mts|cts)$/u.test(origin.value)
|
|
1254
|
+
? origin.value
|
|
1255
|
+
: origin.value.replace(/\.(js|mjs|cjs)$/u, (_, extension: string) =>
|
|
1256
|
+
extension === 'mjs' ? '.d.mts' : extension === 'cjs' ? '.d.cts' : '.d.ts',
|
|
1257
|
+
)
|
|
1258
|
+
if (!/\.d\.(?:ts|mts|cts)$/u.test(declaration))
|
|
1259
|
+
return { outcome: 'form', names: [], detail: 'untyped re-export target' }
|
|
1260
|
+
const targets = readSkillDeclarations(resolve(dirname(path), declaration), [...ancestors, path])
|
|
1261
|
+
if (targets.outcome !== 'read' && targets.outcome !== 'cycle') return targets
|
|
1262
|
+
if (statement.type === 'ExportAllDeclaration') {
|
|
1263
|
+
if (targets.outcome === 'read') for (const name of targets.names) names.add(name)
|
|
1264
|
+
continue
|
|
1265
|
+
}
|
|
1266
|
+
for (const binding of statement.specifiers) {
|
|
1267
|
+
const name =
|
|
1268
|
+
binding.exported.type === 'Identifier' ? binding.exported.name : binding.exported.value
|
|
1269
|
+
const original =
|
|
1270
|
+
binding.local.type === 'Identifier' ? binding.local.name : binding.local.value
|
|
1271
|
+
if (name === 'default') return { outcome: 'form', names: [], detail: 'default re-export' }
|
|
1272
|
+
if (!targets.names.includes(original)) return { outcome: 'name', names: [], detail: original }
|
|
1273
|
+
names.add(name)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { outcome: 'read', names: [...names].sort() }
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Inspects named Orkestrel imports in every Markdown fence against installed declaration exports.
|
|
1281
|
+
*
|
|
1282
|
+
* @param root - The workspace whose installed packages supply the declarations.
|
|
1283
|
+
* @param path - The workspace-relative skill document path reported on a violation.
|
|
1284
|
+
* @param content - The raw Markdown text.
|
|
1285
|
+
* @returns Every unparsed fence, unsupported package, refused declaration, or unexported binding
|
|
1286
|
+
* violation.
|
|
1287
|
+
* @remarks The guide parser supplies fences, including fences nested in lists and blockquotes.
|
|
1288
|
+
* The Oxc parser reads named value and type bindings, aliases, comments, and multiline imports.
|
|
1289
|
+
* A fence the parser refuses reports a violation when its text carries an `@orkestrel/` substring
|
|
1290
|
+
* anywhere, including comments and string literals, because error recovery drops the statements
|
|
1291
|
+
* after the failure. A refused fence without that substring stays outside this check, as prose,
|
|
1292
|
+
* table cells, indented code, default imports,
|
|
1293
|
+
* namespace imports, and imports from other scopes do. A package outside BASE_DEV_DEPENDENCIES
|
|
1294
|
+
* reports a violation, and each refused declaration reading reports the cause
|
|
1295
|
+
* {@link SKILL_DECLARATION_MESSAGES} names. This check proves exported names, not call signatures
|
|
1296
|
+
* or runtime behavior.
|
|
1297
|
+
*/
|
|
1298
|
+
export function inspectSkillImports(
|
|
1299
|
+
root: string,
|
|
1300
|
+
path: string,
|
|
1301
|
+
content: string,
|
|
1302
|
+
): readonly PolicyViolation[] {
|
|
1303
|
+
const violations: PolicyViolation[] = []
|
|
1304
|
+
const entries = new Map<string, SkillDeclarationResult>()
|
|
1305
|
+
for (const fence of createGuide(content).fences()) {
|
|
1306
|
+
const source = parseSync('skill.ts', fence.code)
|
|
1307
|
+
const [failure] = source.errors
|
|
1308
|
+
if (failure !== undefined && fence.code.includes('@orkestrel/')) {
|
|
1309
|
+
violations.push(
|
|
1310
|
+
createPolicyViolation('skill', path, `skill fence could not be parsed: ${failure.message}`),
|
|
1311
|
+
)
|
|
1312
|
+
continue
|
|
1313
|
+
}
|
|
1314
|
+
for (const statement of source.program.body) {
|
|
1315
|
+
if (statement.type !== 'ImportDeclaration') continue
|
|
1316
|
+
const specifier = statement.source.value
|
|
1317
|
+
const bindings = statement.specifiers.filter((binding) => binding.type === 'ImportSpecifier')
|
|
1318
|
+
if (!specifier.startsWith('@orkestrel/') || bindings.length === 0) continue
|
|
1319
|
+
const packageName = specifier.split('/').slice(0, 2).join('/')
|
|
1320
|
+
if (!Object.hasOwn(BASE_DEV_DEPENDENCIES, packageName)) {
|
|
1321
|
+
violations.push(
|
|
1322
|
+
createPolicyViolation(
|
|
1323
|
+
'skill',
|
|
1324
|
+
path,
|
|
1325
|
+
`skill fence import ${specifier} is outside BASE_DEV_DEPENDENCIES: ${packageName}`,
|
|
1326
|
+
),
|
|
1327
|
+
)
|
|
1328
|
+
continue
|
|
1329
|
+
}
|
|
1330
|
+
const cached = entries.get(specifier)
|
|
1331
|
+
const entry = cached ?? readSkillExports(root, specifier)
|
|
1332
|
+
if (cached === undefined) entries.set(specifier, entry)
|
|
1333
|
+
if (entry.outcome !== 'read' && entry.outcome !== 'cycle') {
|
|
1334
|
+
const phrase = SKILL_DECLARATION_MESSAGES[entry.outcome]
|
|
1335
|
+
violations.push(
|
|
1336
|
+
createPolicyViolation(
|
|
1337
|
+
'skill',
|
|
1338
|
+
path,
|
|
1339
|
+
`skill fence import ${specifier} ${entry.detail === undefined ? phrase : `${phrase} ${entry.detail}`}`,
|
|
1340
|
+
),
|
|
1341
|
+
)
|
|
1342
|
+
continue
|
|
1343
|
+
}
|
|
1344
|
+
for (const binding of bindings) {
|
|
1345
|
+
const name =
|
|
1346
|
+
binding.imported.type === 'Identifier' ? binding.imported.name : binding.imported.value
|
|
1347
|
+
if (!entry.names.includes(name)) {
|
|
1348
|
+
violations.push(
|
|
1349
|
+
createPolicyViolation(
|
|
1350
|
+
'skill',
|
|
1351
|
+
path,
|
|
1352
|
+
`skill fence import ${specifier} does not export ${name}`,
|
|
1353
|
+
),
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return violations
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Inspects one discovered skill's required files, metadata, token, references, and fenced imports.
|
|
959
1364
|
*
|
|
960
1365
|
* @param root - The workspace root to inspect.
|
|
961
1366
|
* @param name - The discovered skill directory name.
|
|
1367
|
+
* @param installation - The workspace supplying installed declaration entries. Default: root.
|
|
962
1368
|
* @returns Every skill-family violation in invariant order.
|
|
963
1369
|
*/
|
|
964
|
-
export function inspectSkill(
|
|
1370
|
+
export function inspectSkill(
|
|
1371
|
+
root: string,
|
|
1372
|
+
name: string,
|
|
1373
|
+
installation = root,
|
|
1374
|
+
): readonly PolicyViolation[] {
|
|
965
1375
|
const base = `${SKILL_FAMILY_ROOT}/${name}`
|
|
966
1376
|
const skill = `${base}/SKILL.md`
|
|
967
1377
|
const metadata = `${base}/agents/openai.yaml`
|
|
@@ -1013,6 +1423,7 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1013
1423
|
}
|
|
1014
1424
|
}
|
|
1015
1425
|
violations.push(...inspectSkillTemplateTODOs('skill', skill, content))
|
|
1426
|
+
violations.push(...inspectSkillImports(installation, skill, content))
|
|
1016
1427
|
}
|
|
1017
1428
|
const hasMetadata = isPolicyFile(root, metadata)
|
|
1018
1429
|
if (!hasMetadata) {
|
|
@@ -1056,8 +1467,10 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1056
1467
|
),
|
|
1057
1468
|
)
|
|
1058
1469
|
} else {
|
|
1470
|
+
const referenceContent = readFileSync(join(root, path), 'utf8')
|
|
1059
1471
|
violations.push(
|
|
1060
|
-
...inspectSkillTemplateTODOs('skill', path,
|
|
1472
|
+
...inspectSkillTemplateTODOs('skill', path, referenceContent),
|
|
1473
|
+
...inspectSkillImports(installation, path, referenceContent),
|
|
1061
1474
|
)
|
|
1062
1475
|
}
|
|
1063
1476
|
}
|
|
@@ -1148,11 +1561,13 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1148
1561
|
* Inspects every immediate member of the discovered skill family.
|
|
1149
1562
|
*
|
|
1150
1563
|
* @param root - The workspace root to inspect.
|
|
1564
|
+
* @param installation - The workspace supplying installed declaration entries. Default: root.
|
|
1151
1565
|
* @returns Every skill-family violation in directory and invariant order.
|
|
1152
1566
|
*/
|
|
1153
|
-
export function inspectSkillFamily(root: string): readonly PolicyViolation[] {
|
|
1567
|
+
export function inspectSkillFamily(root: string, installation = root): readonly PolicyViolation[] {
|
|
1154
1568
|
const violations: PolicyViolation[] = []
|
|
1155
|
-
for (const name of readSkillFamily(root))
|
|
1569
|
+
for (const name of readSkillFamily(root))
|
|
1570
|
+
violations.push(...inspectSkill(root, name, installation))
|
|
1156
1571
|
return violations
|
|
1157
1572
|
}
|
|
1158
1573
|
|
|
@@ -2041,7 +2456,7 @@ export function inspectPolicyControl(control: PolicyControl): readonly PolicyVio
|
|
|
2041
2456
|
scratch.write(marker, '')
|
|
2042
2457
|
rmSync(join(scratch.path, marker))
|
|
2043
2458
|
}
|
|
2044
|
-
if (control.rule === 'skill') return inspectSkillFamily(scratch.path)
|
|
2459
|
+
if (control.rule === 'skill') return inspectSkillFamily(scratch.path, process.cwd())
|
|
2045
2460
|
if (control.rule === 'bridge') return inspectSkillBridges(scratch.path)
|
|
2046
2461
|
return inspectPolicyWorkspace(scratch.path)
|
|
2047
2462
|
} finally {
|
|
@@ -2121,6 +2536,155 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2121
2536
|
|
|
2122
2537
|
/** Lists the physical in-family controls for every skill-family assertion class. */
|
|
2123
2538
|
export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
2539
|
+
{
|
|
2540
|
+
label: 'rejects an unexported value binding in a skill fence',
|
|
2541
|
+
membership: 'named value imports in canonical skill fences',
|
|
2542
|
+
rule: 'skill',
|
|
2543
|
+
files: [
|
|
2544
|
+
{
|
|
2545
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2546
|
+
content:
|
|
2547
|
+
SKILL_POLICY_TEXT +
|
|
2548
|
+
'\n```ts\nimport { s2MissingValue } from "@orkestrel/test/browser"\n```\n',
|
|
2549
|
+
},
|
|
2550
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2551
|
+
],
|
|
2552
|
+
violations: [
|
|
2553
|
+
{
|
|
2554
|
+
rule: 'skill',
|
|
2555
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2556
|
+
message: 'skill fence import @orkestrel/test/browser does not export s2MissingValue',
|
|
2557
|
+
},
|
|
2558
|
+
],
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
label: 'rejects an unexported type binding in a skill fence',
|
|
2562
|
+
membership: 'named type imports in canonical skill fences',
|
|
2563
|
+
rule: 'skill',
|
|
2564
|
+
files: [
|
|
2565
|
+
{
|
|
2566
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2567
|
+
content:
|
|
2568
|
+
SKILL_POLICY_TEXT +
|
|
2569
|
+
'\n```ts\nimport type { S2MissingType } from "@orkestrel/test/browser"\n```\n',
|
|
2570
|
+
},
|
|
2571
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2572
|
+
],
|
|
2573
|
+
violations: [
|
|
2574
|
+
{
|
|
2575
|
+
rule: 'skill',
|
|
2576
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2577
|
+
message: 'skill fence import @orkestrel/test/browser does not export S2MissingType',
|
|
2578
|
+
},
|
|
2579
|
+
],
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
label: 'accepts exported value and type bindings from root and browser entries',
|
|
2583
|
+
membership: 'named value and type imports in canonical skill fences',
|
|
2584
|
+
rule: 'skill',
|
|
2585
|
+
files: [
|
|
2586
|
+
{
|
|
2587
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2588
|
+
content:
|
|
2589
|
+
SKILL_POLICY_TEXT +
|
|
2590
|
+
'\n```ts\nimport { waitForCondition, type WaitOptions } from "@orkestrel/test"\nimport { clickAccessible } from "@orkestrel/test/browser"\nimport type { CaptureVariant } from "@orkestrel/test/browser"\n```\n',
|
|
2591
|
+
},
|
|
2592
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2593
|
+
],
|
|
2594
|
+
violations: [],
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
label: 'reports one absent binding beside exported bindings in the same fence',
|
|
2598
|
+
membership: 'named value and type imports in canonical skill fences',
|
|
2599
|
+
rule: 'skill',
|
|
2600
|
+
files: [
|
|
2601
|
+
{
|
|
2602
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2603
|
+
content:
|
|
2604
|
+
SKILL_POLICY_TEXT +
|
|
2605
|
+
'\n```ts\nimport { s2MissingValue, type WaitOptions } from "@orkestrel/test"\nimport { clickAccessible } from "@orkestrel/test/browser"\nimport type { CaptureVariant } from "@orkestrel/test/browser"\n```\n',
|
|
2606
|
+
},
|
|
2607
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2608
|
+
],
|
|
2609
|
+
violations: [
|
|
2610
|
+
{
|
|
2611
|
+
rule: 'skill',
|
|
2612
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2613
|
+
message: 'skill fence import @orkestrel/test does not export s2MissingValue',
|
|
2614
|
+
},
|
|
2615
|
+
],
|
|
2616
|
+
},
|
|
2617
|
+
{
|
|
2618
|
+
label: 'accepts a fenced import of the package the inspected workspace itself publishes',
|
|
2619
|
+
membership: 'named imports of the workspace package in canonical skill fences',
|
|
2620
|
+
rule: 'skill',
|
|
2621
|
+
files: [
|
|
2622
|
+
{
|
|
2623
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2624
|
+
content:
|
|
2625
|
+
SKILL_POLICY_TEXT +
|
|
2626
|
+
'\n```ts\nimport { BASE_DEV_DEPENDENCIES } from "@orkestrel/scaffold"\n```\n',
|
|
2627
|
+
},
|
|
2628
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2629
|
+
],
|
|
2630
|
+
violations: [],
|
|
2631
|
+
},
|
|
2632
|
+
{
|
|
2633
|
+
label: 'rejects a skill fence the parser cannot read beside an Orkestrel import',
|
|
2634
|
+
membership: 'canonical skill fences whose text names an Orkestrel specifier',
|
|
2635
|
+
rule: 'skill',
|
|
2636
|
+
files: [
|
|
2637
|
+
{
|
|
2638
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2639
|
+
content:
|
|
2640
|
+
SKILL_POLICY_TEXT +
|
|
2641
|
+
'\n```ts\nimport { waitForCondition } from "@orkestrel/test"\nconst value =\n```\n',
|
|
2642
|
+
},
|
|
2643
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2644
|
+
],
|
|
2645
|
+
},
|
|
2646
|
+
{
|
|
2647
|
+
label: 'rejects a fenced package outside the base dependency set',
|
|
2648
|
+
membership: 'Orkestrel package imports in canonical skill fences',
|
|
2649
|
+
rule: 'skill',
|
|
2650
|
+
files: [
|
|
2651
|
+
{
|
|
2652
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2653
|
+
content:
|
|
2654
|
+
SKILL_POLICY_TEXT + '\n```ts\nimport { isString } from "@orkestrel/contract"\n```\n',
|
|
2655
|
+
},
|
|
2656
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2657
|
+
],
|
|
2658
|
+
violations: [
|
|
2659
|
+
{
|
|
2660
|
+
rule: 'skill',
|
|
2661
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2662
|
+
message:
|
|
2663
|
+
'skill fence import @orkestrel/contract is outside BASE_DEV_DEPENDENCIES: @orkestrel/contract',
|
|
2664
|
+
},
|
|
2665
|
+
],
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
label: 'reports an unexported binding against its named reference file',
|
|
2669
|
+
membership: 'named imports in fences in referenced skill documents',
|
|
2670
|
+
rule: 'skill',
|
|
2671
|
+
files: [
|
|
2672
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_REFERENCE_TEXT },
|
|
2673
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2674
|
+
{
|
|
2675
|
+
path: '.agents/skills/sample/references/example.md',
|
|
2676
|
+
content:
|
|
2677
|
+
'# Example\n\n~~~ts\nimport { s2MissingReference } from "@orkestrel/test/browser"\n~~~\n',
|
|
2678
|
+
},
|
|
2679
|
+
],
|
|
2680
|
+
violations: [
|
|
2681
|
+
{
|
|
2682
|
+
rule: 'skill',
|
|
2683
|
+
path: '.agents/skills/sample/references/example.md',
|
|
2684
|
+
message: 'skill fence import @orkestrel/test/browser does not export s2MissingReference',
|
|
2685
|
+
},
|
|
2686
|
+
],
|
|
2687
|
+
},
|
|
2124
2688
|
{
|
|
2125
2689
|
label: 'rejects a SKILL.md without frontmatter',
|
|
2126
2690
|
membership: 'exact-case regular SKILL.md files in discovered skill directories',
|