@yugoo-design/dsh-design-studio 0.1.2 → 0.2.0

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/lib/node.js ADDED
@@ -0,0 +1,2159 @@
1
+ import { a as readStudioArchiveResource, i as exportStudioSourceArchive, n as normalizeStudioInstanceChanges, s as finalizeStudioPanelPreview, t as STUDIO_INSTANCE_CHANGE_LIMITS } from "./instance-changes-RvOGeBaE.js";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { loadDesignBundle, loadDesignKit, parseFrontmatter, parseKitMounts, parseMindsetSkillNames } from "@yugoo-design/dsh-design-core";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { lstat, mkdir, open, readFile, readdir, realpath, rename, rmdir, unlink } from "node:fs/promises";
6
+ import { isMap, isScalar, isSeq, parse, parseDocument, stringify } from "yaml";
7
+ //#region src/node/studio-panel-drafts.mjs
8
+ /** Atomic local source storage and retained trial captures. No project writes. */
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
10
+ const HASH = /^[0-9a-f]{64}$/;
11
+ const MAX_SOURCE_BYTES = 16777216;
12
+ const MAX_DOCUMENT_BYTES = 2097152;
13
+ const inside = (root, path) => {
14
+ const rel = relative(root, path);
15
+ return !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
16
+ };
17
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
18
+ const moveToSystemTrash = async (file) => {
19
+ const { default: trash } = await import("trash");
20
+ await trash([file], { glob: false });
21
+ };
22
+ /** Existing namespaces preserve saved source and historical trial data. */
23
+ async function createStudioDraftPorts({ stateRoot, kitRoot, readInstalledKit, readInstalledBundle, moveToTrash = moveToSystemTrash }, kind) {
24
+ if (!["panel", "instance"].includes(kind)) throw new Error("Select the Panel history or instance source namespace.");
25
+ if (!isAbsolute(stateRoot)) throw new Error("Studio requires an explicit absolute state directory.");
26
+ if (kind === "instance" && typeof moveToTrash !== "function") throw new Error("Studio requires a system trash adapter.");
27
+ const kitRootReal = kind === "instance" && kitRoot === void 0 ? void 0 : await realpath(kitRoot);
28
+ if (kitRootReal && inside(kitRootReal, resolve(stateRoot))) throw new Error("Studio drafts must not live inside the installed Kit.");
29
+ await mkdir(stateRoot, {
30
+ recursive: true,
31
+ mode: 448
32
+ });
33
+ const state = await realpath(stateRoot);
34
+ const root = join(state, `studio-${kind}-drafts`);
35
+ const format = `studio-${kind}-draft/v1`;
36
+ if (kitRootReal && inside(kitRootReal, root)) throw new Error("Studio drafts must not live inside the installed Kit.");
37
+ await mkdir(root, {
38
+ recursive: true,
39
+ mode: 448
40
+ });
41
+ if ((await lstat(root)).isSymbolicLink()) throw new Error("Studio storage cannot be a symlink.");
42
+ const snapshots = join(root, "snapshots");
43
+ await mkdir(snapshots, { mode: 448 }).catch((error) => {
44
+ if (error.code !== "EEXIST") throw error;
45
+ });
46
+ if (!(await lstat(snapshots)).isDirectory() || (await lstat(snapshots)).isSymbolicLink()) throw new Error("Unsafe Studio snapshot directory.");
47
+ const withLock = async (operation) => {
48
+ const lock = join(root, "write.lock");
49
+ try {
50
+ await mkdir(lock, { mode: 448 });
51
+ } catch (error) {
52
+ if (error.code === "EEXIST") throw new Error("Studio is being saved elsewhere. Retry; after a crash, stop Studio before clearing its empty write.lock directory.");
53
+ throw error;
54
+ }
55
+ try {
56
+ return await operation();
57
+ } finally {
58
+ await rmdir(lock);
59
+ }
60
+ };
61
+ const readRegular = async (file, limit) => {
62
+ const info = await lstat(file);
63
+ if (!info.isFile() || info.isSymbolicLink() || info.size > limit) throw new Error("Invalid or oversized Studio file.");
64
+ return readFile(file, "utf8");
65
+ };
66
+ const writeAtomic = async (file, text) => {
67
+ const temporary = join(root, `${randomUUID()}.tmp`);
68
+ let handle;
69
+ try {
70
+ handle = await open(temporary, "wx", 384);
71
+ await handle.writeFile(text, "utf8");
72
+ await handle.sync();
73
+ await handle.close();
74
+ handle = void 0;
75
+ await rename(temporary, file);
76
+ } finally {
77
+ await handle?.close();
78
+ await unlink(temporary).catch((error) => {
79
+ if (error.code !== "ENOENT") throw error;
80
+ });
81
+ }
82
+ };
83
+ const draftPath = (id) => {
84
+ if (typeof id !== "string" || !UUID.test(id)) throw new Error("Invalid Studio source identity.");
85
+ return join(root, `${id}.json`);
86
+ };
87
+ const read = async (id) => {
88
+ const record = JSON.parse(await readRegular(draftPath(id), MAX_DOCUMENT_BYTES));
89
+ const pin = (value) => typeof value?.name === "string" && typeof value?.version === "string";
90
+ const validContent = kind === "panel" ? typeof record.panelSource === "string" && typeof record.sampleSource === "string" && pin(record.kit) && pin(record.bundle) : ["kit", "bundle"].includes(record.kind) && (record.recordVersion === 2 ? pin(record[record.kind]) && typeof record.sourceId === "string" && (record.current === true || record.sourceId === `local:${record.kind}:${record[record.kind].name}`) && (record.kind !== "bundle" || typeof record.runtimeName === "string") : (record.recordVersion === void 0 || record.recordVersion === 1) && pin(record.source?.kit) && (record.kind !== "bundle" || pin(record.source?.bundle) && typeof record.bundlePath === "string")) && typeof record.title?.zh === "string" && typeof record.title?.en === "string" && record.overlays !== null && typeof record.overlays === "object" && !Array.isArray(record.overlays) && Object.values(record.overlays).every((value) => typeof value === "string") && Array.isArray(record.mounts) && record.mounts.every((mount) => pin(mount.bundle)) && (record.recordVersion !== 2 || record.mounts.every((mount) => typeof mount.bindingKey === "string" && HASH.test(mount.bundle.source?.snapshot) && typeof mount.bundle.source?.sourceId === "string" && (mount.bundle.instanceId === void 0 || UUID.test(mount.bundle.instanceId)) && (mount.bundle.revision === void 0 || Number.isSafeInteger(mount.bundle.revision) && mount.bundle.revision >= 0)));
91
+ if (record.format !== format || record.id !== id || !Number.isSafeInteger(record.revision) || record.revision < 0 || !HASH.test(record.baseSnapshot) || !validContent) throw new Error("Invalid Studio source record.");
92
+ const { format: _format, ...value } = record;
93
+ if (kind === "instance") delete value.confirmed;
94
+ return value;
95
+ };
96
+ const persist = async (record) => {
97
+ if (kind === "instance") {
98
+ const { confirmed: _confirmed, ...source } = record;
99
+ record = source;
100
+ }
101
+ const text = JSON.stringify({
102
+ format,
103
+ ...record
104
+ });
105
+ if (Buffer.byteLength(text) > MAX_DOCUMENT_BYTES) throw new Error("Studio source record exceeds 2 MiB.");
106
+ await writeAtomic(draftPath(record.id), text);
107
+ return record;
108
+ };
109
+ const repository = {
110
+ read,
111
+ async list() {
112
+ const ids = (await readdir(root)).filter((name) => name.endsWith(".json") && UUID.test(name.slice(0, -5))).sort();
113
+ return Promise.all(ids.map((name) => read(name.slice(0, -5))));
114
+ },
115
+ create: (content) => withLock(() => createUnlocked(content)),
116
+ replace: (id, expectedRevision, content) => withLock(() => replaceUnlocked(id, expectedRevision, content))
117
+ };
118
+ const createUnlocked = async (content) => {
119
+ return persist({
120
+ ...content,
121
+ id: randomUUID(),
122
+ revision: 0
123
+ });
124
+ };
125
+ const replaceUnlocked = async (id, expectedRevision, content) => {
126
+ if ((await read(id)).revision !== expectedRevision) throw new Error("Studio source changed elsewhere. Reopen it before saving.");
127
+ return persist({
128
+ ...content,
129
+ id,
130
+ revision: expectedRevision + 1
131
+ });
132
+ };
133
+ if (kind === "instance") repository.transaction = (operation) => withLock(() => operation({
134
+ read,
135
+ list: repository.list,
136
+ create: createUnlocked,
137
+ replace: replaceUnlocked,
138
+ retainTree: (entries) => retainEntriesUnlocked(checkEntries(entries)),
139
+ async trash(id, expectedRevision) {
140
+ const current = await read(id);
141
+ if (current.revision !== expectedRevision) throw new Error("Studio source changed elsewhere. Reopen it before deleting.");
142
+ const file = draftPath(id);
143
+ await moveToTrash(file);
144
+ try {
145
+ await lstat(file);
146
+ throw new Error("The system trash did not move this Studio working copy.");
147
+ } catch (error) {
148
+ if (error.code !== "ENOENT") throw error;
149
+ }
150
+ return {
151
+ id,
152
+ revision: current.revision,
153
+ trashed: true
154
+ };
155
+ },
156
+ async migrate(id, expectedRevision, content) {
157
+ const current = await read(id);
158
+ if (current.revision !== expectedRevision || current.recordVersion === 2) throw new Error("Legacy instance changed or was already migrated.");
159
+ const backup = join(root, `${id}.legacy-v1.json`);
160
+ const original = JSON.stringify({
161
+ format,
162
+ ...current
163
+ });
164
+ try {
165
+ if (await readRegular(backup, MAX_DOCUMENT_BYTES) !== original) throw new Error("Existing legacy recovery backup differs from the current record.");
166
+ } catch (error) {
167
+ if (error.code !== "ENOENT") throw error;
168
+ await writeAtomic(backup, original);
169
+ }
170
+ return replaceUnlocked(id, expectedRevision, content);
171
+ }
172
+ }));
173
+ const openSnapshot = async (id) => {
174
+ if (typeof id !== "string" || !HASH.test(id)) throw new Error("Invalid Studio source snapshot.");
175
+ const text = await readRegular(join(snapshots, `${id}.json`), MAX_SOURCE_BYTES * 2);
176
+ if (hash(text) !== id) throw new Error("Studio source snapshot integrity check failed.");
177
+ const document = JSON.parse(text);
178
+ const closure = Array.isArray(document) ? void 0 : document;
179
+ if (closure && closure.format !== "studio-source-closure/v1") throw new Error("Invalid Studio source closure.");
180
+ const entries = closure ? closure.entries : document;
181
+ const files = new Map(entries);
182
+ const virtualRoot = resolve(root, "virtual-source", id);
183
+ const keyOf = (path) => {
184
+ const absolute = resolve(path);
185
+ if (!inside(virtualRoot, absolute)) throw new Error("Preview path escapes its source snapshot.");
186
+ return relative(virtualRoot, absolute).split(sep).join("/");
187
+ };
188
+ const children = (path) => {
189
+ const key = keyOf(path), prefix = key ? `${key}/` : "";
190
+ return [...new Set([...files.keys()].filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length).split("/")[0]))].sort();
191
+ };
192
+ const snapshotRoot = closure ? resolve(virtualRoot, closure.kitPath) : virtualRoot;
193
+ keyOf(snapshotRoot);
194
+ const loadOptions = closure ? {
195
+ sourceId: closure.sourceId,
196
+ bundles: closure.bundles.map((bundle) => {
197
+ const rootDir = resolve(virtualRoot, bundle.path);
198
+ keyOf(rootDir);
199
+ return {
200
+ name: bundle.name,
201
+ version: bundle.version,
202
+ sourceId: bundle.sourceId,
203
+ rootDir
204
+ };
205
+ })
206
+ } : void 0;
207
+ return {
208
+ id,
209
+ root: snapshotRoot,
210
+ storageRoot: virtualRoot,
211
+ ...loadOptions ? { loadOptions } : {},
212
+ fs: {
213
+ async readBytes(path) {
214
+ const bytes = files.get(keyOf(path));
215
+ return bytes === void 0 ? void 0 : Buffer.from(bytes, "base64");
216
+ },
217
+ async readText(path) {
218
+ const bytes = files.get(keyOf(path));
219
+ return bytes === void 0 ? void 0 : new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(bytes, "base64"));
220
+ },
221
+ async readResource(path) {
222
+ const bytes = files.get(keyOf(path));
223
+ return bytes === void 0 ? void 0 : readStudioArchiveResource(path, Buffer.from(bytes, "base64"));
224
+ },
225
+ async listDir(path) {
226
+ return children(path);
227
+ },
228
+ async isDirectory(path) {
229
+ return children(path).length > 0;
230
+ },
231
+ async realPath(path) {
232
+ keyOf(path);
233
+ return resolve(path);
234
+ }
235
+ }
236
+ };
237
+ };
238
+ const checkEntries = (entries) => {
239
+ if (!Array.isArray(entries) || entries.length > 4096) throw new Error("Source tree exceeds 4096 files.");
240
+ const seen = /* @__PURE__ */ new Set();
241
+ let bytes = 0;
242
+ return entries.map((entry) => {
243
+ const { path, base64 } = entry ?? {};
244
+ if (typeof path !== "string" || !path || path.length > 1024 || /[\\:\u0000-\u001f\u007f]/u.test(path) || path.split("/").some((part) => !part || [
245
+ ".",
246
+ "..",
247
+ ".git",
248
+ "node_modules"
249
+ ].includes(part)) || seen.has(path) || typeof base64 !== "string" || Buffer.from(base64, "base64").toString("base64") !== base64) throw new Error("Invalid source tree entry.");
250
+ seen.add(path);
251
+ bytes += Buffer.byteLength(base64, "base64");
252
+ if (bytes > MAX_SOURCE_BYTES) throw new Error("Source tree exceeds 16 MiB.");
253
+ return [path, base64];
254
+ }).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
255
+ };
256
+ const retainEntriesUnlocked = async (entries, closure) => {
257
+ const text = JSON.stringify(closure ? {
258
+ ...closure,
259
+ entries
260
+ } : entries), id = hash(text), destination = join(snapshots, `${id}.json`);
261
+ try {
262
+ if (hash(await readRegular(destination, MAX_SOURCE_BYTES * 2)) !== id) throw new Error("Studio snapshot content mismatch.");
263
+ return openSnapshot(id);
264
+ } catch (error) {
265
+ if (error.code !== "ENOENT") throw error;
266
+ }
267
+ await writeAtomic(destination, text);
268
+ return openSnapshot(id);
269
+ };
270
+ const retainEntries = (entries, closure) => withLock(() => retainEntriesUnlocked(entries, closure));
271
+ const sources = {
272
+ open: openSnapshot,
273
+ ...kind === "instance" ? { retainTree: (entries) => withLock(() => retainEntriesUnlocked(checkEntries(entries))) } : {},
274
+ async capture(pin) {
275
+ if (!kitRootReal || typeof readInstalledKit !== "function") throw new Error("Installed Kit capture is not configured for this storage port.");
276
+ const installed = await readInstalledKit();
277
+ if (pin.name !== installed.name) throw new Error("Select the current installed Kit identity.");
278
+ const entries = [], ignored = /* @__PURE__ */ new Set([".git", "node_modules"]);
279
+ let total = 0, nodes = 0;
280
+ const walk = async (directory, base, prefix) => {
281
+ if (++nodes > 8192 || relative(base, directory).split(sep).length > 32) throw new Error("Studio source tree exceeds directory limits.");
282
+ for (const name of (await readdir(directory)).sort()) {
283
+ if (ignored.has(name)) continue;
284
+ const file = join(directory, name), info = await lstat(file);
285
+ if (info.isSymbolicLink()) throw new Error("Studio cannot snapshot source symlinks.");
286
+ if (info.isDirectory()) await walk(file, base, prefix);
287
+ else if (info.isFile()) {
288
+ if (total + info.size > MAX_SOURCE_BYTES || entries.length >= 4096) throw new Error("Kit preview snapshot exceeds 16 MiB or 4096 files.");
289
+ const bytes = await readFile(file);
290
+ total += bytes.length;
291
+ if (total > MAX_SOURCE_BYTES) throw new Error("Kit preview snapshot exceeds 16 MiB.");
292
+ entries.push([prefix + relative(base, file).split(sep).join("/"), bytes.toString("base64")]);
293
+ } else throw new Error("Studio sources must be regular files or directories.");
294
+ }
295
+ };
296
+ const sourceRoot = resolve(installed.sourceRoot ?? kitRootReal);
297
+ if (await realpath(sourceRoot) !== sourceRoot) throw new Error("Studio cannot snapshot source symlinks.");
298
+ if (await readFile(join(sourceRoot, "kit.yaml"), "utf8").catch((error) => {
299
+ if (error.code === "ENOENT") return void 0;
300
+ throw error;
301
+ }) === void 0) {
302
+ await walk(sourceRoot, sourceRoot, "");
303
+ return retainEntries(entries);
304
+ }
305
+ if (!Array.isArray(installed.skills)) throw new Error("Independent capture requires compiled mounted Bundles.");
306
+ const kitPath = `kits/${installed.name}`, bundles = [];
307
+ await walk(sourceRoot, sourceRoot, `${kitPath}/`);
308
+ for (const skill of installed.skills) {
309
+ const pin = {
310
+ name: skill.catalogName ?? skill.name,
311
+ version: String(skill.version)
312
+ };
313
+ const retained = await readInstalledBundle?.(pin);
314
+ const bundleRoot = resolve(retained?.resourceRoot ?? skill.resourceRoot ?? "");
315
+ if (!retained?.resourceRoot && !skill.resourceRoot) throw new Error("Mounted Bundle source root is missing.");
316
+ if (await realpath(bundleRoot) !== bundleRoot) throw new Error("Studio cannot snapshot source symlinks.");
317
+ const path = `bundles/${pin.name}`;
318
+ await walk(bundleRoot, bundleRoot, `${path}/`);
319
+ bundles.push({
320
+ ...pin,
321
+ path,
322
+ sourceId: retained?.sourceId ?? "companion:explicit-kit"
323
+ });
324
+ }
325
+ const checked = checkEntries(entries.map(([path, base64]) => ({
326
+ path,
327
+ base64
328
+ })));
329
+ return retainEntries(checked, {
330
+ format: "studio-source-closure/v1",
331
+ kitPath,
332
+ sourceId: "companion:explicit-kit",
333
+ bundles
334
+ });
335
+ }
336
+ };
337
+ return {
338
+ repository: kind === "panel" ? {
339
+ read,
340
+ list: repository.list
341
+ } : repository,
342
+ sources
343
+ };
344
+ }
345
+ //#endregion
346
+ //#region src/node/studio-instances.mjs
347
+ /** Independent local Studio sources; composition and identity rewriting belong to Core adapters. */
348
+ /** Byte-complete virtual snapshot over canonical retained S2 files; no filesystem extraction. */
349
+ function archiveSnapshot(root, entries, sourceId) {
350
+ const files = new Map(entries.map((entry) => [entry.path, Buffer.from(entry.base64, "base64")]));
351
+ const key = (path) => {
352
+ const value = relative(root, resolve(path));
353
+ if (isAbsolute(value) || value === ".." || value.startsWith(`..${sep}`)) throw new Error("Instance archive read escaped its retained tree.");
354
+ return value.split(sep).join("/");
355
+ };
356
+ const children = (path) => {
357
+ const selected = key(path), prefix = selected ? selected + "/" : "";
358
+ return [...new Set([...files.keys()].filter((file) => file.startsWith(prefix)).map((file) => file.slice(prefix.length).split("/")[0]))].sort();
359
+ };
360
+ return {
361
+ root,
362
+ sourceId,
363
+ listFiles: async () => [...files.keys()],
364
+ readBytes: async (path) => {
365
+ const bytes = files.get(key(path));
366
+ if (!bytes) throw new Error("Missing retained instance archive file.");
367
+ return Buffer.from(bytes);
368
+ },
369
+ fs: {
370
+ readText: async (path) => {
371
+ const bytes = files.get(key(path));
372
+ return bytes === void 0 ? void 0 : new TextDecoder("utf8", { fatal: true }).decode(bytes);
373
+ },
374
+ readResource: async (path) => {
375
+ const bytes = files.get(key(path));
376
+ return bytes === void 0 ? void 0 : readStudioArchiveResource(path, bytes);
377
+ },
378
+ listDir: async (path) => children(path),
379
+ isDirectory: async (path) => children(path).length > 0,
380
+ realPath: async (path) => {
381
+ key(path);
382
+ return resolve(path);
383
+ }
384
+ }
385
+ };
386
+ }
387
+ /**
388
+ * @typedef {{path:string,base64:string}} Entry
389
+ * @typedef {{name:string,version:string}} Pin
390
+ * @typedef {{zh:string,en:string}} Title
391
+ * @typedef {{snapshot:string,sourceId:string}} SourceRef
392
+ * @typedef {{bindingKey:string,bundle:Pin & {source:SourceRef,instanceId?:string,revision?:number}}} Mount
393
+ * Required trusted ports (never browser parameters):
394
+ * catalog.listBundles(): Promise<Array<Pin & {title:Title,runtimeName:string}>>
395
+ * catalog.captureKit(pin): Promise<{files:Entry[],title?:Title,mounts:Array<Pin & {bindingKey:string}>}>
396
+ * catalog.captureBundle(pin): Promise<{files:Entry[],title:Title,sourceId:string,runtimeName:string}>
397
+ * composition.forkBundle({files:Entry[],from:Pin,to:Pin,runtimeName:string}): Promise<Entry[]>
398
+ * composition.projectMounts({kit:Pin,kitFiles:Entry[],mounts:Array<Pin & {bindingKey:string}>,resolveBundle(pin)}): Promise<Entry[]>
399
+ * composition.validateKit({kit:Pin,kitFiles:Entry[],mounts:Mount[],resolveBundle(pin)}): Promise<void>
400
+ * resolveBundle returns {files:Entry[],sourceId:string} for retained exact pins only.
401
+ * Optional composition.preview returns the existing StudioPanelPreviewResult:
402
+ * Kit input adds {kind:'kit',bundle,revision,sampleSource} to validateKit input.
403
+ * Bundle input: {kind:'bundle',bundle,runtimeName,bundleFiles,sourceId,revision,sampleSource}.
404
+ * projectMounts owns kit.yaml AND generated XML. forkBundle changes ONLY skill.yaml identity;
405
+ * runtime name, SKILL frontmatter/body and validation paths remain byte-identical.
406
+ */
407
+ const MAX_TEXT = 1048576;
408
+ const pin = (value) => {
409
+ if (!value || typeof value.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(value.name) || typeof value.version !== "string" || !/^\d+\.\d+$/.test(value.version)) throw new Error("Select an exact source version.");
410
+ return {
411
+ name: value.name,
412
+ version: value.version
413
+ };
414
+ };
415
+ const same = (a, b) => a.name === b.name;
416
+ const runtimeNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
417
+ const runtimeNameOf = (value) => {
418
+ if (typeof value !== "string" || !runtimeNamePattern.test(value)) throw new Error("Trusted Bundle runtime name is missing or invalid.");
419
+ return value;
420
+ };
421
+ const titleOf = (value) => {
422
+ if (!value || typeof value.zh !== "string" || typeof value.en !== "string" || !value.zh.trim() || !value.en.trim() || value.zh.length > 200 || value.en.length > 200) throw new Error("Provide Chinese and English display names of at most 200 characters.");
423
+ return {
424
+ zh: value.zh.trim(),
425
+ en: value.en.trim()
426
+ };
427
+ };
428
+ const safePath = (path) => {
429
+ if (typeof path !== "string" || !path || path.length > 1024 || /[\\:\u0000-\u001f\u007f]/u.test(path) || path.split("/").some((part) => !part || [
430
+ ".",
431
+ "..",
432
+ ".git",
433
+ "node_modules",
434
+ "__proto__",
435
+ "constructor",
436
+ "prototype"
437
+ ].includes(part))) throw new Error("Invalid instance source path.");
438
+ return path;
439
+ };
440
+ const checkRevision = (record, expected) => {
441
+ if (!Number.isSafeInteger(expected) || record.revision !== expected) throw new Error("Instance changed elsewhere. Reopen it before saving.");
442
+ };
443
+ const active = (record) => {
444
+ if (record.deletedAt) throw new Error("This working copy is no longer available or has been moved to the system trash.");
445
+ return record;
446
+ };
447
+ const modern = (record) => {
448
+ active(record);
449
+ if (record.recordVersion !== 2) throw new Error("This legacy instance requires explicit migration before editing or mounting.");
450
+ return record;
451
+ };
452
+ const decode$1 = (base64) => {
453
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(base64, "base64"));
454
+ if (text.includes("\0")) throw new Error("Binary source files cannot be edited as text.");
455
+ return text;
456
+ };
457
+ const localPin = (kind, version, id = randomUUID()) => ({
458
+ name: `local_${kind}_${id.replaceAll("-", "_")}`,
459
+ version
460
+ });
461
+ const localSourceId = (kind, identity) => `local:${kind}:${identity.name}`;
462
+ const requireSourceId = (value) => {
463
+ if (typeof value !== "string" || !value || value.length > 4096 || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error("Trusted source identity is missing or invalid.");
464
+ return value;
465
+ };
466
+ const canonicalFiles = (files) => JSON.stringify(files.map(({ path, base64 }) => [path, base64]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
467
+ function blankSources(kind, identity, title) {
468
+ const entry = (path, content) => ({
469
+ path,
470
+ base64: Buffer.from(content, "utf8").toString("base64")
471
+ });
472
+ const frontmatter = stringify({
473
+ ...identity,
474
+ description: kind === "kit" ? "A new Design Kit being authored." : "A new design Bundle being authored."
475
+ });
476
+ if (kind === "kit") return [entry("mindset.md", "---\n" + frontmatter + "---\n\n# " + title.en + "\n\nDefine this Kit’s design purpose, domain judgment and Bundle selection guidance.\n\n<design_skills>\n</design_skills>\n"), entry("kit.yaml", stringify({
477
+ version: 1,
478
+ mounts: []
479
+ }))];
480
+ return [
481
+ entry("SKILL.md", "---\n" + frontmatter + "---\n\n# " + title.en + "\n\nThis is an unfinished authoring source. Define the user need, owned design facts, domain methods, failure handling and revision protocol before using this Bundle.\n\nKeep the [output contract](reference/output-contract.md), schema, validation and Panel aligned.\n"),
482
+ entry("skill.yaml", stringify({
483
+ ...identity,
484
+ title,
485
+ label: { ...title },
486
+ execution: { handoff: "must_edit" },
487
+ presentation: { mode: "declarative" }
488
+ })),
489
+ entry("schema.yaml", "# Define the domain-owned design facts before use.\ntype: object\nadditional_properties: false\nproperties: {}\n"),
490
+ entry("validation.yaml", stringify({
491
+ skill: identity.name,
492
+ rules: []
493
+ })),
494
+ entry("panel.yaml", "# Compose the shared components after defining the design facts.\nversion: 1\nsections: []\nfields: []\n"),
495
+ entry("reference/output-contract.md", "# Output contract\n\nThis Bundle is not yet defined. Specify owned facts, source evidence, validation and saved outputs alongside the method.\n"),
496
+ entry("reference/preview.json", "{}\n")
497
+ ];
498
+ }
499
+ async function createStudioInstances(options) {
500
+ const { catalog, composition, stateRoot } = options;
501
+ if (!catalog || ![
502
+ "listBundles",
503
+ "captureKit",
504
+ "captureBundle"
505
+ ].every((method) => typeof catalog[method] === "function") || !composition || ![
506
+ "forkBundle",
507
+ "projectMounts",
508
+ "validateKit"
509
+ ].every((method) => typeof composition[method] === "function")) throw new Error("Independent Studio catalog and Core composition ports are required.");
510
+ const { repository, sources } = await createStudioDraftPorts(options, "instance");
511
+ const entriesOf = async (snapshotId) => {
512
+ const snapshot = await sources.open(snapshotId), entries = [];
513
+ const walk = async (prefix) => {
514
+ for (const name of await snapshot.fs.listDir(join(snapshot.root, prefix))) {
515
+ const path = prefix ? `${prefix}/${name}` : name;
516
+ if (await snapshot.fs.isDirectory(join(snapshot.root, path))) await walk(path);
517
+ else entries.push({
518
+ path,
519
+ base64: (await snapshot.fs.readBytes(join(snapshot.root, path))).toString("base64")
520
+ });
521
+ }
522
+ };
523
+ await walk("");
524
+ return entries;
525
+ };
526
+ const filesOf = async (record) => {
527
+ const files = new Map((await entriesOf(record.baseSnapshot)).map((entry) => [entry.path, entry.base64]));
528
+ for (const [path, content] of Object.entries(record.overlays)) files.set(path, Buffer.from(content).toString("base64"));
529
+ return [...files].map(([path, base64]) => ({
530
+ path,
531
+ base64
532
+ }));
533
+ };
534
+ const references = (record, records) => records.filter((item) => !item.deletedAt && item.kind === "kit" && item.mounts.some((mount) => (mount.bundle.instanceId ?? mount.instanceId) === record.id || record.current === true && record.kind === "bundle" && mount.bundle.name === record.bundle.name)).map(({ id, title }) => ({
535
+ id,
536
+ title
537
+ }));
538
+ const projection = (record, records) => {
539
+ const { overlays: _overlays, importedFromSnapshot: _imported, ...result } = record;
540
+ return {
541
+ ...result,
542
+ refs: references(record, records),
543
+ ...record.recordVersion !== 2 ? { requiresMigration: true } : {}
544
+ };
545
+ };
546
+ const displayCopy = async (record) => {
547
+ if (record.recordVersion !== 2) return {};
548
+ const snapshot = await sources.open(record.baseSnapshot);
549
+ const text = async (path) => {
550
+ safePath(path);
551
+ let value;
552
+ try {
553
+ value = record.overlays[path] ?? await snapshot.fs.readText(join(snapshot.root, path));
554
+ } catch (error) {
555
+ if (error.code === "ENOENT") return void 0;
556
+ throw error;
557
+ }
558
+ return typeof value === "string" && Buffer.byteLength(value) <= MAX_TEXT ? value : void 0;
559
+ };
560
+ const source = await text(record.kind === "kit" ? "mindset.md" : "SKILL.md");
561
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source ?? "");
562
+ let meta = {}, translated;
563
+ try {
564
+ meta = frontmatter ? parse(frontmatter[1]) ?? {} : {};
565
+ } catch {}
566
+ if (record.kind === "kit") translated = meta.display_description;
567
+ else {
568
+ let locales = {};
569
+ try {
570
+ locales = parse(await text("panel.yaml") ?? "")?.i18n?.locales ?? {};
571
+ } catch {}
572
+ const readLocale = async (locale) => {
573
+ const path = locales[locale];
574
+ if (typeof path !== "string" || !/^locales\/[A-Za-z0-9._-]+\.json$/.test(path)) return void 0;
575
+ try {
576
+ return JSON.parse(await text(path) ?? "{}").bundleDescription;
577
+ } catch {
578
+ return;
579
+ }
580
+ };
581
+ translated = {
582
+ zh: await readLocale("zh-CN"),
583
+ en: await readLocale("en-US")
584
+ };
585
+ if (translated.zh === void 0 && translated.en === void 0) try {
586
+ translated = parse(await text("skill.yaml") ?? "")?.description;
587
+ } catch {}
588
+ }
589
+ return {
590
+ ...typeof meta.description === "string" ? { description: meta.description } : {},
591
+ ...translated && ["zh", "en"].every((language) => typeof translated[language] === "string" && translated[language].trim()) ? { displayDescription: {
592
+ zh: translated.zh,
593
+ en: translated.en
594
+ } } : {}
595
+ };
596
+ };
597
+ const candidates = async (records) => {
598
+ const result = [...(await catalog.listBundles().catch(() => [])).map((item) => ({
599
+ ...pin(item),
600
+ title: titleOf(item.title),
601
+ runtimeName: runtimeNameOf(item.runtimeName),
602
+ ...typeof item.description === "string" ? { description: item.description } : {},
603
+ ...item.displayDescription && ["zh", "en"].every((language) => typeof item.displayDescription[language] === "string" && item.displayDescription[language].trim()) ? { displayDescription: {
604
+ zh: item.displayDescription.zh,
605
+ en: item.displayDescription.en
606
+ } } : {}
607
+ })), ...await Promise.all(records.filter((item) => item.recordVersion === 2 && item.kind === "bundle" && !item.deletedAt).map(async (item) => ({
608
+ ...pin(item.bundle),
609
+ title: item.title,
610
+ runtimeName: runtimeNameOf(item.runtimeName),
611
+ instanceId: item.id,
612
+ revision: item.revision,
613
+ ...await displayCopy(item)
614
+ })))];
615
+ const unique = /* @__PURE__ */ new Map();
616
+ for (const item of result) if (!unique.get(item.name) || item.instanceId !== void 0) unique.set(item.name, item);
617
+ return [...unique.values()];
618
+ };
619
+ const identityDiagnostics = (record, files) => {
620
+ if (record.kind !== "bundle") return [];
621
+ const file = files.find((file) => file.path === "skill.yaml");
622
+ if (!file) return [];
623
+ let manifest;
624
+ try {
625
+ manifest = parse(decode$1(file.base64));
626
+ } catch {
627
+ return [];
628
+ }
629
+ if ((manifest?.identity ?? manifest?.name) === record.bundle.name) return [];
630
+ return [{
631
+ code: "instance.identity",
632
+ subject: "skill.yaml",
633
+ message: `skill.yaml:请设置 identity: ${record.bundle.name};name 与 SKILL.md 的运行名称保持一致。\nSet skill.yaml identity to ${record.bundle.name}. Keep name consistent with the runtime name in SKILL.md.`
634
+ }];
635
+ };
636
+ const displayDiagnostics = (record, diagnostics) => diagnostics.map((diagnostic) => {
637
+ if (record.kind !== "kit") return diagnostic;
638
+ if (/Mindset .*?(?:must contain|has an empty).*?<design_skills> catalog/.test(diagnostic.message)) return {
639
+ ...diagnostic,
640
+ subject: "mindset.md",
641
+ message: "mindset.md 的技能目录尚未完善:请保留方法正文,并按 kit.yaml 中的挂载补充一个 <design_skills> 目录。\nComplete one <design_skills> catalog in mindset.md using the mounts in kit.yaml, while preserving the method guidance."
642
+ };
643
+ return {
644
+ ...diagnostic,
645
+ message: diagnostic.message.replaceAll(record.kit.name, `${record.title.zh} / ${record.title.en}`)
646
+ };
647
+ });
648
+ const readinessDiagnostics = (record, diagnostics) => displayDiagnostics(record, diagnostics).map(({ subject, ...diagnostic }) => ({
649
+ ...diagnostic,
650
+ ...subject ? { path: subject } : {}
651
+ }));
652
+ const readiness = async (record) => {
653
+ if (!record.importedFromSnapshot) return {};
654
+ const files = await filesOf(record), paths = new Set(files.map((file) => file.path)), diagnostics = [];
655
+ const required = record.kind === "bundle" ? [
656
+ "SKILL.md",
657
+ "skill.yaml",
658
+ "schema.yaml",
659
+ "validation.yaml"
660
+ ] : ["mindset.md", "kit.yaml"];
661
+ for (const path of required) if (!paths.has(path)) diagnostics.push({
662
+ code: "instance.missing_source",
663
+ path,
664
+ message: `Missing ${path}. Add this source file to complete the definition.`,
665
+ severity: "error"
666
+ });
667
+ if (!diagnostics.length) try {
668
+ if (record.kind === "kit") {
669
+ for (const mount of record.mounts) if (!(await resolveMounts(record.mounts, record.current === true)(mount.bundle)).files.some((file) => file.path === "SKILL.md")) diagnostics.push({
670
+ code: "instance.missing_mount",
671
+ path: "kit.yaml",
672
+ message: `Missing exact Bundle ${mount.bundle.name}@${mount.bundle.version}. Repair the Bundle selection in mount management.`,
673
+ severity: "error"
674
+ });
675
+ if (!diagnostics.length) await composition.validateKit(await kitInput(record));
676
+ } else {
677
+ const identity = identityDiagnostics(record, files);
678
+ if (identity.length) return { readiness: {
679
+ ready: false,
680
+ diagnostics: readinessDiagnostics(record, identity)
681
+ } };
682
+ if (typeof composition.validateBundle !== "function") throw new Error("The host has not supplied the Core Bundle definition validator.");
683
+ await composition.validateBundle({
684
+ bundle: record.bundle,
685
+ bundleFiles: files,
686
+ sourceId: record.sourceId,
687
+ runtimeName: record.runtimeName
688
+ });
689
+ }
690
+ } catch (error) {
691
+ diagnostics.push({
692
+ code: "instance.definition",
693
+ message: error instanceof Error ? error.message : String(error),
694
+ severity: "error"
695
+ });
696
+ }
697
+ return { readiness: {
698
+ ready: diagnostics.length === 0,
699
+ diagnostics: readinessDiagnostics(record, diagnostics)
700
+ } };
701
+ };
702
+ const importedRuntime = (record, files) => {
703
+ if (record.kind !== "bundle" || !record.importedFromSnapshot) return {};
704
+ try {
705
+ const method = decode$1(files.find((file) => file.path === "SKILL.md")?.base64 ?? "");
706
+ const metadata = parse(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(method)?.[1] ?? "");
707
+ return { runtimeName: runtimeNameOf(metadata?.name) };
708
+ } catch {
709
+ return {};
710
+ }
711
+ };
712
+ const projectImported = async (record, files) => typeof composition.projectSourceIdentity === "function" ? composition.projectSourceIdentity({
713
+ kind: record.kind,
714
+ files,
715
+ identity: record[record.kind]
716
+ }) : files;
717
+ const describe = async (record, records = void 0) => {
718
+ records ??= await repository.list();
719
+ if (record.recordVersion !== 2) return projection(record, records);
720
+ const files = (await filesOf(record)).map((entry) => {
721
+ const bytes = Buffer.byteLength(entry.base64, "base64");
722
+ let editable = bytes <= MAX_TEXT;
723
+ try {
724
+ decode$1(entry.base64);
725
+ } catch {
726
+ editable = false;
727
+ }
728
+ return {
729
+ path: entry.path,
730
+ bytes,
731
+ editable
732
+ };
733
+ }).sort((a, b) => a.path.localeCompare(b.path));
734
+ return {
735
+ ...projection(record, records),
736
+ ...await displayCopy(record),
737
+ ...await readiness(record),
738
+ files,
739
+ ...record.kind === "kit" ? { availableBundles: await candidates(records) } : {}
740
+ };
741
+ };
742
+ const read = async (input) => describe(active(await repository.read(typeof input === "string" ? input : input.id)));
743
+ const mountRequests = (kitFiles, mounts) => {
744
+ const declaration = kitFiles.find((file) => file.path === "kit.yaml");
745
+ try {
746
+ return declaration ? parseKitMounts(decode$1(declaration.base64)) : mounts.map((mount) => pin(mount.bundle));
747
+ } catch {
748
+ return mounts.map((mount) => pin(mount.bundle));
749
+ }
750
+ };
751
+ const resolveMounts = (mounts, currentLibrary = false) => async (requested) => {
752
+ const selected = mounts.find((mount) => same(mount.bundle, pin(requested)));
753
+ if (!selected) throw new Error("Core requested a Bundle outside the selected exact mounts.");
754
+ if (selected.bundle.instanceId) {
755
+ const current = modern(await repository.read(selected.bundle.instanceId));
756
+ if (current.kind !== "bundle" || current.bundle.name !== selected.bundle.name) throw new Error("Mounted Bundle identity changed.");
757
+ return {
758
+ files: await filesOf(current),
759
+ sourceId: current.sourceId
760
+ };
761
+ }
762
+ const current = (await repository.list()).find((record) => !record.deletedAt && record.current === true && record.kind === "bundle" && record.bundle.name === selected.bundle.name);
763
+ if (current) return {
764
+ files: await filesOf(current),
765
+ sourceId: current.sourceId
766
+ };
767
+ if (currentLibrary && selected.bundle.version !== requested.version) return catalog.captureBundle(pin(requested));
768
+ return {
769
+ files: await entriesOf(selected.bundle.source.snapshot),
770
+ sourceId: selected.bundle.source.sourceId
771
+ };
772
+ };
773
+ const kitInput = async (record) => ({
774
+ kit: record.kit,
775
+ kitFiles: await filesOf(record),
776
+ mounts: record.mounts,
777
+ resolveBundle: resolveMounts(record.mounts, record.current === true)
778
+ });
779
+ const retain = async (tx, files, sourceId) => {
780
+ requireSourceId(sourceId);
781
+ return {
782
+ snapshot: (await tx.retainTree(files)).id,
783
+ sourceId
784
+ };
785
+ };
786
+ const mountOne = async (tx, requested, available) => {
787
+ const selected = available.find((item) => same(item, pin(requested)));
788
+ if (!selected) throw new Error(`Bundle ${requested.name}@${requested.version} is unavailable in the global catalog.`);
789
+ const bindingKey = runtimeNameOf(requested.bindingKey ?? selected.runtimeName);
790
+ if (bindingKey !== selected.runtimeName) throw new Error("Bundle binding key must preserve its runtime name.");
791
+ if (selected.instanceId) {
792
+ if (requested.instanceId !== void 0 && requested.instanceId !== selected.instanceId) throw new Error("Bundle instance identity does not match its pin.");
793
+ const record = modern(await tx.read(selected.instanceId));
794
+ return {
795
+ bindingKey,
796
+ bundle: {
797
+ ...record.bundle,
798
+ instanceId: record.id,
799
+ revision: record.revision,
800
+ source: await retain(tx, await filesOf(record), record.sourceId)
801
+ }
802
+ };
803
+ }
804
+ if (requested.instanceId !== void 0 || requested.revision !== void 0) throw new Error("Installed Bundle pins do not accept local instance fields.");
805
+ const captured = await catalog.captureBundle(pin(requested));
806
+ return {
807
+ bindingKey,
808
+ bundle: {
809
+ ...pin(requested),
810
+ source: await retain(tx, captured.files, captured.sourceId)
811
+ }
812
+ };
813
+ };
814
+ const projectedKit = async (record, mounts) => {
815
+ const kitFiles = await composition.projectMounts({
816
+ allowEmptyDraft: true,
817
+ kit: record.kit,
818
+ kitFiles: await filesOf(record),
819
+ mounts: mounts.map((mount) => ({
820
+ bindingKey: mount.bindingKey,
821
+ ...pin(mount.bundle)
822
+ })),
823
+ resolveBundle: resolveMounts(mounts, record.current === true)
824
+ });
825
+ if (kitFiles.some((entry) => entry.path.startsWith("skills/"))) throw new Error("Kit-owned sources cannot contain copied Bundle trees.");
826
+ return kitFiles;
827
+ };
828
+ const newTitle = (input, base) => titleOf(input ?? {
829
+ zh: `${base.zh} 副本`,
830
+ en: `${base.en} copy`
831
+ });
832
+ const prepareFiles = async (current, input) => {
833
+ const before = await filesOf(current), entries = new Map(before.map((file) => [file.path, file.base64])), diff = [];
834
+ let diffBytes = 0;
835
+ for (const { path, content } of input.changes) {
836
+ const original = entries.get(path);
837
+ if (original === void 0 && content === null) throw new Error(`Cannot remove missing source file ${path}.`);
838
+ if (original !== void 0 && Buffer.byteLength(original, "base64") > MAX_TEXT) throw new Error("Text editing is limited to 1 MiB per existing file.");
839
+ const previous = original === void 0 ? void 0 : decode$1(original);
840
+ diffBytes += Buffer.byteLength(previous ?? "") + Buffer.byteLength(content ?? "");
841
+ if (diffBytes > STUDIO_INSTANCE_CHANGE_LIMITS.diffBytes) throw new Error("Instance diff exceeds the 8 MiB text budget.");
842
+ if (current.kind === "kit" && (path === "kit.yaml" || path.startsWith("skills/"))) throw new Error("Use Bundle mount management to edit the Kit declaration; Kit copies contain only Kit-owned files.");
843
+ if (content === null) entries.delete(path);
844
+ else entries.set(path, Buffer.from(content, "utf8").toString("base64"));
845
+ if (previous !== (content ?? void 0)) diff.push({
846
+ path,
847
+ ...previous === void 0 ? {} : { before: previous },
848
+ ...content === null ? {} : { after: content }
849
+ });
850
+ }
851
+ const files = [...entries].map(([path, base64]) => ({
852
+ path,
853
+ base64
854
+ }));
855
+ if (files.length > 4096 || files.reduce((bytes, file) => bytes + Buffer.byteLength(file.base64, "base64"), 0) > 16777216) throw new Error("Instance source tree exceeds 4096 files or 16 MiB.");
856
+ const paths = new Set(entries.keys());
857
+ for (const path of paths) {
858
+ const parts = path.split("/");
859
+ for (let length = 1; length < parts.length; length++) if (paths.has(parts.slice(0, length).join("/"))) throw new Error("Instance source file conflicts with a directory.");
860
+ }
861
+ const dependencyRequests = current.kind === "kit" ? mountRequests(files, current.mounts) : [];
862
+ const digest = createHash("sha256").update(JSON.stringify({
863
+ id: current.id,
864
+ baseRevision: current.revision,
865
+ kind: current.kind,
866
+ pin: current[current.kind],
867
+ runtimeName: current.runtimeName,
868
+ sourceId: current.sourceId,
869
+ mounts: await Promise.all(dependencyRequests.map(async (requested) => ({
870
+ name: requested.name,
871
+ source: await resolveMounts(current.mounts, current.current === true)(requested)
872
+ }))),
873
+ before: canonicalFiles(before),
874
+ after: canonicalFiles(files),
875
+ bundle: input.bundle,
876
+ sampleSource: input.sampleSource
877
+ })).digest("hex");
878
+ return {
879
+ files,
880
+ diff: diff.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
881
+ digest
882
+ };
883
+ };
884
+ const previewChanges = async (current, input, prepared) => {
885
+ const revision = current.revision + 1;
886
+ let result;
887
+ try {
888
+ if (typeof composition.preview !== "function") throw new Error("The host has not supplied the Core preview adapter.");
889
+ if (current.kind === "bundle") {
890
+ if (input.bundle && !same(input.bundle, current.bundle)) throw new Error("Select this exact Bundle instance.");
891
+ const identity = identityDiagnostics(current, prepared.files);
892
+ result = identity.length ? { diagnostics: identity } : await composition.preview({
893
+ kind: "bundle",
894
+ bundle: current.bundle,
895
+ runtimeName: importedRuntime(current, prepared.files).runtimeName ?? current.runtimeName,
896
+ bundleFiles: prepared.files,
897
+ sourceId: current.sourceId,
898
+ revision,
899
+ sampleSource: input.sampleSource
900
+ });
901
+ } else {
902
+ const bundle = pin(input.bundle);
903
+ if (!current.mounts.some((mount) => same(mount.bundle, bundle))) throw new Error("Select an exact mounted Bundle.");
904
+ result = await composition.preview({
905
+ kind: "kit",
906
+ kit: current.kit,
907
+ kitFiles: prepared.files,
908
+ mounts: current.mounts,
909
+ resolveBundle: resolveMounts(current.mounts, current.current === true),
910
+ bundle,
911
+ revision,
912
+ sampleSource: input.sampleSource
913
+ });
914
+ }
915
+ if (!result || !Array.isArray(result.diagnostics)) throw new Error("Core preview did not return a diagnostic result.");
916
+ if (result.preview && result.preview.revision !== revision) throw new Error("Core preview revision does not match this candidate.");
917
+ } catch (error) {
918
+ result = { diagnostics: [{
919
+ code: "instance.candidate",
920
+ message: error instanceof Error ? error.message : String(error)
921
+ }] };
922
+ }
923
+ return {
924
+ id: current.id,
925
+ baseRevision: current.revision,
926
+ revision,
927
+ digest: prepared.digest,
928
+ diff: prepared.diff,
929
+ valid: !!result.preview && !result.diagnostics.some((entry) => entry.severity !== "warning"),
930
+ diagnostics: displayDiagnostics(current, result.diagnostics),
931
+ ...result.preview ? { preview: result.preview } : {}
932
+ };
933
+ };
934
+ const definitionOnly = (current, input, files) => {
935
+ if (current.kind === "kit") return input.bundle === void 0;
936
+ try {
937
+ const manifest = parse(decode$1(files.find((file) => file.path === "skill.yaml")?.base64 ?? ""));
938
+ return manifest && typeof manifest === "object" && !Array.isArray(manifest) && (manifest.presentation === void 0 || manifest.presentation?.mode === "none");
939
+ } catch {
940
+ return false;
941
+ }
942
+ };
943
+ const definitionDiagnostics = async (current, input, files) => {
944
+ const diagnostics = [];
945
+ try {
946
+ if (current.kind === "kit") await composition.validateKit({
947
+ kit: current.kit,
948
+ kitFiles: files,
949
+ mounts: current.mounts,
950
+ resolveBundle: resolveMounts(current.mounts, current.current === true)
951
+ });
952
+ else {
953
+ if (input.bundle && !same(input.bundle, current.bundle)) throw new Error("Select this exact Bundle instance.");
954
+ diagnostics.push(...identityDiagnostics(current, files));
955
+ if (!diagnostics.length) {
956
+ if (typeof composition.validateBundle !== "function") throw new Error("The host has not supplied the Core Bundle definition validator.");
957
+ await composition.validateBundle({
958
+ bundle: current.bundle,
959
+ bundleFiles: files,
960
+ sourceId: current.sourceId,
961
+ runtimeName: importedRuntime(current, files).runtimeName ?? current.runtimeName
962
+ });
963
+ }
964
+ }
965
+ } catch (error) {
966
+ diagnostics.push({
967
+ code: "instance.definition",
968
+ message: error instanceof Error ? error.message : String(error)
969
+ });
970
+ }
971
+ return diagnostics;
972
+ };
973
+ const validateChanges = async (current, input, prepared) => {
974
+ if (!definitionOnly(current, input, prepared.files)) return previewChanges(current, input, prepared);
975
+ const diagnostics = await definitionDiagnostics(current, input, prepared.files);
976
+ return {
977
+ id: current.id,
978
+ baseRevision: current.revision,
979
+ revision: current.revision + 1,
980
+ digest: prepared.digest,
981
+ diff: prepared.diff,
982
+ valid: diagnostics.length === 0,
983
+ diagnostics: displayDiagnostics(current, diagnostics)
984
+ };
985
+ };
986
+ return {
987
+ read,
988
+ async captureKit({ id, expectedRevision } = {}) {
989
+ return repository.transaction(async (tx) => {
990
+ const record = modern(await tx.read(id));
991
+ if (expectedRevision !== void 0) checkRevision(record, expectedRevision);
992
+ if (record.kind !== "kit") throw new Error("Select a Kit source.");
993
+ const captured = /* @__PURE__ */ new Map(), kitFiles = await filesOf(record), resolveBundle = resolveMounts(record.mounts, record.current === true);
994
+ for (const requested of mountRequests(kitFiles, record.mounts)) captured.set(requested.name, await resolveBundle(requested));
995
+ return {
996
+ kit: record.kit,
997
+ kitFiles,
998
+ mounts: structuredClone(record.mounts),
999
+ resolveBundle: async (requested) => captured.get(requested.name)
1000
+ };
1001
+ });
1002
+ },
1003
+ async loadKit(input) {
1004
+ if (typeof composition.loadKit !== "function") throw new Error("The host has not supplied the Core Kit loader.");
1005
+ return composition.loadKit(await this.captureKit(input));
1006
+ },
1007
+ async editCurrent(input) {
1008
+ if (!["kit", "bundle"].includes(input.kind)) throw new Error("Select a Kit or Bundle.");
1009
+ const selected = pin(input[input.kind]);
1010
+ const record = await repository.transaction(async (tx) => {
1011
+ const records = await tx.list();
1012
+ const existing = records.find((item) => !item.deletedAt && item.kind === input.kind && item.current === true && item[item.kind].name === selected.name);
1013
+ if (existing) return existing;
1014
+ const captured = input.kind === "kit" ? await catalog.captureKit(selected) : await catalog.captureBundle(selected);
1015
+ const sourceId = input.kind === "bundle" ? captured.sourceId : `local:kit:${selected.name}`;
1016
+ const source = await retain(tx, captured.files, sourceId);
1017
+ const mounts = [];
1018
+ if (input.kind === "kit") {
1019
+ const available = await candidates(records);
1020
+ for (const mounted of captured.mounts) mounts.push(await mountOne(tx, mounted, available));
1021
+ }
1022
+ return tx.create({
1023
+ recordVersion: 2,
1024
+ current: true,
1025
+ kind: input.kind,
1026
+ [input.kind]: selected,
1027
+ title: captured.title ?? {
1028
+ zh: selected.name,
1029
+ en: selected.name
1030
+ },
1031
+ source: { [input.kind]: selected },
1032
+ sourceId,
1033
+ baseSnapshot: source.snapshot,
1034
+ overlays: {},
1035
+ mounts,
1036
+ ...input.kind === "bundle" ? { runtimeName: captured.runtimeName } : {},
1037
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1038
+ });
1039
+ });
1040
+ return describe(record);
1041
+ },
1042
+ /** Unsaved, bounded source candidate; diff and real preview use the same bytes. */
1043
+ async exportArchive(request) {
1044
+ const input = structuredClone(request);
1045
+ return repository.transaction(async (tx) => {
1046
+ const current = active(await tx.read(input.id));
1047
+ if (current.revision !== input.expectedRevision) throw new Error("Instance revision changed before archive export.");
1048
+ if (current.recordVersion !== 2) throw new Error("Migrate the working copy before archive export.");
1049
+ const root = resolve(stateRoot, "instance-archive-view", current.id);
1050
+ const snapshot = archiveSnapshot(root, await filesOf(current), `studio-instance:${current.id}`);
1051
+ if (current.kind === "bundle") snapshot.bundle = pin(current.bundle);
1052
+ else {
1053
+ snapshot.bundles = [];
1054
+ const resolveBundle = resolveMounts(current.mounts, current.current === true);
1055
+ for (const requested of mountRequests(await filesOf(current), current.mounts)) {
1056
+ const captured = await resolveBundle(requested);
1057
+ snapshot.bundles.push({
1058
+ ...pin(requested),
1059
+ sourceId: captured.sourceId,
1060
+ snapshot: archiveSnapshot(resolve(root, "..", `${current.id}-bundle-${requested.name}`), captured.files, captured.sourceId)
1061
+ });
1062
+ }
1063
+ }
1064
+ return exportStudioSourceArchive(snapshot, {
1065
+ kind: current.kind,
1066
+ pin: current.kind === "kit" ? current.kit : current.bundle,
1067
+ title: current.title,
1068
+ provenance: input.provenance,
1069
+ ...input.dependencies ? { dependencies: input.dependencies } : {}
1070
+ });
1071
+ });
1072
+ },
1073
+ async prepareChanges(request) {
1074
+ const input = normalizeStudioInstanceChanges(request);
1075
+ return repository.transaction(async (tx) => {
1076
+ const current = modern(await tx.read(input.id));
1077
+ checkRevision(current, input.expectedRevision);
1078
+ return validateChanges(current, input, await prepareFiles(current, input));
1079
+ });
1080
+ },
1081
+ /** One snapshot pointer + one existing atomic record CAS, never per-file writes. */
1082
+ async saveChanges(request) {
1083
+ const input = normalizeStudioInstanceChanges(request), mode = request.mode ?? "draft", digest = request.digest;
1084
+ if (!["draft", "validated"].includes(mode)) throw new Error("Select draft or validated save mode.");
1085
+ if ((mode === "validated" || digest !== void 0) && (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest))) throw new Error("Select the exact prepared candidate digest.");
1086
+ let receipt;
1087
+ const record = await repository.transaction(async (tx) => {
1088
+ const current = modern(await tx.read(input.id));
1089
+ checkRevision(current, input.expectedRevision);
1090
+ const prepared = await prepareFiles(current, input);
1091
+ if (digest !== void 0 && digest !== prepared.digest) throw new Error("Candidate source digest changed. Prepare and review the current changes again.");
1092
+ let validation = "not_requested";
1093
+ if (mode === "validated") {
1094
+ const sourceOnly = definitionOnly(current, input, prepared.files);
1095
+ const candidate = await validateChanges(current, input, prepared);
1096
+ if (!candidate.valid) throw new Error(`Candidate failed Core ${sourceOnly ? "definition" : "preview"}: ${candidate.diagnostics.map((entry) => `${entry.code}: ${entry.message}`).join("; ") || "no valid declarative preview"}`);
1097
+ validation = sourceOnly ? "definition_passed" : "preview_passed";
1098
+ }
1099
+ const retained = await retain(tx, prepared.files, current.sourceId);
1100
+ const saved = await tx.replace(current.id, input.expectedRevision, {
1101
+ ...current,
1102
+ ...importedRuntime(current, prepared.files),
1103
+ baseSnapshot: retained.snapshot,
1104
+ overlays: {}
1105
+ });
1106
+ receipt = {
1107
+ baseRevision: current.revision,
1108
+ revision: saved.revision,
1109
+ digest: prepared.digest,
1110
+ mode,
1111
+ validation
1112
+ };
1113
+ return saved;
1114
+ });
1115
+ return {
1116
+ ...await describe(record),
1117
+ changeReceipt: receipt
1118
+ };
1119
+ },
1120
+ async list({ kind } = {}) {
1121
+ if (kind !== void 0 && !["kit", "bundle"].includes(kind)) throw new Error("Unknown instance kind.");
1122
+ const records = await repository.list();
1123
+ return Promise.all(records.filter((record) => !record.deletedAt && (!kind || record.kind === kind)).map(async (record) => ({
1124
+ ...projection(record, records),
1125
+ ...await displayCopy(record),
1126
+ ...await readiness(record)
1127
+ })));
1128
+ },
1129
+ async listBundles() {
1130
+ return candidates(await repository.list());
1131
+ },
1132
+ async importSources(request) {
1133
+ const input = structuredClone(request);
1134
+ if (!input || !["kit", "bundle"].includes(input.kind) || !Array.isArray(input.files)) throw new Error("Provide Kit or Bundle source files.");
1135
+ const kind = input.kind;
1136
+ for (const file of input.files) safePath(file?.path);
1137
+ const paths = new Set(input.files.map((file) => file.path));
1138
+ for (const path of kind === "bundle" ? ["SKILL.md"] : ["mindset.md", "kit.yaml"]) if (!paths.has(path)) throw new Error(`Missing required import source ${path}.`);
1139
+ const metadata = (path) => {
1140
+ try {
1141
+ const text = decode$1(input.files.find((file) => file.path === path)?.base64 ?? "");
1142
+ return parse(path.endsWith(".md") ? /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1] ?? "" : text) ?? {};
1143
+ } catch {
1144
+ return {};
1145
+ }
1146
+ };
1147
+ const method = metadata(kind === "bundle" ? "SKILL.md" : "mindset.md"), manifest = metadata(kind === "bundle" ? "skill.yaml" : "kit.yaml");
1148
+ const version = /^\d+\.\d+$/.test(String(manifest.version)) ? String(manifest.version) : /^\d+\.\d+$/.test(String(method.version)) ? String(method.version) : "1.0";
1149
+ const identity = localPin(kind, version), sourceId = localSourceId(kind, identity);
1150
+ const runtimeName = [method.name, manifest.name].find((value) => typeof value === "string" && runtimeNamePattern.test(value)) ?? identity.name;
1151
+ const label = typeof method.name === "string" && method.name.trim() ? method.name.trim().slice(0, 180) : kind === "kit" ? "Imported Kit" : "Imported Bundle";
1152
+ const inferredTitle = ["zh", "en"].every((language) => typeof manifest.title?.[language] === "string" && manifest.title[language].trim() && manifest.title[language].length <= 200) ? manifest.title : {
1153
+ zh: label,
1154
+ en: label
1155
+ };
1156
+ const baseTitle = titleOf(input.title ?? inferredTitle);
1157
+ const created = await repository.transaction(async (tx) => {
1158
+ const original = await retain(tx, input.files, sourceId);
1159
+ const labels = [...(await tx.list()).filter((record) => !record.deletedAt).map((record) => record.title), ...(await catalog.listBundles()).map((item) => item.title)];
1160
+ let title = baseTitle, suffix = 2;
1161
+ while (labels.some((item) => item.zh === title.zh || item.en === title.en)) {
1162
+ title = titleOf({
1163
+ zh: `${baseTitle.zh.slice(0, 180)} (${suffix})`,
1164
+ en: `${baseTitle.en.slice(0, 180)} (${suffix})`
1165
+ });
1166
+ suffix++;
1167
+ }
1168
+ let files = input.files, mounts = [];
1169
+ if (kind === "kit") {
1170
+ const consumed = /* @__PURE__ */ new Set(), declared = Array.isArray(manifest.mounts) ? manifest.mounts : [];
1171
+ for (const item of declared) {
1172
+ let selected, bindingKey;
1173
+ try {
1174
+ selected = pin(item);
1175
+ bindingKey = runtimeNameOf(item.bindingKey);
1176
+ } catch {
1177
+ continue;
1178
+ }
1179
+ if (mounts.some((mount) => same(mount.bundle, selected) || mount.bindingKey === bindingKey)) continue;
1180
+ const prefix = [
1181
+ `bundles/${selected.name}/${selected.version}/`,
1182
+ `bundles/${selected.name}/`,
1183
+ `skills/${bindingKey}/`
1184
+ ].find((prefix) => input.files.some((file) => file.path.startsWith(prefix)));
1185
+ let bundleFiles = prefix ? input.files.filter((file) => file.path.startsWith(prefix)).map((file) => {
1186
+ consumed.add(file.path);
1187
+ return {
1188
+ ...file,
1189
+ path: file.path.slice(prefix.length)
1190
+ };
1191
+ }) : [];
1192
+ let mountedSourceId = `import:bundle:${selected.name}@${selected.version}`;
1193
+ if (!prefix) try {
1194
+ const capture = await catalog.captureBundle(selected);
1195
+ bundleFiles = capture.files;
1196
+ mountedSourceId = capture.sourceId;
1197
+ } catch {}
1198
+ mounts.push({
1199
+ bindingKey,
1200
+ bundle: {
1201
+ ...selected,
1202
+ source: await retain(tx, bundleFiles, mountedSourceId)
1203
+ }
1204
+ });
1205
+ }
1206
+ files = input.files.filter((file) => !consumed.has(file.path));
1207
+ }
1208
+ const record = {
1209
+ recordVersion: 2,
1210
+ kind,
1211
+ [kind]: identity,
1212
+ title,
1213
+ source: {},
1214
+ sourceId,
1215
+ baseSnapshot: original.snapshot,
1216
+ importedFromSnapshot: original.snapshot,
1217
+ overlays: {},
1218
+ mounts,
1219
+ ...kind === "bundle" ? { runtimeName } : {},
1220
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1221
+ };
1222
+ files = await projectImported(record, files);
1223
+ record.baseSnapshot = (await retain(tx, files, sourceId)).snapshot;
1224
+ return tx.create(record);
1225
+ });
1226
+ return describe(created);
1227
+ },
1228
+ async create(input) {
1229
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => !["kind", "title"].includes(key)) || !["kit", "bundle"].includes(input.kind)) throw new Error("Provide a new Kit or Bundle kind and display title.");
1230
+ const kind = input.kind, title = titleOf(input.title), identity = localPin(kind, "1.0");
1231
+ const sourceId = localSourceId(kind, identity), initial = blankSources(kind, identity, title);
1232
+ const files = kind === "kit" ? await composition.projectMounts({
1233
+ allowEmptyDraft: true,
1234
+ kit: identity,
1235
+ kitFiles: initial,
1236
+ mounts: [],
1237
+ resolveBundle: async () => {
1238
+ throw new Error("Blank Kit creation cannot resolve installed Bundles.");
1239
+ }
1240
+ }) : initial;
1241
+ const created = await repository.transaction(async (tx) => {
1242
+ const source = await retain(tx, files, sourceId);
1243
+ return tx.create({
1244
+ recordVersion: 2,
1245
+ kind,
1246
+ [kind]: identity,
1247
+ title,
1248
+ source: {},
1249
+ sourceId,
1250
+ baseSnapshot: source.snapshot,
1251
+ overlays: {},
1252
+ mounts: [],
1253
+ createdBlank: true,
1254
+ ...kind === "bundle" ? { runtimeName: identity.name } : {},
1255
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1256
+ });
1257
+ });
1258
+ return describe(created);
1259
+ },
1260
+ async copy(input) {
1261
+ if (!["kit", "bundle"].includes(input.kind)) throw new Error("Select a Kit or Bundle instance.");
1262
+ const created = await repository.transaction(async (tx) => {
1263
+ const original = input.sourceId ? modern(await tx.read(input.sourceId)) : void 0;
1264
+ if (original && original.kind !== input.kind) throw new Error("The source instance kind does not match.");
1265
+ if (original && input.expectedRevision !== void 0) checkRevision(original, input.expectedRevision);
1266
+ let files, origin, baseTitle, mounts = [], sourcePin, runtimeName;
1267
+ if (original) {
1268
+ files = await filesOf(original);
1269
+ sourcePin = original[original.kind];
1270
+ origin = original.source;
1271
+ baseTitle = original.title;
1272
+ mounts = structuredClone(original.mounts);
1273
+ runtimeName = original.runtimeName;
1274
+ } else if (input.kind === "bundle") {
1275
+ sourcePin = pin(input.bundle);
1276
+ const captured = await catalog.captureBundle(sourcePin);
1277
+ requireSourceId(captured.sourceId);
1278
+ runtimeName = runtimeNameOf(captured.runtimeName);
1279
+ files = captured.files;
1280
+ baseTitle = titleOf(captured.title);
1281
+ origin = { bundle: sourcePin };
1282
+ } else {
1283
+ sourcePin = pin(input.kit);
1284
+ const captured = await catalog.captureKit(sourcePin);
1285
+ files = captured.files;
1286
+ origin = { kit: sourcePin };
1287
+ const label = sourcePin.name.replace(/[_-]+/g, " ");
1288
+ baseTitle = captured.title ?? {
1289
+ zh: label,
1290
+ en: label
1291
+ };
1292
+ const available = await candidates(await tx.list());
1293
+ for (const mounted of captured.mounts) mounts.push(await mountOne(tx, mounted, available));
1294
+ }
1295
+ const identity = localPin(input.kind, sourcePin.version), sourceId = localSourceId(input.kind, identity);
1296
+ if (input.kind === "kit" && files.some((file) => file.path.startsWith("skills/"))) throw new Error("Kit capture must contain only Kit-owned files; legacy whole-Kit sources require explicit migration.");
1297
+ if (input.kind === "bundle") files = original?.importedFromSnapshot ? await projectImported({
1298
+ kind: "bundle",
1299
+ bundle: identity
1300
+ }, files) : await composition.forkBundle({
1301
+ files,
1302
+ from: sourcePin,
1303
+ to: identity,
1304
+ runtimeName
1305
+ });
1306
+ const source = await retain(tx, files, sourceId);
1307
+ const record = {
1308
+ recordVersion: 2,
1309
+ kind: input.kind,
1310
+ [input.kind]: identity,
1311
+ title: newTitle(input.title, baseTitle),
1312
+ source: origin,
1313
+ sourceId,
1314
+ baseSnapshot: source.snapshot,
1315
+ overlays: {},
1316
+ mounts,
1317
+ ...input.kind === "bundle" ? { runtimeName } : {},
1318
+ ...original?.createdBlank ? { createdBlank: true } : {},
1319
+ ...original?.importedFromSnapshot ? { importedFromSnapshot: original.importedFromSnapshot } : {},
1320
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1321
+ ...original ? { forkedFrom: {
1322
+ id: original.id,
1323
+ revision: original.revision,
1324
+ pin: sourcePin
1325
+ } } : {}
1326
+ };
1327
+ if (record.kind === "kit") record.baseSnapshot = (await retain(tx, record.importedFromSnapshot ? await projectImported(record, files) : await projectedKit(record, mounts), sourceId)).snapshot;
1328
+ return tx.create(record);
1329
+ });
1330
+ return describe(created);
1331
+ },
1332
+ async rename({ id, expectedRevision, title }) {
1333
+ const nextTitle = titleOf(title);
1334
+ const record = await repository.transaction(async (tx) => {
1335
+ const current = modern(await tx.read(id));
1336
+ checkRevision(current, expectedRevision);
1337
+ return tx.replace(id, expectedRevision, {
1338
+ ...current,
1339
+ title: nextTitle
1340
+ });
1341
+ });
1342
+ return describe(record);
1343
+ },
1344
+ async updateMounts({ id, expectedRevision, bundles }) {
1345
+ if (!Array.isArray(bundles) || bundles.length > 4096) throw new Error("Provide a bounded Bundle selection.");
1346
+ if (new Set(bundles.map((item) => pin(item).name)).size !== bundles.length) throw new Error("A Kit can mount each Bundle only once.");
1347
+ const record = await repository.transaction(async (tx) => {
1348
+ const current = modern(await tx.read(id));
1349
+ checkRevision(current, expectedRevision);
1350
+ if (current.kind !== "kit") throw new Error("Mount management requires a local Kit instance.");
1351
+ const available = await candidates(await tx.list()), mounts = [];
1352
+ for (const selected of bundles) {
1353
+ const existing = current.mounts.find((mount) => same(mount.bundle, pin(selected)) && (selected.bindingKey === void 0 || selected.bindingKey === mount.bindingKey) && (selected.instanceId === void 0 || selected.instanceId === mount.bundle.instanceId) && (selected.revision === void 0 || selected.revision === mount.bundle.revision));
1354
+ let mounted = existing;
1355
+ if (existing && current.importedFromSnapshot) {
1356
+ const files = await entriesOf(existing.bundle.source.snapshot);
1357
+ if (available.some((item) => same(item, selected))) {
1358
+ let incomplete = !files.some((file) => file.path === "SKILL.md");
1359
+ if (!incomplete && typeof composition.validateBundle === "function") try {
1360
+ await composition.validateBundle({
1361
+ bundle: pin(existing.bundle),
1362
+ bundleFiles: files,
1363
+ sourceId: existing.bundle.source.sourceId,
1364
+ runtimeName: existing.bindingKey
1365
+ });
1366
+ } catch {
1367
+ incomplete = true;
1368
+ }
1369
+ if (incomplete) try {
1370
+ mounted = await mountOne(tx, selected, available);
1371
+ } catch {}
1372
+ }
1373
+ }
1374
+ mounts.push(mounted ?? await mountOne(tx, selected, available));
1375
+ }
1376
+ if (new Set(mounts.map((mount) => mount.bindingKey)).size !== mounts.length) throw new Error("Bundle binding keys must be unique.");
1377
+ const source = await retain(tx, await projectedKit(current, mounts), current.sourceId);
1378
+ return tx.replace(id, expectedRevision, {
1379
+ ...current,
1380
+ mounts,
1381
+ baseSnapshot: source.snapshot,
1382
+ overlays: {}
1383
+ });
1384
+ });
1385
+ return describe(record);
1386
+ },
1387
+ async remove({ id, expectedRevision }) {
1388
+ return repository.transaction(async (tx) => {
1389
+ const current = active(await tx.read(id));
1390
+ checkRevision(current, expectedRevision);
1391
+ const refs = references(current, await tx.list());
1392
+ if (refs.length) throw new Error(`This Bundle is referenced by ${refs.map((item) => item.title.en).join(", ")}. Referenced instances are protected from deletion.`);
1393
+ return tx.trash(id, expectedRevision);
1394
+ });
1395
+ },
1396
+ async readFile({ id, path }) {
1397
+ safePath(path);
1398
+ const record = modern(await repository.read(id)), file = (await filesOf(record)).find((entry) => entry.path === path);
1399
+ if (!file) throw new Error("Instance source file does not exist.");
1400
+ if (Buffer.byteLength(file.base64, "base64") > MAX_TEXT) throw new Error("Text editing is limited to 1 MiB per file.");
1401
+ return {
1402
+ id,
1403
+ revision: record.revision,
1404
+ path,
1405
+ content: decode$1(file.base64)
1406
+ };
1407
+ },
1408
+ async saveFile({ id, expectedRevision, path, content }) {
1409
+ safePath(path);
1410
+ if (typeof content !== "string" || Buffer.byteLength(content) > MAX_TEXT || content.includes("\0")) throw new Error("Source must be UTF-8 text of at most 1 MiB.");
1411
+ const record = await repository.transaction(async (tx) => {
1412
+ const current = modern(await tx.read(id));
1413
+ checkRevision(current, expectedRevision);
1414
+ const files = await filesOf(current), found = files.find((entry) => entry.path === path);
1415
+ if (!found) throw new Error("Select an existing source file to edit.");
1416
+ decode$1(found.base64);
1417
+ if (current.kind === "kit" && path === "kit.yaml") throw new Error("Use Bundle mount management to edit the Kit declaration.");
1418
+ return tx.replace(id, expectedRevision, {
1419
+ ...current,
1420
+ ...importedRuntime(current, files.map((file) => file.path === path ? {
1421
+ path,
1422
+ base64: Buffer.from(content).toString("base64")
1423
+ } : file)),
1424
+ overlays: {
1425
+ ...current.overlays,
1426
+ [path]: content
1427
+ }
1428
+ });
1429
+ });
1430
+ return describe(record);
1431
+ },
1432
+ async preview({ id, expectedRevision, bundle: requested, sampleSource }) {
1433
+ return repository.transaction(async (tx) => {
1434
+ const record = modern(await tx.read(id));
1435
+ checkRevision(record, expectedRevision);
1436
+ const files = record.kind === "bundle" ? await filesOf(record) : void 0;
1437
+ if (files && definitionOnly(record, {}, files)) return { diagnostics: displayDiagnostics(record, await definitionDiagnostics(record, {}, files)) };
1438
+ if (typeof composition.preview !== "function") throw new Error("The host has not supplied the Core preview adapter.");
1439
+ if (record.kind === "bundle") {
1440
+ const diagnostics = identityDiagnostics(record, files);
1441
+ if (diagnostics.length) return { diagnostics };
1442
+ return composition.preview({
1443
+ kind: "bundle",
1444
+ bundle: record.bundle,
1445
+ runtimeName: record.runtimeName,
1446
+ bundleFiles: files,
1447
+ sourceId: record.sourceId,
1448
+ revision: record.revision,
1449
+ sampleSource
1450
+ });
1451
+ }
1452
+ const bundle = pin(requested);
1453
+ if (!record.mounts.some((mount) => same(mount.bundle, bundle))) throw new Error("Select an exact mounted Bundle.");
1454
+ const result = await composition.preview({
1455
+ kind: "kit",
1456
+ ...await kitInput(record),
1457
+ bundle,
1458
+ revision: record.revision,
1459
+ sampleSource
1460
+ });
1461
+ return {
1462
+ ...result,
1463
+ diagnostics: displayDiagnostics(record, result.diagnostics)
1464
+ };
1465
+ });
1466
+ },
1467
+ async migrate({ id, expectedRevision }) {
1468
+ const migrated = await repository.transaction(async (tx) => {
1469
+ const current = active(await tx.read(id));
1470
+ checkRevision(current, expectedRevision);
1471
+ if (current.recordVersion === 2) throw new Error("Instance is already migrated.");
1472
+ const allFiles = await filesOf(current), identity = localPin(current.kind, current.source[current.kind].version, id), sourceId = localSourceId(current.kind, identity);
1473
+ let files, mounts = [], runtimeName;
1474
+ if (current.kind === "bundle") {
1475
+ const prefix = `${current.bundlePath}/`;
1476
+ files = allFiles.filter((entry) => entry.path.startsWith(prefix)).map((entry) => ({
1477
+ ...entry,
1478
+ path: entry.path.slice(prefix.length)
1479
+ }));
1480
+ runtimeName = runtimeNameOf(current.source.bundle.name);
1481
+ files = await composition.forkBundle({
1482
+ files,
1483
+ from: current.source.bundle,
1484
+ to: identity,
1485
+ runtimeName
1486
+ });
1487
+ } else {
1488
+ for (const path of Object.keys(current.overlays)) if (path.startsWith("skills/")) {
1489
+ const name = path.split("/")[1];
1490
+ if (!current.mounts.some((mount) => mount.bundle.name === name && mount.instanceId)) throw new Error("Legacy Kit contains independent Bundle edits. Preserve them as Bundle copies before migrating this Kit.");
1491
+ }
1492
+ files = allFiles.filter((entry) => !entry.path.startsWith("skills/"));
1493
+ for (const old of current.mounts) {
1494
+ const prefix = `skills/${old.bundle.name}/`;
1495
+ let bundleFiles = allFiles.filter((entry) => entry.path.startsWith(prefix)).map((entry) => ({
1496
+ ...entry,
1497
+ path: entry.path.slice(prefix.length)
1498
+ }));
1499
+ let bundle = pin(old.bundle), authority;
1500
+ if (old.instanceId) {
1501
+ const linked = active(await tx.read(old.instanceId));
1502
+ bundle = linked.recordVersion === 2 ? linked.bundle : localPin("bundle", old.bundle.version, old.instanceId);
1503
+ authority = localSourceId("bundle", bundle);
1504
+ bundleFiles = await composition.forkBundle({
1505
+ files: bundleFiles,
1506
+ from: old.bundle,
1507
+ to: bundle,
1508
+ runtimeName: old.bundle.name
1509
+ });
1510
+ } else {
1511
+ const trusted = await catalog.captureBundle(bundle);
1512
+ if (canonicalFiles(trusted.files) !== canonicalFiles(bundleFiles)) throw new Error("Legacy Bundle bytes differ from the trusted catalog. Migrate a separate local Bundle before adopting this source.");
1513
+ authority = requireSourceId(trusted.sourceId);
1514
+ }
1515
+ mounts.push({
1516
+ bindingKey: old.bundle.name,
1517
+ bundle: {
1518
+ ...bundle,
1519
+ source: await retain(tx, bundleFiles, authority),
1520
+ ...old.instanceId ? {
1521
+ instanceId: old.instanceId,
1522
+ revision: old.revision
1523
+ } : {}
1524
+ }
1525
+ });
1526
+ }
1527
+ }
1528
+ const next = {
1529
+ ...current,
1530
+ recordVersion: 2,
1531
+ [current.kind]: identity,
1532
+ sourceId,
1533
+ baseSnapshot: (await retain(tx, files, sourceId)).snapshot,
1534
+ overlays: {},
1535
+ mounts,
1536
+ ...current.kind === "bundle" ? { runtimeName } : {},
1537
+ migration: {
1538
+ fromRevision: current.revision,
1539
+ fromSnapshot: current.baseSnapshot
1540
+ }
1541
+ };
1542
+ delete next.bundlePath;
1543
+ if (current.kind === "kit") next.baseSnapshot = (await retain(tx, await projectedKit(next, mounts), sourceId)).snapshot;
1544
+ return tx.migrate(id, expectedRevision, next);
1545
+ });
1546
+ return describe(migrated);
1547
+ }
1548
+ };
1549
+ }
1550
+ //#endregion
1551
+ //#region src/node/studio-instance-composition.mjs
1552
+ /** Core-backed Studio adapters over trusted captures; no filesystem, installation or execution port. */
1553
+ const MAX_TREE_BYTES = 16777216;
1554
+ const MAX_TREE_FILES = 4096;
1555
+ const MAX_CLOSURE_BYTES = 67108864;
1556
+ const MAX_CLOSURE_FILES = 16384;
1557
+ const pinOf = (value) => {
1558
+ if (!value || typeof value.name !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(value.name) || typeof value.version !== "string" || !/^\d+\.\d+$/.test(value.version)) throw new Error("Studio composition requires exact content pins.");
1559
+ return {
1560
+ name: value.name,
1561
+ version: value.version
1562
+ };
1563
+ };
1564
+ const samePin$1 = (a, b) => a.name === b.name;
1565
+ const sourceIdOf = (value) => {
1566
+ if (typeof value !== "string" || !value || value.length > 4096 || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error("Trusted Bundle source identity is required.");
1567
+ return value;
1568
+ };
1569
+ const decode = (bytes) => {
1570
+ const value = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1571
+ if (value.includes("\0")) throw new Error("Studio definition text contains binary content.");
1572
+ return value;
1573
+ };
1574
+ const entry = (path, content) => ({
1575
+ path,
1576
+ base64: Buffer.from(content, "utf8").toString("base64")
1577
+ });
1578
+ /** The virtual filesystem never falls back to the host filesystem. */
1579
+ function memorySource(entries, budget = {
1580
+ bytes: 0,
1581
+ files: 0
1582
+ }) {
1583
+ if (!Array.isArray(entries) || entries.length > MAX_TREE_FILES) throw new Error("Studio source exceeds 4096 files.");
1584
+ const root = resolve("/studio-instance-memory", randomUUID()), files = /* @__PURE__ */ new Map(), directories = /* @__PURE__ */ new Set([""]);
1585
+ let total = 0;
1586
+ for (const item of entries) {
1587
+ const { path, base64 } = item ?? {};
1588
+ if (typeof path !== "string" || !path || path.length > 1024 || /[\\:\u0000-\u001f\u007f]/u.test(path) || path.split("/").some((part) => !part || [
1589
+ ".",
1590
+ "..",
1591
+ ".git",
1592
+ "node_modules"
1593
+ ].includes(part)) || path.split("/").length > 32 || files.has(path) || typeof base64 !== "string") throw new Error("Invalid or duplicate Studio source path.");
1594
+ const bytes = Buffer.from(base64, "base64");
1595
+ if (bytes.toString("base64") !== base64) throw new Error("Studio source base64 is not canonical.");
1596
+ total += bytes.length;
1597
+ if (total > MAX_TREE_BYTES) throw new Error("Studio source exceeds 16 MiB.");
1598
+ files.set(path, bytes);
1599
+ const parts = path.split("/");
1600
+ for (let count = 1; count < parts.length; count++) directories.add(parts.slice(0, count).join("/"));
1601
+ }
1602
+ if ([...files.keys()].some((path) => directories.has(path))) throw new Error("Studio source file conflicts with a directory.");
1603
+ budget.bytes += total;
1604
+ budget.files += files.size;
1605
+ if (budget.bytes > MAX_CLOSURE_BYTES || budget.files > MAX_CLOSURE_FILES) throw new Error("Studio composition exceeds its 64 MiB or 16384 file budget.");
1606
+ const keyOf = (path) => {
1607
+ const rel = relative(root, resolve(path));
1608
+ if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) throw new Error("Studio composition path escapes captured source.");
1609
+ return rel.split(sep).join("/");
1610
+ };
1611
+ return {
1612
+ root,
1613
+ fs: {
1614
+ async readText(path) {
1615
+ const bytes = files.get(keyOf(path));
1616
+ return bytes === void 0 ? void 0 : bytes.toString("utf8");
1617
+ },
1618
+ async readBytes(path) {
1619
+ const bytes = files.get(keyOf(path));
1620
+ return bytes === void 0 ? void 0 : Buffer.from(bytes);
1621
+ },
1622
+ async listDir(path) {
1623
+ const key = keyOf(path), prefix = key ? `${key}/` : "";
1624
+ return [...new Set([...files.keys()].filter((name) => name.startsWith(prefix)).map((name) => name.slice(prefix.length).split("/")[0]))].sort();
1625
+ },
1626
+ async isDirectory(path) {
1627
+ return directories.has(keyOf(path));
1628
+ },
1629
+ async realPath(path) {
1630
+ keyOf(path);
1631
+ return resolve(path);
1632
+ }
1633
+ },
1634
+ entries: [...files].map(([path, bytes]) => ({
1635
+ path,
1636
+ base64: bytes.toString("base64")
1637
+ })),
1638
+ text(path) {
1639
+ const bytes = files.get(path);
1640
+ if (bytes === void 0) throw new Error(`Missing Studio definition ${path}.`);
1641
+ return decode(bytes);
1642
+ }
1643
+ };
1644
+ }
1645
+ /** Edit only one top-level YAML scalar; comments and every other source byte survive. */
1646
+ function scalarEdit(source, key, value, allowInsert = false) {
1647
+ const document = parseDocument(source);
1648
+ if (document.errors.length || !isMap(document.contents)) throw new Error("Studio requires one valid YAML mapping.");
1649
+ const node = document.get(key, true), encoded = JSON.stringify(value);
1650
+ if (node !== void 0) {
1651
+ if (!isScalar(node) || !node.range) throw new Error(`Studio cannot rewrite non-scalar ${key}.`);
1652
+ return source.slice(0, node.range[0]) + encoded + source.slice(node.range[1]);
1653
+ }
1654
+ if (!allowInsert) throw new Error(`Missing required identity field ${key}.`);
1655
+ if (document.contents.flow) {
1656
+ const index = document.contents.range[0] + 1;
1657
+ return source.slice(0, index) + `${key}: ${encoded}${document.contents.items.length ? ", " : ""}` + source.slice(index);
1658
+ }
1659
+ const first = document.contents.items[0]?.key;
1660
+ if (!first?.range) throw new Error("Studio cannot insert identity into an empty mapping.");
1661
+ const start = source.lastIndexOf("\n", first.range[0] - 1) + 1;
1662
+ const indent = source.slice(start, first.range[0]), newline = source.includes("\r\n") ? "\r\n" : "\n";
1663
+ return source.slice(0, start) + `${indent}${key}: ${encoded}${newline}` + source.slice(start);
1664
+ }
1665
+ function projectRelationIdentifiers(source, from, to) {
1666
+ if (from === to || !source.trim()) return source;
1667
+ const document = parseDocument(source);
1668
+ if (document.errors.length || !isMap(document.contents)) throw new Error("Studio cannot project malformed relations.yaml.");
1669
+ const relations = document.get("relations", true), edits = [];
1670
+ const rebase = (node, separator) => {
1671
+ if (!isScalar(node) || typeof node.value !== "string" || !node.range || !node.value.startsWith(`${from}${separator}`)) return;
1672
+ edits.push({
1673
+ start: node.range[0],
1674
+ end: node.range[1],
1675
+ value: JSON.stringify(to + node.value.slice(from.length))
1676
+ });
1677
+ };
1678
+ if (isSeq(relations)) for (const relation of relations.items) {
1679
+ if (!isMap(relation)) continue;
1680
+ for (const key of [
1681
+ "type",
1682
+ "meaning",
1683
+ "inverse"
1684
+ ]) rebase(relation.get(key, true), ":");
1685
+ for (const key of ["domain", "range"]) {
1686
+ const values = relation.get(key, true);
1687
+ if (isSeq(values)) for (const value of values.items) rebase(value, "/");
1688
+ }
1689
+ }
1690
+ for (const edit of edits.sort((a, b) => b.start - a.start)) source = source.slice(0, edit.start) + edit.value + source.slice(edit.end);
1691
+ return source;
1692
+ }
1693
+ async function projectMindset(source, kit, mounts, describeBundle, allowEmptyDraft = false) {
1694
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
1695
+ if (!match) throw new Error("Kit Mindset requires explicit YAML frontmatter.");
1696
+ const parsed = parseFrontmatter(source, "mindset.md");
1697
+ const catalogs = [...parsed.body.matchAll(/<design_skills>\s*([\s\S]*?)\s*<\/design_skills>/g)];
1698
+ const emptyDraft = allowEmptyDraft && catalogs.length === 1 && catalogs[0][1].trim() === "";
1699
+ if (!emptyDraft) parseMindsetSkillNames(parsed.body, String(parsed.meta.name));
1700
+ let frontmatter = scalarEdit(match[1], "name", kit.name);
1701
+ frontmatter = scalarEdit(frontmatter, "version", kit.version);
1702
+ const frontStart = source.indexOf(match[1]), frontEnd = frontStart + match[1].length;
1703
+ const renamed = source.slice(0, frontStart) + frontmatter + source.slice(frontEnd);
1704
+ const bodyStart = renamed.length - parseFrontmatter(renamed, "mindset.md").body.length;
1705
+ const marker = renamed.indexOf("<design_skills>", bodyStart);
1706
+ if (marker < 0) throw new Error("Mindset has no generated Bundle catalog block.");
1707
+ const start = marker + 15;
1708
+ const end = renamed.indexOf("</design_skills>", start);
1709
+ const block = renamed.slice(start, end), headings = [...block.matchAll(/^##\s+(.+?)\s*$/gm)];
1710
+ if (end < start || headings.length === 0 && !emptyDraft) throw new Error("Mindset generated Bundle catalog is empty or malformed.");
1711
+ const sections = new Map(headings.map((heading, index) => [heading[1], block.slice(heading.index, headings[index + 1]?.index ?? block.length)]));
1712
+ const newline = renamed.includes("\r\n") ? "\r\n" : "\n";
1713
+ const selected = [];
1714
+ for (const mount of mounts) if (sections.has(mount.bindingKey)) selected.push(sections.get(mount.bindingKey));
1715
+ else {
1716
+ const description = (await describeBundle(mount)).trim().replace(/\r\n?/g, "\n");
1717
+ if (!description) throw new Error(`Bundle ${mount.name} needs a SKILL frontmatter routing description before it can enter the Mindset catalog.`);
1718
+ const escaped = description.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\n", newline);
1719
+ selected.push(`## ${mount.bindingKey}${newline}${newline}${escaped}${newline}${newline}`);
1720
+ }
1721
+ const generated = block.slice(0, headings[0]?.index ?? block.length) + selected.join("");
1722
+ const projected = renamed.slice(0, start) + generated + renamed.slice(end);
1723
+ const names = allowEmptyDraft && mounts.length === 0 ? [] : parseMindsetSkillNames(parseFrontmatter(projected, "mindset.md").body, kit.name);
1724
+ if (JSON.stringify(names) !== JSON.stringify(mounts.map((mount) => mount.bindingKey))) throw new Error("Generated Mindset catalog differs from Kit mounts.");
1725
+ return projected;
1726
+ }
1727
+ /**
1728
+ * trusted.listBundles(): Promise<Pin[]>; trusted.captureBundle(pin): {files,sourceId};
1729
+ * trusted.captureKit(pin): {files,title?}. Captures are exact, host-selected sources.
1730
+ * composition.projectMounts additionally accepts resolveBundle(pin) for retained local sources;
1731
+ * the trusted installed capture is used only when that resolver is omitted.
1732
+ * Optional trusted.preview overrides the default Core-backed retained-source preview.
1733
+ * Returned catalog/composition match createStudioInstances; there is no legacy-tree fallback.
1734
+ */
1735
+ function createStudioInstanceAdapters(trusted) {
1736
+ if (!trusted || ![
1737
+ "listBundles",
1738
+ "captureBundle",
1739
+ "captureKit"
1740
+ ].every((key) => typeof trusted[key] === "function")) throw new Error("Trusted independent catalog capture ports are required.");
1741
+ const loadBundle = async (files, sourceId, expected, budget) => {
1742
+ const source = memorySource(files, budget);
1743
+ for (const path of [
1744
+ "skill.yaml",
1745
+ "SKILL.md",
1746
+ "schema.yaml",
1747
+ "validation.yaml"
1748
+ ]) source.text(path);
1749
+ return {
1750
+ source,
1751
+ content: await loadDesignBundle(source.fs, {
1752
+ rootDir: source.root,
1753
+ sourceId: sourceIdOf(sourceId),
1754
+ current: true
1755
+ }, { name: pinOf(expected).name })
1756
+ };
1757
+ };
1758
+ const compileKit = async ({ kit, kitFiles, mounts, resolveBundle }) => {
1759
+ const expected = pinOf(kit), budget = {
1760
+ bytes: 0,
1761
+ files: 0
1762
+ }, source = memorySource(kitFiles, budget);
1763
+ const declared = parseKitMounts(source.text("kit.yaml"));
1764
+ const selected = mounts.map((mount) => ({
1765
+ bindingKey: mount.bindingKey,
1766
+ ...pinOf(mount.bundle ?? mount)
1767
+ }));
1768
+ const membership = (mounts) => mounts.map(({ bindingKey, name }) => ({
1769
+ bindingKey,
1770
+ name
1771
+ }));
1772
+ if (JSON.stringify(membership(declared)) !== JSON.stringify(membership(selected))) throw new Error("Selected mounts differ from authoritative kit.yaml.");
1773
+ const loaded = await loadDesignKit(source.fs, source.root, {
1774
+ sourceId: `local:kit:${expected.name}`,
1775
+ current: true,
1776
+ resolveBundle: async (pin) => {
1777
+ if (!declared.some((mount) => samePin$1(mount, pin))) throw new Error("Core requested an undeclared Bundle.");
1778
+ const capture = await resolveBundle(pin);
1779
+ if (!capture) throw new Error(`Missing exact Bundle ${pin.name}@${pin.version}.`);
1780
+ return (await loadBundle(capture.files, capture.sourceId, pin, budget)).content;
1781
+ }
1782
+ });
1783
+ if (!samePin$1({
1784
+ name: loaded.name,
1785
+ version: String(loaded.version)
1786
+ }, expected)) throw new Error("Kit source identity differs from its exact pin.");
1787
+ return loaded;
1788
+ };
1789
+ const previewRetained = async (input) => {
1790
+ const failure = (code, message) => ({ diagnostics: [{
1791
+ code,
1792
+ message
1793
+ }] });
1794
+ if (!input || !["kit", "bundle"].includes(input.kind) || !Number.isSafeInteger(input.revision) || input.revision < 0) return failure("preview.request", "Select a retained Kit or Bundle and a non-negative revision.");
1795
+ const { kind, revision, sampleSource, runtimeName, sourceId } = input;
1796
+ let bundle, kit, kitFiles, bundleFiles, mounts;
1797
+ const resolveBundle = input.resolveBundle;
1798
+ try {
1799
+ bundle = pinOf(input.bundle);
1800
+ if (kind === "kit") {
1801
+ kit = pinOf(input.kit);
1802
+ kitFiles = structuredClone(input.kitFiles);
1803
+ mounts = structuredClone(input.mounts);
1804
+ if (!Array.isArray(mounts) || !mounts.some((mount) => samePin$1(mount.bundle ?? mount, bundle))) return failure("preview.bundle_pin", "Select an exact mounted Bundle.");
1805
+ if (typeof resolveBundle !== "function") return failure("preview.request", "Kit preview requires its retained Bundle resolver.");
1806
+ } else {
1807
+ if (input.kit !== void 0) return failure("preview.request", "A standalone Bundle preview cannot claim a Kit context.");
1808
+ bundleFiles = structuredClone(input.bundleFiles);
1809
+ }
1810
+ let compiled, retained;
1811
+ const oversizedPanel = (files) => Array.isArray(files) && files.some((file) => file.path === "panel.yaml" && typeof file.base64 === "string" && Buffer.byteLength(file.base64, "base64") > 1048576);
1812
+ if (kind === "bundle") {
1813
+ if (oversizedPanel(bundleFiles)) return failure("preview.size", "Panel source is limited to 1 MiB.");
1814
+ retained = await loadBundle(bundleFiles, sourceId, bundle);
1815
+ compiled = retained.content.skill;
1816
+ if (runtimeName !== void 0 && compiled.name !== runtimeName) return failure("preview.bundle_pin", "The retained runtime name does not match the selected Bundle.");
1817
+ } else {
1818
+ const requested = parseKitMounts(memorySource(kitFiles).text("kit.yaml")).find((mount) => mount.name === bundle.name);
1819
+ if (!requested) return failure("preview.bundle_pin", "The Bundle is outside the Kit declaration.");
1820
+ const captured = await resolveBundle(requested);
1821
+ if (!captured) return failure("preview.bundle_pin", "The retained Bundle source is missing.");
1822
+ const selected = {
1823
+ files: structuredClone(captured.files),
1824
+ sourceId: captured.sourceId
1825
+ };
1826
+ if (oversizedPanel(selected.files)) return failure("preview.size", "Panel source is limited to 1 MiB.");
1827
+ compiled = (await compileKit({
1828
+ kit,
1829
+ kitFiles,
1830
+ mounts,
1831
+ resolveBundle: (pin) => samePin$1(pin, bundle) ? Promise.resolve(selected) : resolveBundle(pin)
1832
+ })).skills.find((skill) => (skill.catalogName ?? skill.name) === bundle.name);
1833
+ retained = { source: memorySource(selected.files) };
1834
+ }
1835
+ if (!compiled) return failure("preview.bundle_pin", "The source does not match the selected Bundle version.");
1836
+ if (typeof compiled.metadata?.panelYaml !== "string") return failure("preview.not_declarative", "Only an existing declarative Panel can be previewed.");
1837
+ let sample = sampleSource;
1838
+ if (sample === void 0) {
1839
+ const example = retained.source.entries.find((file) => file.path === "reference/preview.json");
1840
+ if (example) sample = retained.source.text(example.path);
1841
+ else if (compiled.metadata.panelManifest?.initial_values !== void 0) sample = JSON.stringify(compiled.metadata.panelManifest.initial_values);
1842
+ else return failure("preview.sample_missing", "Add reference/preview.json, Panel initial_values, or explicit isolated sample JSON.");
1843
+ }
1844
+ return finalizeStudioPanelPreview({
1845
+ compiled,
1846
+ bundle: {
1847
+ ...bundle,
1848
+ version: String(compiled.version)
1849
+ },
1850
+ ...kit === void 0 ? {} : { kit },
1851
+ revision,
1852
+ sampleSource: sample,
1853
+ readSampleResource: (path) => retained.source.entries.some((file) => file.path === path) ? retained.source.text(path) : void 0
1854
+ });
1855
+ } catch (error) {
1856
+ return failure("preview.definition", error instanceof Error ? error.message : String(error));
1857
+ }
1858
+ };
1859
+ const catalog = {
1860
+ async listBundles() {
1861
+ const pins = await trusted.listBundles();
1862
+ if (!Array.isArray(pins) || pins.length > 1024) throw new Error("Studio catalog exceeds 1024 Bundle pins.");
1863
+ const seen = /* @__PURE__ */ new Set(), result = [];
1864
+ for (const value of pins) {
1865
+ const pin = pinOf(value), key = `${pin.name}@${pin.version}`;
1866
+ if (seen.has(key)) throw new Error(`Ambiguous exact Bundle ${key}.`);
1867
+ seen.add(key);
1868
+ const captured = await catalog.captureBundle(pin);
1869
+ result.push({
1870
+ ...pin,
1871
+ title: captured.title,
1872
+ runtimeName: captured.runtimeName,
1873
+ ...captured.description === void 0 ? {} : { description: captured.description },
1874
+ ...captured.displayDescription === void 0 ? {} : { displayDescription: { ...captured.displayDescription } }
1875
+ });
1876
+ }
1877
+ return result;
1878
+ },
1879
+ async captureBundle(value) {
1880
+ const pin = pinOf(value), raw = await trusted.captureBundle(pin);
1881
+ const { source, content } = await loadBundle(raw.files, raw.sourceId, pin);
1882
+ return {
1883
+ files: source.entries,
1884
+ sourceId: raw.sourceId,
1885
+ title: { ...content.skill.title },
1886
+ runtimeName: content.skill.name,
1887
+ ...typeof content.skill.description === "string" ? { description: content.skill.description } : {},
1888
+ ...content.skill.pickerDescription ? { displayDescription: { ...content.skill.pickerDescription } } : {}
1889
+ };
1890
+ },
1891
+ async captureKit(value) {
1892
+ const kit = pinOf(value), raw = await trusted.captureKit(kit), source = memorySource(raw.files);
1893
+ if (source.entries.some((file) => file.path.startsWith("skills/"))) throw new Error("Kit capture must not contain embedded Bundle trees.");
1894
+ const mounts = parseKitMounts(source.text("kit.yaml"));
1895
+ await compileKit({
1896
+ kit,
1897
+ kitFiles: source.entries,
1898
+ mounts,
1899
+ resolveBundle: (pin) => trusted.captureBundle(pin)
1900
+ });
1901
+ return {
1902
+ files: source.entries,
1903
+ mounts,
1904
+ ...raw.title === void 0 ? {} : { title: raw.title }
1905
+ };
1906
+ }
1907
+ };
1908
+ const composition = {
1909
+ loadKit: compileKit,
1910
+ async forkBundle({ files, from, to, runtimeName }) {
1911
+ from = pinOf(from);
1912
+ to = pinOf(to);
1913
+ if (to.name === from.name || to.version !== from.version) throw new Error("A fork requires a new catalog identity and preserves its source version.");
1914
+ const before = await loadBundle(files, `studio:fork-source:${from.name}`, from);
1915
+ if (before.content.skill.name !== runtimeName) throw new Error("Fork runtime name does not match its retained source.");
1916
+ const presentation = before.content.skill.metadata?.skillManifest?.presentation;
1917
+ if (presentation?.mode === "native" || presentation?.mode === "auto" && presentation.renderer && !before.content.skill.metadata?.panelYaml) throw new Error("Native-only Bundles cannot be forked: the new identity has no trusted native renderer authority.");
1918
+ const manifest = scalarEdit(before.source.text("skill.yaml"), "identity", to.name, true);
1919
+ const next = before.source.entries.map((file) => file.path === "skill.yaml" ? entry(file.path, manifest) : file);
1920
+ const after = await loadBundle(next, `local:bundle:${to.name}`, to);
1921
+ if (after.content.skill.name !== runtimeName || after.content.skill.content !== before.content.skill.content) throw new Error("A fork cannot alter the runtime name or Bundle method.");
1922
+ return next;
1923
+ },
1924
+ async projectMounts({ kit, kitFiles, mounts, allowEmptyDraft = false, resolveBundle = (pin) => trusted.captureBundle(pin) }) {
1925
+ kit = pinOf(kit);
1926
+ const declaration = stringify({
1927
+ version: 1,
1928
+ mounts: mounts.map((mount) => ({
1929
+ bindingKey: mount.bindingKey,
1930
+ ...pinOf(mount)
1931
+ }))
1932
+ });
1933
+ const normalized = allowEmptyDraft && mounts.length === 0 ? [] : parseKitMounts(declaration), source = memorySource(kitFiles);
1934
+ const budget = {
1935
+ bytes: 0,
1936
+ files: 0
1937
+ };
1938
+ const originalMindset = source.text("mindset.md");
1939
+ let mindset;
1940
+ try {
1941
+ mindset = await projectMindset(originalMindset, kit, normalized, async (mount) => {
1942
+ const captured = await resolveBundle(pinOf(mount));
1943
+ if (!captured) throw new Error(`Missing exact Bundle ${mount.name}@${mount.version} for catalog projection.`);
1944
+ const loaded = await loadBundle(captured.files, captured.sourceId, mount, budget);
1945
+ if (loaded.content.skill.name !== mount.bindingKey) throw new Error("Bundle bindingKey must preserve the runtime name.");
1946
+ return loaded.content.skill.description;
1947
+ }, allowEmptyDraft);
1948
+ } catch (error) {
1949
+ if (!allowEmptyDraft) throw error;
1950
+ return [...composition.projectSourceIdentity({
1951
+ kind: "kit",
1952
+ files: source.entries,
1953
+ identity: kit
1954
+ }).filter((file) => file.path !== "kit.yaml"), entry("kit.yaml", declaration)];
1955
+ }
1956
+ const originalName = String(parseFrontmatter(originalMindset, "mindset.md").meta.name);
1957
+ return [
1958
+ ...source.entries.filter((file) => !["kit.yaml", "mindset.md"].includes(file.path)).map((file) => {
1959
+ if (file.path !== "relations.yaml" || originalName === kit.name) return file;
1960
+ source.text(file.path);
1961
+ const text = Buffer.from(file.base64, "base64").toString("utf8");
1962
+ const projected = projectRelationIdentifiers(text, originalName, kit.name);
1963
+ return projected === text ? file : entry(file.path, projected);
1964
+ }),
1965
+ entry("mindset.md", mindset),
1966
+ entry("kit.yaml", declaration)
1967
+ ];
1968
+ },
1969
+ projectSourceIdentity({ kind, files, identity }) {
1970
+ const source = memorySource(files);
1971
+ if (kind === "bundle") return source.entries.map((file) => {
1972
+ if (file.path !== "skill.yaml") return file;
1973
+ try {
1974
+ return entry(file.path, scalarEdit(source.text(file.path), "identity", identity.name, true));
1975
+ } catch {
1976
+ return file;
1977
+ }
1978
+ });
1979
+ let originalName;
1980
+ return source.entries.map((file) => {
1981
+ if (file.path !== "mindset.md") return file;
1982
+ try {
1983
+ const text = source.text(file.path), match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
1984
+ if (!match) return file;
1985
+ originalName = parseFrontmatter(text, "mindset.md").meta.name;
1986
+ let frontmatter = scalarEdit(match[1], "name", identity.name, true);
1987
+ frontmatter = scalarEdit(frontmatter, "version", identity.version, true);
1988
+ const start = text.indexOf(match[1]);
1989
+ return entry(file.path, text.slice(0, start) + frontmatter + text.slice(start + match[1].length));
1990
+ } catch {
1991
+ return file;
1992
+ }
1993
+ }).map((file) => {
1994
+ if (file.path !== "relations.yaml" || typeof originalName !== "string" || originalName === identity.name) return file;
1995
+ try {
1996
+ return entry(file.path, projectRelationIdentifiers(source.text(file.path), originalName, identity.name));
1997
+ } catch {
1998
+ return file;
1999
+ }
2000
+ });
2001
+ },
2002
+ async validateBundle({ bundle, bundleFiles, sourceId, runtimeName }) {
2003
+ const { content } = await loadBundle(bundleFiles, sourceId, bundle);
2004
+ if (content.skill.name !== runtimeName) throw new Error("Bundle runtime name differs from the retained method.");
2005
+ },
2006
+ async validateKit(input) {
2007
+ await compileKit(input);
2008
+ },
2009
+ preview: typeof trusted.preview === "function" ? (input) => trusted.preview(input) : previewRetained
2010
+ };
2011
+ return {
2012
+ catalog,
2013
+ composition
2014
+ };
2015
+ }
2016
+ //#endregion
2017
+ //#region src/node/studio-authoring.mjs
2018
+ /**
2019
+ * Read/prepare capability for one trusted Studio instance revision.
2020
+ *
2021
+ * The host authenticates and binds the native invocation before constructing this
2022
+ * adapter. Scope is never a model argument. Pass only the existing instance
2023
+ * read/readFile/prepareChanges ports and a same-build component catalog reader
2024
+ * returning the JSON projection used by design_list_panel_components.
2025
+ *
2026
+ * read({paths?}) returns coherent, bounded source text plus the actual catalog.
2027
+ * allowedPaths seeds the initial read; the current instance and revision bind
2028
+ * subsequent legal reads, replacements and new files.
2029
+ * prepare({changes}) returns {request,candidate}: request.changes contains full
2030
+ * replacement files for the existing UI's saveChanges path, while candidate is
2031
+ * S1's exact diff/preview/digest. Neither operation saves, publishes, dispatches
2032
+ * a model, or proves native execution. This first capability excludes deletion.
2033
+ */
2034
+ const object = (value, keys, label) => {
2035
+ if (!value || typeof value !== "object" || Array.isArray(value) || Reflect.ownKeys(value).some((key) => !keys.includes(key))) throw new Error(`Invalid ${label}; scope and authority cannot be supplied by the caller.`);
2036
+ return value;
2037
+ };
2038
+ const samePin = (a, b) => a?.name === b?.name && a?.version === b?.version;
2039
+ const changed = () => /* @__PURE__ */ new Error("Authoring instance changed. Reopen and explicitly bind the current revision; do not retry this scope automatically.");
2040
+ function createStudioAuthoring({ scope, instances, readComponents } = {}) {
2041
+ object(scope, [
2042
+ "id",
2043
+ "expectedRevision",
2044
+ "allowedPaths",
2045
+ "bundle"
2046
+ ], "trusted authoring scope");
2047
+ if (!instances || ![
2048
+ "read",
2049
+ "readFile",
2050
+ "prepareChanges"
2051
+ ].every((key) => typeof instances[key] === "function") || typeof readComponents !== "function" || !Array.isArray(scope.allowedPaths)) throw new Error("Authoring requires existing instance read/prepare ports, an initial reading focus and the actual component catalog reader.");
2052
+ const normalized = normalizeStudioInstanceChanges({
2053
+ id: scope.id,
2054
+ expectedRevision: scope.expectedRevision,
2055
+ changes: scope.allowedPaths.map((path) => ({
2056
+ path,
2057
+ content: ""
2058
+ })),
2059
+ ...scope.bundle === void 0 ? {} : { bundle: scope.bundle }
2060
+ });
2061
+ const binding = Object.freeze({
2062
+ id: normalized.id,
2063
+ expectedRevision: normalized.expectedRevision,
2064
+ allowedPaths: Object.freeze(normalized.changes.map((change) => change.path)),
2065
+ ...normalized.bundle ? { bundle: normalized.bundle } : {}
2066
+ });
2067
+ const readInstance = instances.read.bind(instances), readFile = instances.readFile.bind(instances);
2068
+ const prepareChanges = instances.prepareChanges.bind(instances);
2069
+ const requestFor = (changes) => normalizeStudioInstanceChanges({
2070
+ id: binding.id,
2071
+ expectedRevision: binding.expectedRevision,
2072
+ changes,
2073
+ ...binding.bundle ? { bundle: binding.bundle } : {}
2074
+ });
2075
+ const checkPaths = (paths) => {
2076
+ return requestFor(paths.map((path) => ({
2077
+ path,
2078
+ content: ""
2079
+ }))).changes.map((change) => change.path);
2080
+ };
2081
+ const current = async () => {
2082
+ const record = await readInstance({ id: binding.id });
2083
+ if (!record || record.id !== binding.id || record.revision !== binding.expectedRevision || record.deletedAt) throw changed();
2084
+ if (!["kit", "bundle"].includes(record.kind) || !Array.isArray(record.files)) throw new Error("Authoring requires a current complete-source working copy.");
2085
+ if (binding.bundle && (record.kind === "bundle" ? !samePin(binding.bundle, record.bundle) : !record.mounts?.some((mount) => samePin(binding.bundle, mount.bundle)))) throw new Error("Authoring preview Bundle is outside this exact instance.");
2086
+ return record;
2087
+ };
2088
+ const components = async () => {
2089
+ const value = await readComponents();
2090
+ if (!value || !Array.isArray(value.components) || !value.components.length) throw new Error("The actual authoring component catalog is unavailable.");
2091
+ const text = JSON.stringify(value);
2092
+ if (Buffer.byteLength(text) > STUDIO_INSTANCE_CHANGE_LIMITS.sampleBytes) throw new Error("Authoring component catalog exceeds the 256 KiB response budget.");
2093
+ return JSON.parse(text);
2094
+ };
2095
+ return Object.freeze({
2096
+ async read(input = {}) {
2097
+ object(input, ["paths"], "authoring read request");
2098
+ if (input.paths !== void 0 && !Array.isArray(input.paths)) throw new Error("Select an exact list of scoped source paths.");
2099
+ const paths = checkPaths(input.paths ?? binding.allowedPaths);
2100
+ const record = await current();
2101
+ const selected = paths.map((path) => ({
2102
+ path,
2103
+ file: record.files.find((file) => file.path === path)
2104
+ }));
2105
+ if (selected.some(({ file }) => file && (!file.editable || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || file.bytes > STUDIO_INSTANCE_CHANGE_LIMITS.fileBytes))) throw new Error("Scoped source must be editable UTF-8 text of at most 1 MiB per file.");
2106
+ if (selected.reduce((bytes, { file }) => bytes + (file?.bytes ?? 0), 0) > STUDIO_INSTANCE_CHANGE_LIMITS.totalBytes) throw new Error("Authoring read exceeds the 4 MiB text budget; select fewer paths.");
2107
+ const catalog = await components();
2108
+ const files = await Promise.all(selected.map(async ({ path, file }) => {
2109
+ if (!file) return {
2110
+ path,
2111
+ exists: false
2112
+ };
2113
+ const value = await readFile({
2114
+ id: binding.id,
2115
+ path
2116
+ });
2117
+ if (value?.id !== binding.id || value.revision !== binding.expectedRevision || value.path !== path) throw changed();
2118
+ return {
2119
+ path,
2120
+ exists: true,
2121
+ content: value.content
2122
+ };
2123
+ }));
2124
+ const existing = files.filter((file) => file.exists);
2125
+ if (existing.length) requestFor(existing.map(({ path, content }) => ({
2126
+ path,
2127
+ content
2128
+ })));
2129
+ await current();
2130
+ return {
2131
+ scope: binding,
2132
+ files,
2133
+ components: catalog
2134
+ };
2135
+ },
2136
+ async prepare(input) {
2137
+ object(input, ["changes"], "authoring prepare request");
2138
+ if (!Array.isArray(input.changes)) throw new Error("Provide scoped complete-file replacements.");
2139
+ for (const change of input.changes) {
2140
+ object(change, ["path", "content"], "authoring file replacement");
2141
+ if (change.content === null) throw new Error("File deletion is not granted by this authoring capability.");
2142
+ }
2143
+ const request = requestFor(input.changes);
2144
+ await current();
2145
+ const catalog = await components();
2146
+ const candidate = await prepareChanges(request);
2147
+ await current();
2148
+ if (candidate?.id !== binding.id || candidate.baseRevision !== binding.expectedRevision || candidate.revision !== binding.expectedRevision + 1 || typeof candidate.valid !== "boolean" || !/^[a-f0-9]{64}$/.test(candidate.digest) || !Array.isArray(candidate.diagnostics) || !Array.isArray(candidate.diff) || candidate.diff.some((file) => !request.changes.some((change) => change.path === file.path)) || candidate.preview && candidate.preview.revision !== candidate.revision) throw new Error("Prepared candidate does not match this authoring revision and file set.");
2149
+ return {
2150
+ scope: binding,
2151
+ components: catalog,
2152
+ request,
2153
+ candidate: structuredClone(candidate)
2154
+ };
2155
+ }
2156
+ });
2157
+ }
2158
+ //#endregion
2159
+ export { createStudioAuthoring, createStudioDraftPorts, createStudioInstanceAdapters, createStudioInstances };