halfcode-compiler.xnl 0.2.1 → 0.2.3
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 +53 -0
- package/dist/application-assembly.d.ts +2 -2
- package/dist/application-assembly.js +1 -1
- package/dist/index-96qHPZj2.d.ts +126 -0
- package/dist/{index-FdimP_SL.d.ts → index-Cqh9pEBa.d.ts} +10 -1
- package/dist/{index-DKwcVwK6.d.ts → index-D2mpux6L.d.ts} +1 -1
- package/dist/index-DdRHFKSQ.d.ts +192 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +22 -3
- package/dist/kind-definition.d.ts +1 -1
- package/dist/resource-core-Byhqyadq.d.ts +20 -0
- package/dist/resource-core.d.ts +3 -2
- package/dist/resource-core.js +432 -2
- package/dist/resource-mapping.d.ts +14 -1
- package/dist/resource-mapping.js +2 -2
- package/dist/resource-projection.d.ts +1 -1
- package/dist/skill-capsule.d.ts +3 -2
- package/dist/skill-capsule.js +2 -2
- package/dist/src-25wZPUyX.js +305 -0
- package/dist/src-D1Bmq7Lo.js +1205 -0
- package/dist/src-DCRih45n.js +947 -0
- package/dist/{src-4m42imop.js → src-DRIws6tY.js} +43 -3
- package/dist/system-skills/resource-dsl/KindDefinitions/SkillCapsule/manifest.xnl +21 -0
- package/dist/system-skills/resource-dsl/KindDefinitions/WikiPage/manifest.xnl +10 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/SKILL.metadata.yaml +2 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/SKILL.template.ejs +20 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/manifest.xnl +18 -0
- package/dist/system-skills/resource-dsl/SkillCapsules/System/resource-mappings.xnl +10 -0
- package/dist/system-skills/resource-dsl/WikiPages/documents/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/files/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/index/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/language/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/migration/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/projections/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/WikiPages/skill-capsules/manifest.xnl +6 -0
- package/dist/system-skills/resource-dsl/documents.md +119 -0
- package/dist/system-skills/resource-dsl/examples/function.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/index.md +17 -0
- package/dist/system-skills/resource-dsl/examples/kind-definition.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/resource-mappings.xnl +21 -0
- package/dist/system-skills/resource-dsl/examples/resource-package.xnl +19 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-authoring.xnl +11 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-devops.xnl +12 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-kind-definition.xnl +27 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-resource-dsl.xnl +8 -0
- package/dist/system-skills/resource-dsl/examples/skill-capsule-run.xnl +8 -0
- package/dist/system-skills/resource-dsl/files.md +47 -0
- package/dist/system-skills/resource-dsl/index.md +59 -0
- package/dist/system-skills/resource-dsl/language.md +64 -0
- package/dist/system-skills/resource-dsl/manifest.xnl +10 -0
- package/dist/system-skills/resource-dsl/migration.md +40 -0
- package/dist/system-skills/resource-dsl/projections.md +142 -0
- package/dist/system-skills/resource-dsl/skill-capsules.md +94 -0
- package/dist/system-skills/sys-halfcode-resource-dsl.plan.js +267 -0
- package/package.json +2 -2
- package/dist/index-DqYap2u_.d.ts +0 -81
- package/dist/index-wU-iKJnW.d.ts +0 -38
- package/dist/src-BlKlpg0J.js +0 -180
- package/dist/src-D8xlMdXx.js +0 -561
- package/dist/src-DKkIFGWH.js +0 -507
|
@@ -0,0 +1,1205 @@
|
|
|
1
|
+
import { l as resourceRefToFqn, u as resolveObjectOperationCompilation } from "./src-DRIws6tY.js";
|
|
2
|
+
import { c as unsafeUnicodeCodeUnitIssue, i as planResourceMappings, o as safePathLexicalIssue, r as parseResourceMappings, t as ResourceMappingPathError } from "./src-25wZPUyX.js";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, posix, resolve } from "node:path";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
//#region ../compiler-skill/src/index.ts
|
|
7
|
+
var SkillCapsuleDistributionError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(`${code}: ${message}`);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "SkillCapsuleDistributionError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const SKILL_PROVENANCE_PATH = "references/.halfcode/provenance.json";
|
|
16
|
+
async function compileSkillCapsule(input) {
|
|
17
|
+
validateSkillDescriptor(input.skill);
|
|
18
|
+
const references = input.references ?? [];
|
|
19
|
+
for (const reference of references) validateReferencePath(reference.path);
|
|
20
|
+
const plannedFiles = [atomicTextFile("SKILL.md", renderSkill(input.skill, input.registry, references)), ...references.map((reference) => atomicTextFile(`references/${reference.path}`, reference.content))];
|
|
21
|
+
assertNoTargetCollisions(plannedFiles.map((file) => ({
|
|
22
|
+
targetRelativePath: file.targetRelativePath,
|
|
23
|
+
owner: file.targetRelativePath === "SKILL.md" ? "generated:skill" : `reference:${file.targetRelativePath.slice(11)}`
|
|
24
|
+
})), "SKILL_TARGET_COLLISION");
|
|
25
|
+
await applyAtomicFiles(plannedFiles, input.outputDir, void 0, ["references"]);
|
|
26
|
+
return {
|
|
27
|
+
skillName: input.skill.name,
|
|
28
|
+
outputRoot: input.outputDir,
|
|
29
|
+
files: plannedFiles.map((file) => file.targetRelativePath)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function compileResourceSkillCapsule(input) {
|
|
33
|
+
const capsule = (await planSkillCapsuleDistribution({
|
|
34
|
+
assembly: input.assembly,
|
|
35
|
+
rootSkillFqns: [input.skillFqn],
|
|
36
|
+
schemas: input.schemas
|
|
37
|
+
})).capsules.find((item) => item.identity.fqn === input.skillFqn);
|
|
38
|
+
if (!capsule) failDistribution("SKILL_ROOT_MISSING", `SkillCapsule not found: ${input.skillFqn}`);
|
|
39
|
+
const files = capsule.files.map((file) => ({
|
|
40
|
+
targetRelativePath: file.capsuleRelativePath,
|
|
41
|
+
content: plannedFileBytes(file),
|
|
42
|
+
contentDigest: file.contentDigest
|
|
43
|
+
}));
|
|
44
|
+
await applyAtomicFiles(files, input.outputDir);
|
|
45
|
+
return {
|
|
46
|
+
skillName: capsule.identity.name,
|
|
47
|
+
outputRoot: input.outputDir,
|
|
48
|
+
files: files.map((file) => file.targetRelativePath).sort(compareStrings)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function planSkillCapsuleDistribution(input) {
|
|
52
|
+
const graph = resolveSkillClosure(input.assembly, input.rootSkillFqns);
|
|
53
|
+
const identities = new Map(graph.topology.map((fqn) => {
|
|
54
|
+
return [fqn, skillIdentity(graph.skills.get(fqn))];
|
|
55
|
+
}));
|
|
56
|
+
const outputNames = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const identity of identities.values()) {
|
|
58
|
+
validateSkillOutputName(identity.name);
|
|
59
|
+
const prior = outputNames.get(identity.name);
|
|
60
|
+
if (prior) failDistribution("SKILL_OUTPUT_NAME_DUPLICATE", `SkillCapsules ${prior} and ${identity.fqn} both claim output name ${identity.name}`);
|
|
61
|
+
outputNames.set(identity.name, identity.fqn);
|
|
62
|
+
}
|
|
63
|
+
const capsules = [];
|
|
64
|
+
for (const fqn of graph.topology) {
|
|
65
|
+
const skill = graph.skills.get(fqn);
|
|
66
|
+
const identity = identities.get(fqn);
|
|
67
|
+
const materialized = await planResourceSkillCapsuleFiles(input.assembly, skill, identity, input.schemas);
|
|
68
|
+
const dependencies = skill.dependencies.map((dependency) => identities.get(dependency.fqn)).sort((left, right) => compareStrings(left.fqn, right.fqn));
|
|
69
|
+
const files = materialized.files.map((file) => {
|
|
70
|
+
const targetRelativePath = posix.join(identity.name, file.targetRelativePath);
|
|
71
|
+
return plannedSkillFile({
|
|
72
|
+
skillFqn: identity.fqn,
|
|
73
|
+
skillApiVersion: identity.apiVersion,
|
|
74
|
+
skillVersion: identity.version,
|
|
75
|
+
capsuleRelativePath: file.targetRelativePath,
|
|
76
|
+
targetRelativePath
|
|
77
|
+
}, file);
|
|
78
|
+
}).sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
79
|
+
const capsuleDigest = digestJson({
|
|
80
|
+
format: "halfcode.skill-capsule/v1",
|
|
81
|
+
identity,
|
|
82
|
+
dependencies,
|
|
83
|
+
files: files.map(fileDigestProjection)
|
|
84
|
+
});
|
|
85
|
+
capsules.push(Object.freeze({
|
|
86
|
+
identity,
|
|
87
|
+
dependencies: Object.freeze(dependencies),
|
|
88
|
+
files: Object.freeze(files),
|
|
89
|
+
capsuleDigest
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
const files = capsules.flatMap((capsule) => capsule.files).sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
93
|
+
assertNoTargetCollisions(files.map((file) => ({
|
|
94
|
+
targetRelativePath: file.targetRelativePath,
|
|
95
|
+
owner: `skill:${file.skillFqn}`
|
|
96
|
+
})), "SKILL_CLOSURE_TARGET_COLLISION");
|
|
97
|
+
const roots = [...new Set(input.rootSkillFqns)].sort(compareStrings).map((fqn) => identities.get(fqn));
|
|
98
|
+
const closureDigest = digestJson({
|
|
99
|
+
format: "halfcode.skill-distribution/v1",
|
|
100
|
+
roots,
|
|
101
|
+
topology: graph.topology,
|
|
102
|
+
capsules: capsules.map((capsule) => ({
|
|
103
|
+
identity: capsule.identity,
|
|
104
|
+
capsuleDigest: capsule.capsuleDigest
|
|
105
|
+
}))
|
|
106
|
+
});
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
format: "halfcode.skill-distribution/v1",
|
|
109
|
+
roots: Object.freeze(roots),
|
|
110
|
+
topology: Object.freeze([...graph.topology]),
|
|
111
|
+
capsules: Object.freeze(capsules),
|
|
112
|
+
files: Object.freeze(files),
|
|
113
|
+
closureDigest
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function skillCapsuleDistributionProjection(plan) {
|
|
117
|
+
return {
|
|
118
|
+
format: plan.format,
|
|
119
|
+
roots: plan.roots,
|
|
120
|
+
topology: plan.topology,
|
|
121
|
+
capsules: plan.capsules.map((capsule) => ({
|
|
122
|
+
identity: capsule.identity,
|
|
123
|
+
dependencies: capsule.dependencies,
|
|
124
|
+
files: capsule.files.map((file) => ({
|
|
125
|
+
...fileDigestProjection(file),
|
|
126
|
+
contentBase64: file.contentBase64
|
|
127
|
+
})),
|
|
128
|
+
capsuleDigest: capsule.capsuleDigest
|
|
129
|
+
})),
|
|
130
|
+
closureDigest: plan.closureDigest
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function applySkillCapsuleDistributionPlan(plan, options) {
|
|
134
|
+
assertValidDistributionPlan(plan);
|
|
135
|
+
await applyAtomicFiles(plan.files.map((file) => ({
|
|
136
|
+
targetRelativePath: file.targetRelativePath,
|
|
137
|
+
content: plannedFileBytes(file),
|
|
138
|
+
contentDigest: file.contentDigest
|
|
139
|
+
})), options.outputRoot, options.onStep);
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
outputRoot: options.outputRoot,
|
|
142
|
+
closureDigest: plan.closureDigest,
|
|
143
|
+
installedSkills: Object.freeze(plan.topology.map((fqn) => plan.capsules.find((capsule) => capsule.identity.fqn === fqn).identity)),
|
|
144
|
+
files: Object.freeze(plan.files.map((file) => file.targetRelativePath))
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function planResourceSkillCapsuleFiles(assembly, skill, identity, schemaInput) {
|
|
148
|
+
const schemas = schemaReader(schemaInput);
|
|
149
|
+
const selection = selectSkillResources(assembly, skill);
|
|
150
|
+
const mappings = parseSkillResourceMappings(skill);
|
|
151
|
+
const callableResources = [...selection.functions, ...selection.composedFunctions];
|
|
152
|
+
assertCallableClosure(callableResources, schemas);
|
|
153
|
+
assertObjectOperationArtifactClosure(selection, schemas);
|
|
154
|
+
const generatedTargets = plannedGeneratedTargets(selection, callableResources, mappings);
|
|
155
|
+
assertNoGeneratedTargetCollisions(generatedTargets);
|
|
156
|
+
let resourceMappingPlan;
|
|
157
|
+
try {
|
|
158
|
+
resourceMappingPlan = await planResourceMappings(mappings, {
|
|
159
|
+
moduleRoots: new Map(assembly.modules.map((module) => [module.id, module.resourceRootDir])),
|
|
160
|
+
defaultSourceRoot: skill.source.directory,
|
|
161
|
+
reservedTargetPaths: generatedTargets.map((claim) => claim.targetRelativePath)
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
translateResourceMappingPathError(error);
|
|
165
|
+
}
|
|
166
|
+
const skillName = skillOutputName(skill);
|
|
167
|
+
const skillText = renderEjsTemplate(skill.template.content, {
|
|
168
|
+
skill,
|
|
169
|
+
metadata: Object.freeze({
|
|
170
|
+
...skill.metadata,
|
|
171
|
+
name: skillName
|
|
172
|
+
}),
|
|
173
|
+
references: referenceModel(selection, mappings),
|
|
174
|
+
renderPromptFragment: (fqn) => {
|
|
175
|
+
return selection.promptFragments.find((item) => item.fqn === fqn)?.instruction?.content.trim() ?? "";
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const mappingFiles = await Promise.all(resourceMappingPlan.entries.map(async (entry) => {
|
|
179
|
+
const content = new Uint8Array(await readFile(entry.sourcePath));
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
targetRelativePath: entry.targetRelativePath,
|
|
182
|
+
content,
|
|
183
|
+
contentDigest: digestBytes(content)
|
|
184
|
+
});
|
|
185
|
+
}));
|
|
186
|
+
const payloadFiles = [
|
|
187
|
+
...planReferenceFiles(selection, schemas, mappings),
|
|
188
|
+
...planCallableArtifacts(callableResources, schemas, mappings.callableArtifactsTarget),
|
|
189
|
+
...planObjectOperationArtifacts(selection, schemas),
|
|
190
|
+
...mappingFiles,
|
|
191
|
+
atomicTextFile("SKILL.md", skillText.trimEnd() + "\n")
|
|
192
|
+
].sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
193
|
+
const files = [...payloadFiles, atomicFile(SKILL_PROVENANCE_PATH, canonicalSkillProvenanceBytes(identity, payloadFiles))].sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
194
|
+
assertNoTargetCollisions(files.map((file) => ({
|
|
195
|
+
targetRelativePath: file.targetRelativePath,
|
|
196
|
+
owner: `skill:${skill.fqn}`
|
|
197
|
+
})), "SKILL_TARGET_COLLISION");
|
|
198
|
+
return {
|
|
199
|
+
skillName,
|
|
200
|
+
files: Object.freeze(files)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function resolveSkillClosure(assembly, rootSkillFqns) {
|
|
204
|
+
if (rootSkillFqns.length === 0) failDistribution("SKILL_ROOTS_EMPTY", "at least one root SkillCapsule is required");
|
|
205
|
+
const skills = /* @__PURE__ */ new Map();
|
|
206
|
+
const graph = /* @__PURE__ */ new Map();
|
|
207
|
+
const resolveSkill = (fqn, owner) => {
|
|
208
|
+
const resource = assembly.byFqn.get(fqn);
|
|
209
|
+
if (!resource) failDistribution(owner ? "SKILL_DEPENDENCY_MISSING" : "SKILL_ROOT_MISSING", owner ? `${owner.fqn} dependency resource://${fqn} does not exist` : `SkillCapsule not found: ${fqn}`);
|
|
210
|
+
if (resource.kind !== "SkillCapsule") failDistribution(owner ? "SKILL_DEPENDENCY_KIND_MISMATCH" : "SKILL_ROOT_KIND_MISMATCH", `${owner?.fqn ?? fqn} references ${fqn}, which is ${resource.kind}`);
|
|
211
|
+
return resource;
|
|
212
|
+
};
|
|
213
|
+
const collect = (skill) => {
|
|
214
|
+
if (skills.has(skill.fqn)) return;
|
|
215
|
+
skills.set(skill.fqn, skill);
|
|
216
|
+
const dependencyFqns = [];
|
|
217
|
+
const seen = /* @__PURE__ */ new Set();
|
|
218
|
+
for (const dependency of [...skill.dependencies].sort((left, right) => compareStrings(left.fqn, right.fqn))) {
|
|
219
|
+
let refFqn;
|
|
220
|
+
try {
|
|
221
|
+
refFqn = resourceRefToFqn(dependency.ref);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
failDistribution("SKILL_DEPENDENCY_REF_INVALID", `${skill.fqn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
}
|
|
225
|
+
if (refFqn !== dependency.fqn) failDistribution("SKILL_DEPENDENCY_REF_INVALID", `${skill.fqn} dependency ref ${dependency.ref} does not match projected FQN ${dependency.fqn}`);
|
|
226
|
+
if (seen.has(dependency.fqn)) failDistribution("SKILL_DEPENDENCY_DUPLICATE", `${skill.fqn} declares ${dependency.fqn} more than once`);
|
|
227
|
+
seen.add(dependency.fqn);
|
|
228
|
+
const target = resolveSkill(dependency.fqn, skill);
|
|
229
|
+
if (target.version !== dependency.version) failDistribution("SKILL_DEPENDENCY_VERSION_MISMATCH", `${skill.fqn} dependency ${dependency.ref} expected ${dependency.version} but found ${target.version}`);
|
|
230
|
+
dependencyFqns.push(target.fqn);
|
|
231
|
+
collect(target);
|
|
232
|
+
}
|
|
233
|
+
graph.set(skill.fqn, Object.freeze(dependencyFqns));
|
|
234
|
+
};
|
|
235
|
+
for (const fqn of [...new Set(rootSkillFqns)].sort(compareStrings)) collect(resolveSkill(fqn));
|
|
236
|
+
assertAcyclicSkillGraph(graph);
|
|
237
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
238
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const [fqn, dependencies] of graph) {
|
|
240
|
+
indegree.set(fqn, dependencies.length);
|
|
241
|
+
for (const dependency of dependencies) {
|
|
242
|
+
const values = dependents.get(dependency) ?? [];
|
|
243
|
+
values.push(fqn);
|
|
244
|
+
dependents.set(dependency, values);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const ready = [...indegree].filter(([, count]) => count === 0).map(([fqn]) => fqn).sort(compareStrings);
|
|
248
|
+
const topology = [];
|
|
249
|
+
while (ready.length > 0) {
|
|
250
|
+
const current = ready.shift();
|
|
251
|
+
topology.push(current);
|
|
252
|
+
for (const dependent of [...dependents.get(current) ?? []].sort(compareStrings)) {
|
|
253
|
+
const next = (indegree.get(dependent) ?? 0) - 1;
|
|
254
|
+
indegree.set(dependent, next);
|
|
255
|
+
if (next === 0) {
|
|
256
|
+
ready.push(dependent);
|
|
257
|
+
ready.sort(compareStrings);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (topology.length !== skills.size) failDistribution("SKILL_DEPENDENCY_CYCLE", "dependency graph contains a cycle");
|
|
262
|
+
return {
|
|
263
|
+
skills,
|
|
264
|
+
topology: Object.freeze(topology)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function assertAcyclicSkillGraph(graph) {
|
|
268
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
269
|
+
const visited = /* @__PURE__ */ new Set();
|
|
270
|
+
const stack = [];
|
|
271
|
+
const visit = (fqn) => {
|
|
272
|
+
if (visited.has(fqn)) return;
|
|
273
|
+
if (visiting.has(fqn)) {
|
|
274
|
+
const start = stack.indexOf(fqn);
|
|
275
|
+
failDistribution("SKILL_DEPENDENCY_CYCLE", canonicalCyclePath([...stack.slice(start), fqn]).join(" -> "));
|
|
276
|
+
}
|
|
277
|
+
visiting.add(fqn);
|
|
278
|
+
stack.push(fqn);
|
|
279
|
+
for (const dependency of [...graph.get(fqn) ?? []].sort(compareStrings)) visit(dependency);
|
|
280
|
+
stack.pop();
|
|
281
|
+
visiting.delete(fqn);
|
|
282
|
+
visited.add(fqn);
|
|
283
|
+
};
|
|
284
|
+
for (const fqn of [...graph.keys()].sort(compareStrings)) visit(fqn);
|
|
285
|
+
}
|
|
286
|
+
function canonicalCyclePath(path) {
|
|
287
|
+
const nodes = path.slice(0, -1);
|
|
288
|
+
if (nodes.length <= 1) return [nodes[0], nodes[0]];
|
|
289
|
+
const rotations = nodes.map((_, index) => [...nodes.slice(index), ...nodes.slice(0, index)]);
|
|
290
|
+
rotations.sort((left, right) => compareStrings(left.join("\0"), right.join("\0")));
|
|
291
|
+
return [...rotations[0], rotations[0][0]];
|
|
292
|
+
}
|
|
293
|
+
function skillIdentity(skill) {
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
fqn: skill.fqn,
|
|
296
|
+
name: skillOutputName(skill),
|
|
297
|
+
apiVersion: skill.apiVersion,
|
|
298
|
+
version: skill.version
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function skillOutputName(skill) {
|
|
302
|
+
if (skill.name !== void 0) {
|
|
303
|
+
if (Object.prototype.hasOwnProperty.call(skill.metadata, "name")) failDistribution("SKILL_OUTPUT_NAME_INVALID", `Skill ${skill.fqn} name is owned by XNL and must not be repeated in YAML metadata`);
|
|
304
|
+
const issue = safePathLexicalIssue(skill.name, "segment");
|
|
305
|
+
if (issue) failDistribution("SKILL_OUTPUT_NAME_INVALID", `Skill ${skill.fqn} XNL name must be one safe path segment: ${JSON.stringify(skill.name)} (${issue})`);
|
|
306
|
+
return skill.name;
|
|
307
|
+
}
|
|
308
|
+
if (!Object.prototype.hasOwnProperty.call(skill.metadata, "name")) return lastFqnSegment(skill.fqn);
|
|
309
|
+
const name = skill.metadata.name;
|
|
310
|
+
if (typeof name !== "string") failDistribution("SKILL_OUTPUT_NAME_INVALID", `Skill ${skill.fqn} present YAML name must be a string: ${JSON.stringify(name)}`);
|
|
311
|
+
const issue = safePathLexicalIssue(name, "segment");
|
|
312
|
+
if (issue) failDistribution("SKILL_OUTPUT_NAME_INVALID", `Skill ${skill.fqn} present YAML name must be one safe path segment: ${JSON.stringify(name)} (${issue})`);
|
|
313
|
+
return name;
|
|
314
|
+
}
|
|
315
|
+
function parseSkillResourceMappings(skill) {
|
|
316
|
+
try {
|
|
317
|
+
return parseResourceMappings(skill.resourceMappings?.content ?? "<ResourceMappings #default>", skill.resourceMappings?.absolutePath ?? `${skill.source.logicalPath}#ResourceMappings`);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
translateResourceMappingPathError(error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function translateResourceMappingPathError(error) {
|
|
323
|
+
if (error instanceof ResourceMappingPathError) {
|
|
324
|
+
const role = error.attribute === "to" ? "target" : "source path";
|
|
325
|
+
failDistribution(error.attribute === "to" ? "SKILL_PLAN_TARGET_INVALID" : "SKILL_RESOURCE_MAPPING_INVALID", `ResourceMappings owner ${error.owner} raw ${role} ${JSON.stringify(error.rawPath)} is unsafe: ${error.issue}`);
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
function validateSkillOutputName(name) {
|
|
330
|
+
const issue = safePathLexicalIssue(name, "segment");
|
|
331
|
+
if (issue) failDistribution("SKILL_OUTPUT_NAME_INVALID", `Skill output name must be one safe path segment: ${JSON.stringify(name)} (${issue})`);
|
|
332
|
+
}
|
|
333
|
+
function assertValidDistributionPlan(plan) {
|
|
334
|
+
if (plan.format !== "halfcode.skill-distribution/v1") failDistribution("SKILL_PLAN_FORMAT_INVALID", `unsupported plan format ${String(plan.format)}`);
|
|
335
|
+
if (plan.roots.length === 0) failDistribution("SKILL_PLAN_ROOTS_EMPTY", "at least one root SkillCapsule is required");
|
|
336
|
+
if (new Set(plan.roots.map((root) => root.fqn)).size !== plan.roots.length) failDistribution("SKILL_PLAN_ROOT_INVALID", "roots contain duplicate Skill identities");
|
|
337
|
+
if (new Set(plan.topology).size !== plan.topology.length) failDistribution("SKILL_PLAN_TOPOLOGY_INVALID", "topology contains duplicate Skill FQNs");
|
|
338
|
+
const capsuleByFqn = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const capsule of plan.capsules) {
|
|
340
|
+
validateSkillOutputName(capsule.identity.name);
|
|
341
|
+
if (!capsule.identity.fqn.trim() || !capsule.identity.apiVersion.trim() || !capsule.identity.version.trim()) failDistribution("SKILL_PLAN_IDENTITY_INVALID", "planned Skill identity requires non-empty FQN, apiVersion and version");
|
|
342
|
+
if (capsuleByFqn.has(capsule.identity.fqn)) failDistribution("SKILL_PLAN_CAPSULE_DUPLICATE", `duplicate planned capsule ${capsule.identity.fqn}`);
|
|
343
|
+
capsuleByFqn.set(capsule.identity.fqn, capsule);
|
|
344
|
+
for (const file of capsule.files) {
|
|
345
|
+
validatePlannedFilePath(file, "targetRelativePath");
|
|
346
|
+
validatePlannedFilePath(file, "capsuleRelativePath");
|
|
347
|
+
}
|
|
348
|
+
assertCanonicalSkillEntry(capsule);
|
|
349
|
+
for (const file of capsule.files) {
|
|
350
|
+
assertValidPlannedFile(file);
|
|
351
|
+
if (file.skillFqn !== capsule.identity.fqn || file.skillApiVersion !== capsule.identity.apiVersion || file.skillVersion !== capsule.identity.version) failDistribution("SKILL_PLAN_FILE_OWNER_INVALID", `file ${file.targetRelativePath} has the wrong Skill owner`);
|
|
352
|
+
if (file.targetRelativePath !== posix.join(capsule.identity.name, file.capsuleRelativePath)) failDistribution("SKILL_PLAN_TARGET_INVALID", `file ${file.targetRelativePath} is outside Skill output ${capsule.identity.name}`);
|
|
353
|
+
}
|
|
354
|
+
assertCanonicalSkillProvenance(capsule);
|
|
355
|
+
const expectedCapsuleDigest = digestJson({
|
|
356
|
+
format: "halfcode.skill-capsule/v1",
|
|
357
|
+
identity: capsule.identity,
|
|
358
|
+
dependencies: capsule.dependencies,
|
|
359
|
+
files: capsule.files.map(fileDigestProjection)
|
|
360
|
+
});
|
|
361
|
+
if (capsule.capsuleDigest !== expectedCapsuleDigest) failDistribution("SKILL_PLAN_CAPSULE_DIGEST_INVALID", `capsule digest mismatch for ${capsule.identity.fqn}`);
|
|
362
|
+
}
|
|
363
|
+
if (plan.topology.some((fqn) => !capsuleByFqn.has(fqn)) || plan.topology.length !== plan.capsules.length) failDistribution("SKILL_PLAN_TOPOLOGY_INVALID", "topology and planned capsules do not identify the same closure");
|
|
364
|
+
if (JSON.stringify(plan.capsules.map((capsule) => capsule.identity.fqn)) !== JSON.stringify(plan.topology)) failDistribution("SKILL_PLAN_TOPOLOGY_INVALID", "planned capsules must use dependency-first topology order");
|
|
365
|
+
const topologyIndex = new Map(plan.topology.map((fqn, index) => [fqn, index]));
|
|
366
|
+
for (const capsule of plan.capsules) {
|
|
367
|
+
const seenDependencies = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const dependency of capsule.dependencies) {
|
|
369
|
+
if (seenDependencies.has(dependency.fqn)) failDistribution("SKILL_PLAN_DEPENDENCY_DUPLICATE", `${capsule.identity.fqn} repeats dependency ${dependency.fqn}`);
|
|
370
|
+
seenDependencies.add(dependency.fqn);
|
|
371
|
+
const target = capsuleByFqn.get(dependency.fqn);
|
|
372
|
+
if (!target || JSON.stringify(target.identity) !== JSON.stringify(dependency)) failDistribution("SKILL_PLAN_DEPENDENCY_INVALID", `${capsule.identity.fqn} dependency ${dependency.fqn} is not bound to the planned identity`);
|
|
373
|
+
if ((topologyIndex.get(dependency.fqn) ?? Number.MAX_SAFE_INTEGER) >= topologyIndex.get(capsule.identity.fqn)) failDistribution("SKILL_PLAN_TOPOLOGY_INVALID", `${dependency.fqn} must precede ${capsule.identity.fqn}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const root of plan.roots) {
|
|
377
|
+
const capsule = capsuleByFqn.get(root.fqn);
|
|
378
|
+
if (!capsule || JSON.stringify(capsule.identity) !== JSON.stringify(root)) failDistribution("SKILL_PLAN_ROOT_INVALID", `root identity ${root.fqn} is not in the planned closure`);
|
|
379
|
+
}
|
|
380
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
381
|
+
const visit = (fqn) => {
|
|
382
|
+
if (reachable.has(fqn)) return;
|
|
383
|
+
reachable.add(fqn);
|
|
384
|
+
for (const dependency of capsuleByFqn.get(fqn).dependencies) visit(dependency.fqn);
|
|
385
|
+
};
|
|
386
|
+
for (const root of plan.roots) visit(root.fqn);
|
|
387
|
+
const disconnected = plan.topology.filter((fqn) => !reachable.has(fqn)).sort(compareStrings);
|
|
388
|
+
if (disconnected.length > 0) failDistribution("SKILL_PLAN_CLOSURE_DISCONNECTED", `planned capsules are not reachable from roots: ${disconnected.join(", ")}`);
|
|
389
|
+
for (const file of plan.files) assertValidPlannedFile(file);
|
|
390
|
+
assertNoTargetCollisions(plan.files.map((file) => ({
|
|
391
|
+
targetRelativePath: file.targetRelativePath,
|
|
392
|
+
owner: file.skillFqn
|
|
393
|
+
})), "SKILL_CLOSURE_TARGET_COLLISION");
|
|
394
|
+
const flattened = plan.capsules.flatMap((capsule) => capsule.files).sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
395
|
+
const publicFileClaims = plan.files.map(fileDigestProjection);
|
|
396
|
+
const capsuleFileClaims = flattened.map(fileDigestProjection);
|
|
397
|
+
if (JSON.stringify(publicFileClaims) !== JSON.stringify(capsuleFileClaims)) failDistribution("SKILL_PLAN_FILE_SET_INVALID", "plan files do not match capsule file claims");
|
|
398
|
+
const expectedClosureDigest = digestJson({
|
|
399
|
+
format: "halfcode.skill-distribution/v1",
|
|
400
|
+
roots: plan.roots,
|
|
401
|
+
topology: plan.topology,
|
|
402
|
+
capsules: plan.capsules.map((capsule) => ({
|
|
403
|
+
identity: capsule.identity,
|
|
404
|
+
capsuleDigest: capsule.capsuleDigest
|
|
405
|
+
}))
|
|
406
|
+
});
|
|
407
|
+
if (plan.closureDigest !== expectedClosureDigest) failDistribution("SKILL_PLAN_CLOSURE_DIGEST_INVALID", "closure digest does not match the planned capsules");
|
|
408
|
+
}
|
|
409
|
+
function assertCanonicalSkillEntry(capsule) {
|
|
410
|
+
const canonicalTarget = `${capsule.identity.name}/SKILL.md`;
|
|
411
|
+
const canonical = capsule.files.filter((file) => file.capsuleRelativePath === "SKILL.md" && file.targetRelativePath === canonicalTarget);
|
|
412
|
+
const rootSkillClaims = capsule.files.filter((file) => !file.capsuleRelativePath.includes("/") && file.capsuleRelativePath.toUpperCase() === "SKILL.MD");
|
|
413
|
+
if (capsule.files.length === 0 || canonical.length !== 1 || rootSkillClaims.length !== 1) failDistribution("SKILL_PLAN_REQUIRED_FILE_INVALID", `planned capsule ${capsule.identity.fqn} must contain exactly one canonical SKILL.md at ${canonicalTarget}`);
|
|
414
|
+
}
|
|
415
|
+
function assertValidPlannedFile(file) {
|
|
416
|
+
validatePlannedFilePath(file, "targetRelativePath");
|
|
417
|
+
validatePlannedFilePath(file, "capsuleRelativePath");
|
|
418
|
+
const content = plannedFileBytes(file);
|
|
419
|
+
const compatibilityContent = file.content;
|
|
420
|
+
if (!(compatibilityContent instanceof Uint8Array) || !bytesEqual(content, compatibilityContent)) failDistribution("SKILL_PLAN_CONTENT_DIGEST_INVALID", `compatibility content mismatch for ${file.targetRelativePath}`);
|
|
421
|
+
const actualDigest = digestBytes(content);
|
|
422
|
+
if (file.contentDigest !== actualDigest) failDistribution("SKILL_PLAN_CONTENT_DIGEST_INVALID", `content digest mismatch for ${file.targetRelativePath}`);
|
|
423
|
+
}
|
|
424
|
+
function plannedSkillFile(identity, file) {
|
|
425
|
+
const contentBase64 = Buffer.from(file.content).toString("base64");
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
...identity,
|
|
428
|
+
contentBase64,
|
|
429
|
+
get content() {
|
|
430
|
+
return decodeCanonicalBase64(contentBase64, identity.targetRelativePath);
|
|
431
|
+
},
|
|
432
|
+
contentDigest: file.contentDigest
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function plannedFileBytes(file) {
|
|
436
|
+
return decodeCanonicalBase64(file.contentBase64, file.targetRelativePath);
|
|
437
|
+
}
|
|
438
|
+
function decodeCanonicalBase64(contentBase64, targetRelativePath) {
|
|
439
|
+
if (typeof contentBase64 !== "string") failDistribution("SKILL_PLAN_CONTENT_ENCODING_INVALID", `contentBase64 is required for ${targetRelativePath}`);
|
|
440
|
+
const content = Buffer.from(contentBase64, "base64");
|
|
441
|
+
if (content.toString("base64") !== contentBase64) failDistribution("SKILL_PLAN_CONTENT_ENCODING_INVALID", `contentBase64 is not canonical for ${targetRelativePath}`);
|
|
442
|
+
return new Uint8Array(content);
|
|
443
|
+
}
|
|
444
|
+
function bytesEqual(left, right) {
|
|
445
|
+
if (left.length !== right.length) return false;
|
|
446
|
+
return left.every((byte, index) => byte === right[index]);
|
|
447
|
+
}
|
|
448
|
+
function fileDigestProjection(file) {
|
|
449
|
+
return {
|
|
450
|
+
skillFqn: file.skillFqn,
|
|
451
|
+
skillApiVersion: file.skillApiVersion,
|
|
452
|
+
skillVersion: file.skillVersion,
|
|
453
|
+
capsuleRelativePath: file.capsuleRelativePath,
|
|
454
|
+
targetRelativePath: file.targetRelativePath,
|
|
455
|
+
contentDigest: file.contentDigest
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function atomicTextFile(targetRelativePath, content) {
|
|
459
|
+
const bytes = new TextEncoder().encode(content);
|
|
460
|
+
return Object.freeze({
|
|
461
|
+
targetRelativePath,
|
|
462
|
+
content: bytes,
|
|
463
|
+
contentDigest: digestBytes(bytes)
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function atomicFile(targetRelativePath, content) {
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
targetRelativePath,
|
|
469
|
+
content,
|
|
470
|
+
contentDigest: digestBytes(content)
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function canonicalSkillProvenanceBytes(identity, payloadFiles) {
|
|
474
|
+
const manifest = {
|
|
475
|
+
format: "halfcode.skill-provenance/v1",
|
|
476
|
+
generatedBy: "halfcode.skill-distribution/v1",
|
|
477
|
+
source: {
|
|
478
|
+
fqn: identity.fqn,
|
|
479
|
+
apiVersion: identity.apiVersion,
|
|
480
|
+
version: identity.version
|
|
481
|
+
},
|
|
482
|
+
payloadFiles: payloadFiles.map((file) => ({
|
|
483
|
+
path: file.targetRelativePath,
|
|
484
|
+
contentDigest: file.contentDigest
|
|
485
|
+
})).sort((left, right) => compareStrings(left.path, right.path))
|
|
486
|
+
};
|
|
487
|
+
return new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
488
|
+
}
|
|
489
|
+
function assertCanonicalSkillProvenance(capsule) {
|
|
490
|
+
const manifests = capsule.files.filter((file) => file.capsuleRelativePath === SKILL_PROVENANCE_PATH);
|
|
491
|
+
if (manifests.length !== 1) failDistribution("SKILL_PLAN_PROVENANCE_INVALID", `planned capsule ${capsule.identity.fqn} must contain exactly one canonical ${SKILL_PROVENANCE_PATH}`);
|
|
492
|
+
const manifest = manifests[0];
|
|
493
|
+
const payloadFiles = capsule.files.filter((file) => file !== manifest).map((file) => ({
|
|
494
|
+
targetRelativePath: file.capsuleRelativePath,
|
|
495
|
+
contentDigest: file.contentDigest
|
|
496
|
+
}));
|
|
497
|
+
const expected = canonicalSkillProvenanceBytes(capsule.identity, payloadFiles);
|
|
498
|
+
if (!bytesEqual(plannedFileBytes(manifest), expected)) failDistribution("SKILL_PLAN_PROVENANCE_INVALID", `planned capsule ${capsule.identity.fqn} provenance does not match source identity and payload files`);
|
|
499
|
+
}
|
|
500
|
+
async function readAtomicFiles(root) {
|
|
501
|
+
const files = [];
|
|
502
|
+
const visit = async (directory, prefix) => {
|
|
503
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => compareStrings(left.name, right.name))) {
|
|
504
|
+
const absolutePath = join(directory, entry.name);
|
|
505
|
+
const relativePath = prefix ? posix.join(prefix, entry.name) : entry.name;
|
|
506
|
+
if (entry.isSymbolicLink()) failDistribution("SKILL_PLAN_SYMLINK_UNSUPPORTED", `generated file is a symbolic link: ${relativePath}`);
|
|
507
|
+
if (entry.isDirectory()) await visit(absolutePath, relativePath);
|
|
508
|
+
else if (entry.isFile()) {
|
|
509
|
+
const content = new Uint8Array(await readFile(absolutePath));
|
|
510
|
+
files.push(Object.freeze({
|
|
511
|
+
targetRelativePath: relativePath,
|
|
512
|
+
content,
|
|
513
|
+
contentDigest: digestBytes(content)
|
|
514
|
+
}));
|
|
515
|
+
} else failDistribution("SKILL_PLAN_FILE_TYPE_UNSUPPORTED", `generated entry is not a regular file: ${relativePath}`);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
await visit(root, "");
|
|
519
|
+
return files.sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
520
|
+
}
|
|
521
|
+
async function applyAtomicFiles(files, outputRoot, onStep, directories = []) {
|
|
522
|
+
if (!outputRoot.trim()) failDistribution("SKILL_APPLY_ROOT_INVALID", "outputRoot must be non-empty");
|
|
523
|
+
const outputRootControl = unsafeUnicodeCodeUnitIssue(outputRoot);
|
|
524
|
+
if (outputRootControl) failDistribution("SKILL_APPLY_ROOT_INVALID", `outputRoot is unsafe: ${outputRootControl}`);
|
|
525
|
+
const frozenFiles = files.map((file) => ({
|
|
526
|
+
targetRelativePath: file.targetRelativePath,
|
|
527
|
+
content: new Uint8Array(file.content),
|
|
528
|
+
contentDigest: file.contentDigest
|
|
529
|
+
}));
|
|
530
|
+
for (const file of frozenFiles) {
|
|
531
|
+
validatePlanTarget(file.targetRelativePath);
|
|
532
|
+
if (file.contentDigest !== digestBytes(file.content)) failDistribution("SKILL_PLAN_CONTENT_DIGEST_INVALID", `content digest mismatch for ${file.targetRelativePath}`);
|
|
533
|
+
}
|
|
534
|
+
assertNoTargetCollisions(frozenFiles.map((file) => ({
|
|
535
|
+
targetRelativePath: file.targetRelativePath,
|
|
536
|
+
owner: `atomic-file:${file.targetRelativePath}`
|
|
537
|
+
})), "SKILL_CLOSURE_TARGET_COLLISION");
|
|
538
|
+
for (const directory of directories) validatePlanTarget(directory);
|
|
539
|
+
const liveRoot = resolve(outputRoot);
|
|
540
|
+
const parent = dirname(liveRoot);
|
|
541
|
+
const liveName = basename(liveRoot);
|
|
542
|
+
if (!liveName) failDistribution("SKILL_APPLY_ROOT_INVALID", "outputRoot must not be a filesystem root");
|
|
543
|
+
await mkdir(parent, { recursive: true });
|
|
544
|
+
const stagingRoot = await mkdtemp(join(parent, `.${liveName}.staging-`));
|
|
545
|
+
const backupRoot = join(parent, `.${liveName}.backup-${randomUUID()}`);
|
|
546
|
+
let backupCreated = false;
|
|
547
|
+
let liveInstalled = false;
|
|
548
|
+
try {
|
|
549
|
+
for (const directory of directories) await mkdir(join(stagingRoot, ...directory.split("/")), { recursive: true });
|
|
550
|
+
for (const file of [...frozenFiles].sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath))) {
|
|
551
|
+
const target = join(stagingRoot, ...file.targetRelativePath.split("/"));
|
|
552
|
+
await mkdir(dirname(target), { recursive: true });
|
|
553
|
+
await writeFile(target, file.content);
|
|
554
|
+
}
|
|
555
|
+
await onStep?.("after-stage");
|
|
556
|
+
const stagedFiles = await readAtomicFiles(stagingRoot);
|
|
557
|
+
const expected = [...frozenFiles].sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath));
|
|
558
|
+
if (JSON.stringify(stagedFiles.map(atomicFileProjection)) !== JSON.stringify(expected.map(atomicFileProjection))) failDistribution("SKILL_APPLY_READBACK_INVALID", "staged files do not match the frozen plan");
|
|
559
|
+
await onStep?.("after-readback");
|
|
560
|
+
if (await pathExists(liveRoot)) {
|
|
561
|
+
await rename(liveRoot, backupRoot);
|
|
562
|
+
backupCreated = true;
|
|
563
|
+
await onStep?.("after-backup");
|
|
564
|
+
}
|
|
565
|
+
await onStep?.("before-live-rename");
|
|
566
|
+
await rename(stagingRoot, liveRoot);
|
|
567
|
+
liveInstalled = true;
|
|
568
|
+
if (backupCreated) await rm(backupRoot, {
|
|
569
|
+
recursive: true,
|
|
570
|
+
force: true
|
|
571
|
+
});
|
|
572
|
+
} catch (error) {
|
|
573
|
+
try {
|
|
574
|
+
if (backupCreated) {
|
|
575
|
+
if (liveInstalled || await pathExists(liveRoot)) await rm(liveRoot, {
|
|
576
|
+
recursive: true,
|
|
577
|
+
force: true
|
|
578
|
+
});
|
|
579
|
+
await rename(backupRoot, liveRoot);
|
|
580
|
+
}
|
|
581
|
+
await rm(stagingRoot, {
|
|
582
|
+
recursive: true,
|
|
583
|
+
force: true
|
|
584
|
+
});
|
|
585
|
+
} catch (rollbackError) {
|
|
586
|
+
throw new SkillCapsuleDistributionError("SKILL_APPLY_ROLLBACK_FAILED", `${error instanceof Error ? error.message : String(error)}; rollback: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
587
|
+
}
|
|
588
|
+
throw error;
|
|
589
|
+
} finally {
|
|
590
|
+
if (!liveInstalled) await rm(stagingRoot, {
|
|
591
|
+
recursive: true,
|
|
592
|
+
force: true
|
|
593
|
+
}).catch(() => void 0);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function atomicFileProjection(file) {
|
|
597
|
+
return {
|
|
598
|
+
targetRelativePath: file.targetRelativePath,
|
|
599
|
+
contentDigest: file.contentDigest
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function assertNoTargetCollisions(claims, code) {
|
|
603
|
+
const normalized = [...claims].map((claim) => {
|
|
604
|
+
validatePlanTarget(claim.targetRelativePath);
|
|
605
|
+
return claim;
|
|
606
|
+
}).sort((left, right) => compareStrings(left.targetRelativePath, right.targetRelativePath) || compareStrings(left.owner, right.owner));
|
|
607
|
+
const claimByPath = /* @__PURE__ */ new Map();
|
|
608
|
+
for (const current of normalized) {
|
|
609
|
+
const segments = current.targetRelativePath.split("/");
|
|
610
|
+
for (let length = 1; length < segments.length; length += 1) {
|
|
611
|
+
const ancestor = claimByPath.get(segments.slice(0, length).join("/"));
|
|
612
|
+
if (ancestor) failTargetCollision(code, ancestor, current);
|
|
613
|
+
}
|
|
614
|
+
const duplicate = claimByPath.get(current.targetRelativePath);
|
|
615
|
+
if (duplicate) failTargetCollision(code, duplicate, current);
|
|
616
|
+
claimByPath.set(current.targetRelativePath, current);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function failTargetCollision(code, left, right) {
|
|
620
|
+
failDistribution(code, `target claim overlap: owner ${left.owner} target ${left.targetRelativePath} conflicts with owner ${right.owner} target ${right.targetRelativePath}`);
|
|
621
|
+
}
|
|
622
|
+
function validatePlannedFilePath(file, field) {
|
|
623
|
+
validatePlanTarget(file[field], {
|
|
624
|
+
owner: file.skillFqn,
|
|
625
|
+
field
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function validatePlanTarget(path, diagnostic) {
|
|
629
|
+
const issue = safePathLexicalIssue(path, "relative-path");
|
|
630
|
+
if (issue) failDistribution("SKILL_PLAN_TARGET_INVALID", diagnostic ? `plan file owner ${diagnostic.owner} field ${diagnostic.field} raw target ${JSON.stringify(path)} must be a safe relative path (${issue})` : `plan target must be a safe relative path: ${JSON.stringify(path)} (${issue})`);
|
|
631
|
+
}
|
|
632
|
+
function digestBytes(content) {
|
|
633
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
634
|
+
}
|
|
635
|
+
function digestJson(value) {
|
|
636
|
+
return digestBytes(new TextEncoder().encode(JSON.stringify(value)));
|
|
637
|
+
}
|
|
638
|
+
function compareStrings(left, right) {
|
|
639
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
640
|
+
}
|
|
641
|
+
async function pathExists(path) {
|
|
642
|
+
return Boolean(await lstat(path).catch(() => void 0));
|
|
643
|
+
}
|
|
644
|
+
function failDistribution(code, message) {
|
|
645
|
+
throw new SkillCapsuleDistributionError(code, message);
|
|
646
|
+
}
|
|
647
|
+
function renderSkill(skill, registry, references) {
|
|
648
|
+
const lines = [
|
|
649
|
+
"---",
|
|
650
|
+
`name: ${skill.name}`,
|
|
651
|
+
`description: ${skill.description}`,
|
|
652
|
+
"---",
|
|
653
|
+
"",
|
|
654
|
+
`# ${skill.name}`,
|
|
655
|
+
"",
|
|
656
|
+
skill.description,
|
|
657
|
+
""
|
|
658
|
+
];
|
|
659
|
+
if (skill.instructions?.trim()) lines.push("## Instructions", "", skill.instructions.trim(), "");
|
|
660
|
+
if (registry) {
|
|
661
|
+
lines.push("## Resources", "");
|
|
662
|
+
for (const [kind, records] of [...registry.byKind.entries()].sort(([a], [b]) => compareStrings(a, b))) {
|
|
663
|
+
lines.push(`### ${kind}`, "");
|
|
664
|
+
for (const record of [...records].sort((a, b) => compareStrings(a.fqn ?? a.name ?? "", b.fqn ?? b.name ?? ""))) lines.push(`- ${record.fqn ?? record.name}: ${record.description}`);
|
|
665
|
+
lines.push("");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (references.length > 0) {
|
|
669
|
+
lines.push("## References", "");
|
|
670
|
+
for (const reference of references) lines.push(`- references/${reference.path}`);
|
|
671
|
+
lines.push("");
|
|
672
|
+
}
|
|
673
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
674
|
+
}
|
|
675
|
+
function validateSkillDescriptor(skill) {
|
|
676
|
+
if (!skill.name.trim()) throw new Error("Skill name is required");
|
|
677
|
+
if (!skill.description.trim()) throw new Error("Skill description is required");
|
|
678
|
+
}
|
|
679
|
+
function validateReferencePath(path) {
|
|
680
|
+
const issue = safePathLexicalIssue(path, "relative-path");
|
|
681
|
+
if (issue) failDistribution("SKILL_REFERENCE_PATH_INVALID", `Invalid Skill reference path ${JSON.stringify(path)}: ${issue}`);
|
|
682
|
+
}
|
|
683
|
+
function selectSkillResources(assembly, skill) {
|
|
684
|
+
const includes = skill.includes.length > 0 ? skill.includes : [
|
|
685
|
+
...assembly.functions.map((item) => ({
|
|
686
|
+
kind: item.kind,
|
|
687
|
+
ref: `resource://${item.fqn}`
|
|
688
|
+
})),
|
|
689
|
+
...assembly.composedFunctions.map((item) => ({
|
|
690
|
+
kind: item.kind,
|
|
691
|
+
ref: `resource://${item.fqn}`
|
|
692
|
+
})),
|
|
693
|
+
...assembly.businessObjects.map((item) => ({
|
|
694
|
+
kind: item.kind,
|
|
695
|
+
ref: `resource://${item.fqn}`
|
|
696
|
+
})),
|
|
697
|
+
...assembly.pageObjects.map((item) => ({
|
|
698
|
+
kind: item.kind,
|
|
699
|
+
ref: `resource://${item.fqn}`
|
|
700
|
+
})),
|
|
701
|
+
...assembly.applicationSops.map((item) => ({
|
|
702
|
+
kind: item.kind,
|
|
703
|
+
ref: `resource://${item.fqn}`
|
|
704
|
+
})),
|
|
705
|
+
...assembly.promptFragments.map((item) => ({
|
|
706
|
+
kind: item.kind,
|
|
707
|
+
ref: `resource://${item.fqn}`
|
|
708
|
+
})),
|
|
709
|
+
...assembly.wikiPages.map((item) => ({
|
|
710
|
+
kind: item.kind,
|
|
711
|
+
ref: `resource://${item.fqn}`
|
|
712
|
+
}))
|
|
713
|
+
];
|
|
714
|
+
const fqnSet = new Set(includes.map((include) => resourceRefToFqn(include.ref)));
|
|
715
|
+
const selected = (resources) => resources.filter((item) => fqnSet.has(item.fqn)).sort((left, right) => compareStrings(left.fqn, right.fqn));
|
|
716
|
+
const businessObjects = selected(assembly.businessObjects).map(canonicalObjectOwner);
|
|
717
|
+
const pageObjects = selected(assembly.pageObjects).map(canonicalObjectOwner);
|
|
718
|
+
const businessSopRefs = new Set(businessObjects.flatMap((item) => item.sopRefs).map(resourceRefToFqn));
|
|
719
|
+
return {
|
|
720
|
+
assembly,
|
|
721
|
+
functions: selected(assembly.functions),
|
|
722
|
+
composedFunctions: selected(assembly.composedFunctions),
|
|
723
|
+
businessObjects,
|
|
724
|
+
pageObjects,
|
|
725
|
+
businessObjectSops: assembly.businessObjectSops.filter((item) => businessSopRefs.has(item.fqn)).sort((left, right) => compareStrings(left.fqn, right.fqn)),
|
|
726
|
+
applicationSops: selected(assembly.applicationSops),
|
|
727
|
+
promptFragments: selected(assembly.promptFragments),
|
|
728
|
+
wikiPages: selected(assembly.wikiPages)
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function canonicalObjectOwner(resource) {
|
|
732
|
+
const canonical = {
|
|
733
|
+
...resource,
|
|
734
|
+
targetKinds: [...resource.targetKinds].sort((left, right) => compareStrings(left.kindFqn, right.kindFqn)),
|
|
735
|
+
operations: [...resource.operations].map((operation) => ({
|
|
736
|
+
...operation,
|
|
737
|
+
invocationModes: [...operation.invocationModes].sort(compareStrings)
|
|
738
|
+
})).sort((left, right) => compareStrings(left.ref, right.ref))
|
|
739
|
+
};
|
|
740
|
+
if (resource.kind === "BusinessObject") return {
|
|
741
|
+
...canonical,
|
|
742
|
+
sopRefs: [...resource.sopRefs].sort(compareStrings),
|
|
743
|
+
relatedRefs: [...resource.relatedRefs].sort(compareStrings)
|
|
744
|
+
};
|
|
745
|
+
return canonical;
|
|
746
|
+
}
|
|
747
|
+
function planReferenceFiles(selection, schemas, mappings) {
|
|
748
|
+
const files = [];
|
|
749
|
+
for (const item of selection.composedFunctions) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), callableReferenceMarkdown(item, schemas)));
|
|
750
|
+
for (const item of selection.functions) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), callableReferenceMarkdown(item, schemas)));
|
|
751
|
+
for (const item of selection.businessObjects) {
|
|
752
|
+
const sops = selection.businessObjectSops.filter((sop) => item.sopRefs.map(resourceRefToFqn).includes(sop.fqn));
|
|
753
|
+
files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), businessObjectMarkdown(item, sops, (kind, fqn) => mappedResourcePath(mappings, kind, fqn))));
|
|
754
|
+
}
|
|
755
|
+
for (const item of selection.pageObjects) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), objectOwnerMarkdown(item)));
|
|
756
|
+
for (const item of selection.businessObjectSops) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), textReferenceMarkdown(item)));
|
|
757
|
+
for (const item of selection.applicationSops) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), textReferenceMarkdown(item)));
|
|
758
|
+
for (const item of selection.promptFragments) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), textReferenceMarkdown(item)));
|
|
759
|
+
for (const item of selection.wikiPages) files.push(planReferenceFile(mappedResourcePath(mappings, item.kind, item.fqn), textReferenceMarkdown(item)));
|
|
760
|
+
return files;
|
|
761
|
+
}
|
|
762
|
+
function objectOwnerMarkdown(resource) {
|
|
763
|
+
return [
|
|
764
|
+
`# ${lastFqnSegment(resource.fqn)}`,
|
|
765
|
+
"",
|
|
766
|
+
`<fqn>${resource.fqn}</fqn>`,
|
|
767
|
+
`<kind>${resource.kind}</kind>`,
|
|
768
|
+
`<description>${resource.description}</description>`,
|
|
769
|
+
"",
|
|
770
|
+
"<available_operations>",
|
|
771
|
+
...resource.operations.map((operation) => `- ${operation.ref}: ${operation.behavior}, targets=${operation.targets.kind}, modes=${operation.invocationModes.join(",")}`),
|
|
772
|
+
"</available_operations>",
|
|
773
|
+
"",
|
|
774
|
+
"<usage>",
|
|
775
|
+
"Call `run_object_operation({ targets, invocation, config })`; action invocation uses input and mutation invocation uses desired.",
|
|
776
|
+
"</usage>",
|
|
777
|
+
""
|
|
778
|
+
].join("\n");
|
|
779
|
+
}
|
|
780
|
+
function planReferenceFile(path, content) {
|
|
781
|
+
validatePlanTarget(path);
|
|
782
|
+
return atomicTextFile(path, content.trimEnd() + "\n");
|
|
783
|
+
}
|
|
784
|
+
function callableReferenceMarkdown(resource, schemas) {
|
|
785
|
+
return [
|
|
786
|
+
`# ${lastFqnSegment(resource.fqn)}`,
|
|
787
|
+
"",
|
|
788
|
+
`<fqn>${resource.fqn}</fqn>`,
|
|
789
|
+
`<kind>${resource.kind}</kind>`,
|
|
790
|
+
`<description>${resource.description}</description>`,
|
|
791
|
+
"",
|
|
792
|
+
"<usage>",
|
|
793
|
+
`Call \`run_callable_resource(${JSON.stringify(resource.fqn)}, input, config)\` with an input object matching <input_schema>. The host supplies runtime separately; omit config when the capability does not need it.`,
|
|
794
|
+
"</usage>",
|
|
795
|
+
"",
|
|
796
|
+
"<input_schema>",
|
|
797
|
+
"```json",
|
|
798
|
+
JSON.stringify(schemas(resource.inputContractRef), null, 2),
|
|
799
|
+
"```",
|
|
800
|
+
"</input_schema>",
|
|
801
|
+
"",
|
|
802
|
+
"<output_schema>",
|
|
803
|
+
"```json",
|
|
804
|
+
JSON.stringify(schemas(resource.outputContractRef), null, 2),
|
|
805
|
+
"```",
|
|
806
|
+
"</output_schema>",
|
|
807
|
+
"",
|
|
808
|
+
"<instruction>",
|
|
809
|
+
resource.instruction?.content.trim() ?? "",
|
|
810
|
+
"</instruction>",
|
|
811
|
+
""
|
|
812
|
+
].join("\n");
|
|
813
|
+
}
|
|
814
|
+
function businessObjectMarkdown(resource, sops, mappedPath) {
|
|
815
|
+
return [
|
|
816
|
+
`# ${lastFqnSegment(resource.fqn)}`,
|
|
817
|
+
"",
|
|
818
|
+
`<fqn>${resource.fqn}</fqn>`,
|
|
819
|
+
`<kind>${resource.kind}</kind>`,
|
|
820
|
+
`<description>${resource.description}</description>`,
|
|
821
|
+
"",
|
|
822
|
+
"<available_operations>",
|
|
823
|
+
...resource.operations.map((operation) => `- ${operation.ref}: ${operation.behavior}, targets=${operation.targets.kind}, modes=${operation.invocationModes.join(",")}`),
|
|
824
|
+
"</available_operations>",
|
|
825
|
+
"",
|
|
826
|
+
"<available_sops>",
|
|
827
|
+
...sops.map((item) => `- ${item.fqn}: ${mappedPath(item.kind, item.fqn)}`),
|
|
828
|
+
"</available_sops>",
|
|
829
|
+
"",
|
|
830
|
+
"<usage>",
|
|
831
|
+
"Call `run_object_operation({ targets, invocation, config })`; action invocation uses input and mutation invocation uses desired.",
|
|
832
|
+
"</usage>",
|
|
833
|
+
"",
|
|
834
|
+
"<instruction>",
|
|
835
|
+
resource.instruction?.content.trim() ?? "",
|
|
836
|
+
"</instruction>",
|
|
837
|
+
""
|
|
838
|
+
].join("\n");
|
|
839
|
+
}
|
|
840
|
+
function textReferenceMarkdown(resource) {
|
|
841
|
+
return [
|
|
842
|
+
`# ${lastFqnSegment(resource.fqn)}`,
|
|
843
|
+
"",
|
|
844
|
+
`<fqn>${resource.fqn}</fqn>`,
|
|
845
|
+
`<kind>${resource.kind}</kind>`,
|
|
846
|
+
`<description>${resource.description}</description>`,
|
|
847
|
+
"",
|
|
848
|
+
resource.instruction?.content.trim() ?? "",
|
|
849
|
+
""
|
|
850
|
+
].join("\n");
|
|
851
|
+
}
|
|
852
|
+
function planCallableArtifacts(resources, schemas, targetBase) {
|
|
853
|
+
const files = [];
|
|
854
|
+
const imports = [];
|
|
855
|
+
const entries = [];
|
|
856
|
+
for (const resource of resources) {
|
|
857
|
+
const filePath = `${`${resource.kind}s`}/${lastFqnSegment(resource.fqn)}.js`;
|
|
858
|
+
const symbol = safeJsIdentifier(resource.fqn);
|
|
859
|
+
files.push(atomicTextFile(`${targetBase}${filePath}`, [
|
|
860
|
+
`import { ${resource.codeBinding.exportName} as entry } from ${JSON.stringify(resource.codeBinding.moduleSpecifier)}`,
|
|
861
|
+
"",
|
|
862
|
+
"export default entry",
|
|
863
|
+
""
|
|
864
|
+
].join("\n")));
|
|
865
|
+
imports.push(`import ${symbol} from ${JSON.stringify(`./${filePath}`)}`);
|
|
866
|
+
entries.push(JSON.stringify(resource.fqn) + `: { kind: ${JSON.stringify(resource.kind)}, handler: ${symbol} }`);
|
|
867
|
+
}
|
|
868
|
+
const registry = resources.map((resource) => ({
|
|
869
|
+
fqn: resource.fqn,
|
|
870
|
+
kind: resource.kind,
|
|
871
|
+
description: resource.description,
|
|
872
|
+
inputContractRef: resource.inputContractRef,
|
|
873
|
+
outputContractRef: resource.outputContractRef,
|
|
874
|
+
inputSchema: schemas(resource.inputContractRef),
|
|
875
|
+
outputSchema: schemas(resource.outputContractRef),
|
|
876
|
+
module: `./${resource.kind}s/${lastFqnSegment(resource.fqn)}.js`
|
|
877
|
+
}));
|
|
878
|
+
files.push(atomicTextFile(`${targetBase}registry.json`, `${JSON.stringify({ callableResources: registry }, null, 2)}\n`));
|
|
879
|
+
files.push(atomicTextFile(`${targetBase}bundle.js`, [
|
|
880
|
+
"import { runWithRuntime } from \"halfcode-compiler.xnl/authoring-runtime\"",
|
|
881
|
+
...imports,
|
|
882
|
+
"",
|
|
883
|
+
`const registry = { ${entries.join(", ")} }`,
|
|
884
|
+
"let callableRuntime",
|
|
885
|
+
"",
|
|
886
|
+
"/**",
|
|
887
|
+
" * Host-only initialization boundary. Bind runtime capabilities before exposing",
|
|
888
|
+
" * run_callable_resource to AI-authored code; runtime is never an AI argument.",
|
|
889
|
+
" */",
|
|
890
|
+
"export function initialize_callable_runtime(runtime) {",
|
|
891
|
+
" if (!runtime) throw new Error(\"Callable runtime is required\")",
|
|
892
|
+
" callableRuntime = runtime",
|
|
893
|
+
"}",
|
|
894
|
+
"",
|
|
895
|
+
"/**",
|
|
896
|
+
" * AI-facing callable entry. Callers provide only the resource identity, business",
|
|
897
|
+
" * input, and optional invocation config; the host-owned runtime stays hidden.",
|
|
898
|
+
" */",
|
|
899
|
+
"export async function run_callable_resource(fqn, input, config) {",
|
|
900
|
+
" if (!callableRuntime) throw new Error(\"Callable runtime has not been initialized\")",
|
|
901
|
+
" return invokeCallableResource(callableRuntime, fqn, input, config)",
|
|
902
|
+
"}",
|
|
903
|
+
"",
|
|
904
|
+
"/**",
|
|
905
|
+
" * Framework-internal bridge. It activates the host runtime context and forwards",
|
|
906
|
+
" * input/config unchanged to the deterministic resource handler.",
|
|
907
|
+
" */",
|
|
908
|
+
"async function invokeCallableResource(runtime, fqn, input, config) {",
|
|
909
|
+
" const entry = registry[fqn]",
|
|
910
|
+
" if (!entry) throw new Error(`Unknown callable resource: ${fqn}`)",
|
|
911
|
+
" return runWithRuntime(runtime, () => entry.handler(input, config))",
|
|
912
|
+
"}",
|
|
913
|
+
"",
|
|
914
|
+
"export function resolve_callable_resource(fqn) {",
|
|
915
|
+
" const entry = registry[fqn]",
|
|
916
|
+
" if (!entry) throw new Error(`Unknown callable resource: ${fqn}`)",
|
|
917
|
+
" return { fqn, kind: entry.kind }",
|
|
918
|
+
"}",
|
|
919
|
+
""
|
|
920
|
+
].join("\n")));
|
|
921
|
+
return files;
|
|
922
|
+
}
|
|
923
|
+
function planObjectOperationArtifacts(selection, schemas) {
|
|
924
|
+
const targetBase = "objects/";
|
|
925
|
+
const owners = [...selection.businessObjects, ...selection.pageObjects];
|
|
926
|
+
const operations = owners.flatMap((owner) => owner.operations).sort((left, right) => compareStrings(left.ref, right.ref));
|
|
927
|
+
const targetKinds = owners.flatMap((owner) => owner.targetKinds).sort((left, right) => compareStrings(left.kindFqn, right.kindFqn));
|
|
928
|
+
const files = [];
|
|
929
|
+
const imports = [];
|
|
930
|
+
const entries = [];
|
|
931
|
+
for (const definition of operations) {
|
|
932
|
+
const binding = resolveObjectOperationCompilation(selection.assembly, definition.ref);
|
|
933
|
+
const filePath = `handlers/${`${safeJsIdentifier(definition.ref)}.js`}`;
|
|
934
|
+
const symbol = safeJsIdentifier(definition.ref);
|
|
935
|
+
files.push(atomicTextFile(`${targetBase}${filePath}`, [
|
|
936
|
+
`import { ${binding.codeBinding.exportName} as entry } from ${JSON.stringify(binding.codeBinding.moduleSpecifier)}`,
|
|
937
|
+
"",
|
|
938
|
+
"export default entry",
|
|
939
|
+
""
|
|
940
|
+
].join("\n")));
|
|
941
|
+
imports.push(`import ${symbol} from ${JSON.stringify(`./${filePath}`)}`);
|
|
942
|
+
entries.push(`${JSON.stringify(definition.ref)}: ${symbol}`);
|
|
943
|
+
}
|
|
944
|
+
const registry = {
|
|
945
|
+
targetKinds,
|
|
946
|
+
operations: operations.map((definition) => ({
|
|
947
|
+
...definition,
|
|
948
|
+
...objectOperationContractProjection(selection.assembly, definition, schemas)
|
|
949
|
+
}))
|
|
950
|
+
};
|
|
951
|
+
files.push(atomicTextFile(`${targetBase}registry.json`, `${JSON.stringify(registry, null, 2)}\n`));
|
|
952
|
+
files.push(atomicTextFile(`${targetBase}bundle.js`, [
|
|
953
|
+
"import { assertObjectOperationCall } from \"halfcode-compiler.xnl\"",
|
|
954
|
+
"import { runWithRuntime } from \"halfcode-compiler.xnl/authoring-runtime\"",
|
|
955
|
+
"import catalog from \"./registry.json\" with { type: \"json\" }",
|
|
956
|
+
...imports,
|
|
957
|
+
"",
|
|
958
|
+
`const handlers = { ${entries.join(", ")} }`,
|
|
959
|
+
"const definitions = Object.fromEntries(catalog.operations.map((definition) => [definition.ref, definition]))",
|
|
960
|
+
"let objectOperationRuntime",
|
|
961
|
+
"",
|
|
962
|
+
"/** Host-only runtime initialization; runtime is never an AI argument. */",
|
|
963
|
+
"export function initialize_object_operation_runtime(runtime) {",
|
|
964
|
+
" if (!runtime) throw new Error(\"Object operation runtime is required\")",
|
|
965
|
+
" objectOperationRuntime = runtime",
|
|
966
|
+
"}",
|
|
967
|
+
"",
|
|
968
|
+
"/** AI-facing object entry with orthogonal targets, invocation and config. */",
|
|
969
|
+
"export async function run_object_operation(call) {",
|
|
970
|
+
" if (!objectOperationRuntime) throw new Error(\"Object operation runtime has not been initialized\")",
|
|
971
|
+
" const operationRef = call?.invocation?.operationRef",
|
|
972
|
+
" const definition = definitions[operationRef]",
|
|
973
|
+
" if (!definition) throw new Error(`Unknown object operation: ${operationRef}`)",
|
|
974
|
+
" const handler = handlers[operationRef]",
|
|
975
|
+
" if (!handler) throw new Error(`Object operation handler is unavailable: ${operationRef}`)",
|
|
976
|
+
" assertObjectOperationCall(call, definition)",
|
|
977
|
+
" return runWithRuntime(objectOperationRuntime, () =>",
|
|
978
|
+
" handler(objectOperationRuntime, call.targets, call.invocation, call.config)",
|
|
979
|
+
" )",
|
|
980
|
+
"}",
|
|
981
|
+
"",
|
|
982
|
+
"export function resolve_object_operation(operationRef) {",
|
|
983
|
+
" const definition = definitions[operationRef]",
|
|
984
|
+
" if (!definition) throw new Error(`Unknown object operation: ${operationRef}`)",
|
|
985
|
+
" return definition",
|
|
986
|
+
"}",
|
|
987
|
+
""
|
|
988
|
+
].join("\n")));
|
|
989
|
+
return files;
|
|
990
|
+
}
|
|
991
|
+
function objectOperationContractProjection(assembly, definition, schemas) {
|
|
992
|
+
const compilation = resolveObjectOperationCompilation(assembly, definition.ref);
|
|
993
|
+
if (!compilation.inputContractRef || !compilation.outputContractRef) return {};
|
|
994
|
+
return {
|
|
995
|
+
inputContractRef: compilation.inputContractRef,
|
|
996
|
+
outputContractRef: compilation.outputContractRef,
|
|
997
|
+
inputSchema: schemas(compilation.inputContractRef),
|
|
998
|
+
outputSchema: schemas(compilation.outputContractRef)
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function referenceModel(selection, mappings) {
|
|
1002
|
+
const ref = (kind, item) => ({
|
|
1003
|
+
fqn: item.fqn,
|
|
1004
|
+
description: item.description,
|
|
1005
|
+
path: mappedResourcePath(mappings, kind, item.fqn)
|
|
1006
|
+
});
|
|
1007
|
+
return {
|
|
1008
|
+
composedFunctions: selection.composedFunctions.map((item) => ref(item.kind, item)),
|
|
1009
|
+
functions: selection.functions.map((item) => ref(item.kind, item)),
|
|
1010
|
+
businessObjects: selection.businessObjects.map((item) => ({
|
|
1011
|
+
fqn: item.fqn,
|
|
1012
|
+
description: item.description,
|
|
1013
|
+
path: mappedResourcePath(mappings, item.kind, item.fqn)
|
|
1014
|
+
})),
|
|
1015
|
+
pageObjects: selection.pageObjects.map((item) => ref(item.kind, item)),
|
|
1016
|
+
applicationSops: selection.applicationSops.map((item) => ref(item.kind, item)),
|
|
1017
|
+
promptFragments: selection.promptFragments.map((item) => ref(item.kind, item)),
|
|
1018
|
+
wikiPages: selection.wikiPages.map((item) => ref(item.kind, item)),
|
|
1019
|
+
callableArtifacts: {
|
|
1020
|
+
registryPath: `${mappings.callableArtifactsTarget}registry.json`,
|
|
1021
|
+
bundlePath: `${mappings.callableArtifactsTarget}bundle.js`
|
|
1022
|
+
},
|
|
1023
|
+
objectArtifacts: {
|
|
1024
|
+
registryPath: "objects/registry.json",
|
|
1025
|
+
bundlePath: "objects/bundle.js"
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function mappedResourcePath(mappings, kind, fqn) {
|
|
1030
|
+
return `${mappings.referenceTargets.get(kind) ?? defaultReferenceTarget(kind)}${kind === "BusinessObject" ? `${lastFqnSegment(fqn)}/BUSINESS_OBJECT.md` : kind === "PageObject" ? `${lastFqnSegment(fqn)}/PAGE_OBJECT.md` : `${lastFqnSegment(fqn)}.md`}`;
|
|
1031
|
+
}
|
|
1032
|
+
function defaultReferenceTarget(kind) {
|
|
1033
|
+
return {
|
|
1034
|
+
Function: "references/Functions/",
|
|
1035
|
+
ComposedFunction: "references/ComposedFunctions/",
|
|
1036
|
+
BusinessObject: "references/BusinessObjects/",
|
|
1037
|
+
PageObject: "references/PageObjects/",
|
|
1038
|
+
BusinessObjectSOP: "references/BusinessObjectSOPs/",
|
|
1039
|
+
ApplicationSOP: "references/ApplicationSOPs/",
|
|
1040
|
+
PromptFragment: "references/PromptFragments/",
|
|
1041
|
+
WikiPage: "references/Wiki/"
|
|
1042
|
+
}[kind] ?? `references/${kind}/`;
|
|
1043
|
+
}
|
|
1044
|
+
function schemaReader(schemas) {
|
|
1045
|
+
const canonicalByFqn = /* @__PURE__ */ new Map();
|
|
1046
|
+
return (ref) => {
|
|
1047
|
+
const fqn = resourceRefToFqn(ref);
|
|
1048
|
+
if (!schemas) return void 0;
|
|
1049
|
+
const cached = canonicalByFqn.get(fqn);
|
|
1050
|
+
if (cached !== void 0) return cached;
|
|
1051
|
+
let value;
|
|
1052
|
+
if (typeof schemas.get === "function") value = schemas.get(fqn);
|
|
1053
|
+
else value = schemas[fqn];
|
|
1054
|
+
if (value === void 0) return void 0;
|
|
1055
|
+
const canonical = canonicalJsonValue(value, fqn);
|
|
1056
|
+
canonicalByFqn.set(fqn, canonical);
|
|
1057
|
+
return canonical;
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function canonicalJsonValue(value, schemaFqn) {
|
|
1061
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
1062
|
+
const visit = (current, path) => {
|
|
1063
|
+
if (current === null || typeof current === "string" || typeof current === "boolean") return current;
|
|
1064
|
+
if (typeof current === "number") {
|
|
1065
|
+
if (!Number.isFinite(current)) failInvalidSchema(schemaFqn, path, "number must be finite");
|
|
1066
|
+
return Object.is(current, -0) ? 0 : current;
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof current !== "object") failInvalidSchema(schemaFqn, path, `unsupported ${typeof current} value`);
|
|
1069
|
+
if (ancestors.has(current)) failInvalidSchema(schemaFqn, path, "cyclic value");
|
|
1070
|
+
if (Object.getOwnPropertySymbols(current).length > 0) failInvalidSchema(schemaFqn, path, "symbol keys are not JSON-compatible");
|
|
1071
|
+
ancestors.add(current);
|
|
1072
|
+
try {
|
|
1073
|
+
if (Array.isArray(current)) {
|
|
1074
|
+
const names = Object.getOwnPropertyNames(current);
|
|
1075
|
+
const indices = Array.from({ length: current.length }, (_, index) => index);
|
|
1076
|
+
const allowed = /* @__PURE__ */ new Set(["length", ...indices.map(String)]);
|
|
1077
|
+
if (names.some((name) => !allowed.has(name)) || indices.some((index) => !(index in current))) failInvalidSchema(schemaFqn, path, "arrays must be dense and contain no extra properties");
|
|
1078
|
+
const values = indices.map((index) => {
|
|
1079
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
|
|
1080
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) failInvalidSchema(schemaFqn, `${path}[${index}]`, "array entries must be enumerable data values");
|
|
1081
|
+
return visit(descriptor.value, `${path}[${index}]`);
|
|
1082
|
+
});
|
|
1083
|
+
return Object.freeze(values);
|
|
1084
|
+
}
|
|
1085
|
+
const prototype = Object.getPrototypeOf(current);
|
|
1086
|
+
if (prototype !== Object.prototype && prototype !== null) failInvalidSchema(schemaFqn, path, "value must be a plain object");
|
|
1087
|
+
const entries = Object.getOwnPropertyNames(current).sort(compareStrings).map((key) => {
|
|
1088
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
1089
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) failInvalidSchema(schemaFqn, `${path}[${JSON.stringify(key)}]`, "properties must be enumerable data values");
|
|
1090
|
+
return [key, visit(descriptor.value, `${path}[${JSON.stringify(key)}]`)];
|
|
1091
|
+
});
|
|
1092
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
1093
|
+
} finally {
|
|
1094
|
+
ancestors.delete(current);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
return visit(value, "$");
|
|
1098
|
+
}
|
|
1099
|
+
function failInvalidSchema(schemaFqn, path, reason) {
|
|
1100
|
+
failDistribution("SKILL_SCHEMA_INVALID", `schema ${schemaFqn} at ${path}: ${reason}`);
|
|
1101
|
+
}
|
|
1102
|
+
function renderEjsTemplate(template, context) {
|
|
1103
|
+
let cursor = 0;
|
|
1104
|
+
let code = "let __out = '';\nconst __append = (value) => { __out += value == null ? '' : String(value) };\nwith (ctx) {\n";
|
|
1105
|
+
for (const match of template.matchAll(/<%([=-]?)([\s\S]*?)%>/g)) {
|
|
1106
|
+
code += `__append(${JSON.stringify(template.slice(cursor, match.index))});\n`;
|
|
1107
|
+
const [, mode, body] = match;
|
|
1108
|
+
if (mode === "=" || mode === "-") code += `__append(${body.trim()});\n`;
|
|
1109
|
+
else code += `${body}\n`;
|
|
1110
|
+
cursor = (match.index ?? 0) + match[0].length;
|
|
1111
|
+
}
|
|
1112
|
+
code += `__append(${JSON.stringify(template.slice(cursor))});\n`;
|
|
1113
|
+
code += "}\nreturn __out";
|
|
1114
|
+
return new Function("ctx", code)(context);
|
|
1115
|
+
}
|
|
1116
|
+
function lastFqnSegment(fqn) {
|
|
1117
|
+
return fqn.split(".").at(-1) ?? fqn;
|
|
1118
|
+
}
|
|
1119
|
+
function safeJsIdentifier(fqn) {
|
|
1120
|
+
return fqn.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
1121
|
+
}
|
|
1122
|
+
function plannedGeneratedTargets(selection, callableResources, mappings) {
|
|
1123
|
+
const referenceResources = [
|
|
1124
|
+
...selection.functions,
|
|
1125
|
+
...selection.composedFunctions,
|
|
1126
|
+
...selection.businessObjects,
|
|
1127
|
+
...selection.pageObjects,
|
|
1128
|
+
...selection.businessObjectSops,
|
|
1129
|
+
...selection.applicationSops,
|
|
1130
|
+
...selection.promptFragments,
|
|
1131
|
+
...selection.wikiPages
|
|
1132
|
+
];
|
|
1133
|
+
return [
|
|
1134
|
+
{
|
|
1135
|
+
targetRelativePath: "SKILL.md",
|
|
1136
|
+
owner: "generated:skill"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
targetRelativePath: SKILL_PROVENANCE_PATH,
|
|
1140
|
+
owner: "generated:provenance"
|
|
1141
|
+
},
|
|
1142
|
+
...referenceResources.map((item) => ({
|
|
1143
|
+
targetRelativePath: mappedResourcePath(mappings, item.kind, item.fqn),
|
|
1144
|
+
owner: `resource:${item.fqn}`
|
|
1145
|
+
})),
|
|
1146
|
+
...callableResources.map((resource) => {
|
|
1147
|
+
return {
|
|
1148
|
+
targetRelativePath: `${mappings.callableArtifactsTarget}${resource.kind}s/${lastFqnSegment(resource.fqn)}.js`,
|
|
1149
|
+
owner: `resource:${resource.fqn}`
|
|
1150
|
+
};
|
|
1151
|
+
}),
|
|
1152
|
+
{
|
|
1153
|
+
targetRelativePath: `${mappings.callableArtifactsTarget}registry.json`,
|
|
1154
|
+
owner: "generated:callable-registry"
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
targetRelativePath: `${mappings.callableArtifactsTarget}bundle.js`,
|
|
1158
|
+
owner: "generated:callable-bundle"
|
|
1159
|
+
},
|
|
1160
|
+
...[...selection.businessObjects, ...selection.pageObjects].flatMap((owner) => owner.operations).map((operation) => ({
|
|
1161
|
+
targetRelativePath: `objects/handlers/${safeJsIdentifier(operation.ref)}.js`,
|
|
1162
|
+
owner: `operation:${operation.ref}`
|
|
1163
|
+
})),
|
|
1164
|
+
{
|
|
1165
|
+
targetRelativePath: "objects/registry.json",
|
|
1166
|
+
owner: "generated:object-registry"
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
targetRelativePath: "objects/bundle.js",
|
|
1170
|
+
owner: "generated:object-bundle"
|
|
1171
|
+
}
|
|
1172
|
+
];
|
|
1173
|
+
}
|
|
1174
|
+
function assertNoGeneratedTargetCollisions(claims) {
|
|
1175
|
+
assertNoTargetCollisions(claims, "SKILL_GENERATED_TARGET_COLLISION");
|
|
1176
|
+
}
|
|
1177
|
+
function assertCallableClosure(resources, schemas) {
|
|
1178
|
+
const fqns = /* @__PURE__ */ new Set();
|
|
1179
|
+
for (const resource of resources) {
|
|
1180
|
+
if (fqns.has(resource.fqn)) throw new Error(`Callable resource FQN appears more than once: ${resource.fqn}`);
|
|
1181
|
+
fqns.add(resource.fqn);
|
|
1182
|
+
if (schemas(resource.inputContractRef) === void 0) throw new Error(`Callable resource ${resource.fqn} is missing input schema ${resource.inputContractRef}`);
|
|
1183
|
+
if (schemas(resource.outputContractRef) === void 0) throw new Error(`Callable resource ${resource.fqn} is missing output schema ${resource.outputContractRef}`);
|
|
1184
|
+
if (!resource.codeBinding.moduleSpecifier || !resource.codeBinding.exportName) throw new Error(`Callable resource ${resource.fqn} is missing an executable code binding`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function assertObjectOperationArtifactClosure(selection, schemas) {
|
|
1188
|
+
const operationRefs = /* @__PURE__ */ new Set();
|
|
1189
|
+
for (const owner of [...selection.businessObjects, ...selection.pageObjects]) for (const definition of owner.operations) {
|
|
1190
|
+
if (operationRefs.has(definition.ref)) throw new Error(`Object operation ref appears more than once: ${definition.ref}`);
|
|
1191
|
+
operationRefs.add(definition.ref);
|
|
1192
|
+
const binding = resolveObjectOperationCompilation(selection.assembly, definition.ref);
|
|
1193
|
+
if (!binding.codeBinding.moduleSpecifier || !binding.codeBinding.exportName) throw new Error(`Object operation ${definition.ref} is missing an executable code binding`);
|
|
1194
|
+
if (!binding.inputContractRef || !binding.outputContractRef) continue;
|
|
1195
|
+
if (schemas(binding.inputContractRef) === void 0) throw new Error(`Object operation ${definition.ref} is missing input schema ${binding.inputContractRef}`);
|
|
1196
|
+
if (schemas(binding.outputContractRef) === void 0) throw new Error(`Object operation ${definition.ref} is missing output schema ${binding.outputContractRef}`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const compilerSkillPackage = {
|
|
1200
|
+
role: "framework",
|
|
1201
|
+
area: "compiler-skill",
|
|
1202
|
+
owns: "standard Skill capsule projection"
|
|
1203
|
+
};
|
|
1204
|
+
//#endregion
|
|
1205
|
+
export { compilerSkillPackage as a, compileSkillCapsule as i, applySkillCapsuleDistributionPlan as n, planSkillCapsuleDistribution as o, compileResourceSkillCapsule as r, skillCapsuleDistributionProjection as s, SkillCapsuleDistributionError as t };
|