@mengine/medeo-tool 1.0.1-alpha.2 → 1.2.1-alpha.10

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.
@@ -0,0 +1,1942 @@
1
+ import { SchemaValidator, SemanticEditor, assertCanonicalEditorResources, createEditSandbox, effectiveVideoClipDurationMs, ensureEditorFoundation, fromVideoDocument, importMediaAsset, solveVideoDocument, speedOf } from "@mengine/medeo-client";
2
+ //#region src/document/compact-projection.ts
3
+ const DEFAULT_TEXT_PREVIEW_LENGTH = 24;
4
+ /** Kind tag shown in the first column (`video_clip` → `clip`). */
5
+ function kindTag(kind) {
6
+ return kind === "video_clip" ? "clip" : kind;
7
+ }
8
+ /** Lane label: `video_clip` tracks display as `main`, otherwise `parts_kind`. */
9
+ function laneLabel(partsKind) {
10
+ return partsKind === "video_clip" ? "main" : partsKind;
11
+ }
12
+ /** Speed token: absent → `1`; linear → numeric multiplier; anything else → `custom`. */
13
+ function speedToken(speedShift) {
14
+ if (speedShift == null) return "1";
15
+ if (speedShift.category === "linear") return String(speedOf(speedShift));
16
+ return "custom";
17
+ }
18
+ function effectiveDurationMs(part, timelineDurationMs) {
19
+ if (part.video_clip != null) return effectiveVideoClipDurationMs(part.video_clip);
20
+ if (part.speech != null) return part.speech.media_duration_ms ?? 0;
21
+ if (part.caption != null) return part.caption.initial_duration_ms ?? 0;
22
+ if (part.bgm != null) return timelineDurationMs;
23
+ return 0;
24
+ }
25
+ function truncateText(text, budget) {
26
+ if (text.length <= budget) return text;
27
+ return `${text.slice(0, budget)}…`;
28
+ }
29
+ function anchorToken(timePosition) {
30
+ if (timePosition.mode === "anchored") return `anchor=${timePosition.anchorPartId}+${timePosition.offsetMs}`;
31
+ if (timePosition.mode === "absolute") return "anchor=abs";
32
+ return "anchor=abs";
33
+ }
34
+ function clipAttrs(clip) {
35
+ const playIn = clip.play_in ?? 0;
36
+ const playOut = clip.play_out ?? 0;
37
+ return `media=${clip.origin_media_id ?? ""} trim=${playIn}-${playOut} speed=${speedToken(clip.speed_shift)} vol=${clip.volume ?? 0}`;
38
+ }
39
+ function partAttrs(part, item, textPreviewLength) {
40
+ if (part.video_clip != null) return clipAttrs(part.video_clip);
41
+ if (part.speech != null) return `${anchorToken(item.time_position)} dur=${part.speech.media_duration_ms ?? 0}`;
42
+ if (part.caption != null) {
43
+ const preview = truncateText(part.caption.text ?? "", textPreviewLength);
44
+ return `${anchorToken(item.time_position)} text="${preview}"`;
45
+ }
46
+ if (part.bgm != null) return `vol=${part.bgm.volume ?? 0}`;
47
+ return "";
48
+ }
49
+ /**
50
+ * Render a `VideoDocument` as compact text: one header line plus one row per
51
+ * timeline part (optionally filtered by `onlyPartIds`). Deterministic and
52
+ * side-effect free — same document always yields the same string.
53
+ */
54
+ function renderCompactProjection(document, options) {
55
+ const onlyPartIds = options?.onlyPartIds;
56
+ const textPreviewLength = options?.textPreviewLength ?? DEFAULT_TEXT_PREVIEW_LENGTH;
57
+ const solved = solveVideoDocument(document);
58
+ const library = document.part_library ?? {};
59
+ const tracks = document.tracks ?? [];
60
+ let totalParts = 0;
61
+ const rows = [];
62
+ for (const track of tracks) {
63
+ const partsKind = track.parts_kind;
64
+ if (partsKind == null) continue;
65
+ const lane = laneLabel(partsKind);
66
+ const tag = kindTag(partsKind);
67
+ for (const item of track.items ?? []) {
68
+ totalParts += 1;
69
+ const partId = item.part_id;
70
+ if (onlyPartIds != null && !onlyPartIds.has(partId)) continue;
71
+ const part = library[partId];
72
+ if (part == null) continue;
73
+ const abs = solved.absByPartId.get(partId) ?? 0;
74
+ const dur = effectiveDurationMs(part, solved.durationMs);
75
+ const attrs = partAttrs(part, item, textPreviewLength);
76
+ rows.push(`${tag} ${partId} ${lane} [${abs},${abs + dur}) ${attrs}`);
77
+ }
78
+ }
79
+ return [`# draft=${document.meta.draft_id ?? ""} v=${document.meta.version ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
80
+ }
81
+ //#endregion
82
+ //#region src/sandbox/preview.ts
83
+ /**
84
+ * Collect part ids referenced by a journal for compact preview filtering.
85
+ * Walks known id-shaped payload keys (宁多勿少) and unions `generated_ids`.
86
+ */
87
+ const PART_ID_KEYS = new Set([
88
+ "clip_id",
89
+ "clip_ids",
90
+ "before_clip_id",
91
+ "after_clip_id",
92
+ "speech_id",
93
+ "speech_ids",
94
+ "speech_part_id",
95
+ "caption_id",
96
+ "caption_ids",
97
+ "bgm_id",
98
+ "anchor_part_id",
99
+ "part_id",
100
+ "body_part_id"
101
+ ]);
102
+ /** Extract every part id a journal entry touches (payload refs + minted ids). */
103
+ function collectAffectedPartIds(journal) {
104
+ const ids = /* @__PURE__ */ new Set();
105
+ for (const entry of journal) {
106
+ for (const generated of entry.generated_ids ?? []) if (generated.length > 0) ids.add(generated);
107
+ collectFromValue(entry.payload, ids);
108
+ }
109
+ return ids;
110
+ }
111
+ function collectFromValue(value, ids) {
112
+ if (value == null) return;
113
+ if (Array.isArray(value)) {
114
+ for (const item of value) collectFromValue(item, ids);
115
+ return;
116
+ }
117
+ if (typeof value !== "object") return;
118
+ for (const [key, child] of Object.entries(value)) {
119
+ if (PART_ID_KEYS.has(key)) {
120
+ if (typeof child === "string" && child.length > 0) ids.add(child);
121
+ else if (Array.isArray(child)) {
122
+ for (const item of child) if (typeof item === "string" && item.length > 0) ids.add(item);
123
+ }
124
+ }
125
+ collectFromValue(child, ids);
126
+ }
127
+ }
128
+ /**
129
+ * Render a ChangePlan preview: header + rows for journal-affected parts only.
130
+ * Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
131
+ */
132
+ function renderPreview(document, journal) {
133
+ return renderCompactProjection(document, { onlyPartIds: journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal) });
134
+ }
135
+ //#endregion
136
+ //#region ../medeo-dsl/src/entities.ts
137
+ function hasSequence(entity) {
138
+ if (isKnownNonSequenceKind(entity.entityKind) || isReservedEntityKind(entity.entityKind)) return false;
139
+ return isSequenceFields(entity);
140
+ }
141
+ function isSequenceFields(value) {
142
+ if (!isRecord$1(value)) return false;
143
+ const extent = value.extent;
144
+ if (!isRecord$1(extent) || !("start" in extent) || extent.start === void 0) return false;
145
+ if (extent.kind === "bounded" && (!("end" in extent) || extent.end === void 0)) return false;
146
+ if (extent.kind === "unbounded" && "end" in extent) return false;
147
+ if (extent.kind !== "bounded" && extent.kind !== "unbounded") return false;
148
+ if (value.sampling !== "native" && value.sampling !== "constant" && value.sampling !== "derived") return false;
149
+ return "coordinateSpace" in value && value.coordinateSpace !== void 0;
150
+ }
151
+ function isKnownSequenceKind(kind) {
152
+ return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
153
+ }
154
+ function isMediaAssetVariantKind(kind) {
155
+ return kind === "video" || kind === "image" || kind === "audio" || kind === "voice";
156
+ }
157
+ function isKnownEntityKind(kind) {
158
+ return isKnownSequenceKind(kind) || isKnownNonSequenceKind(kind);
159
+ }
160
+ function isReservedEntityKind(kind) {
161
+ const normalized = kind.toLowerCase().replaceAll("-", "").replaceAll("_", "");
162
+ return normalized === "speech" || normalized === "videodocument";
163
+ }
164
+ function isKnownNonSequenceKind(kind) {
165
+ return kind === "timeline" || kind === "track" || kind === "clip" || kind === "asset" || kind === "sequence-marker" || kind === "viewport" || kind === "audio-script" || kind === "phonetic-script";
166
+ }
167
+ function isRecord$1(value) {
168
+ return typeof value === "object" && value != null && !Array.isArray(value);
169
+ }
170
+ //#endregion
171
+ //#region ../medeo-dsl/src/ids.ts
172
+ function createEntityId(value) {
173
+ return createId(value, "EntityId");
174
+ }
175
+ function createRelationId(value) {
176
+ return createId(value, "RelationId");
177
+ }
178
+ function createId(value, label) {
179
+ if (value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
180
+ return value;
181
+ }
182
+ //#endregion
183
+ //#region ../medeo-dsl/src/invariants.ts
184
+ /**
185
+ * Media variants own their Asset identity directly: a `physical-asset`
186
+ * Relation may not bind them. Caption inline text (and other non-media
187
+ * Sequence entities) keep their Asset binding.
188
+ */
189
+ function validateMediaAssetIdentity(entity, index) {
190
+ return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
191
+ code: "media_physical_asset_forbidden",
192
+ entityId: entity.entityId,
193
+ message: `Media entity "${entity.entityId}" owns its Asset identity; remove its physical-asset Relation`
194
+ }));
195
+ }
196
+ /** Validates one complete set of entities and its authoritative Relation rows. */
197
+ function validateEntityRelationSet(entityRefs, index, options) {
198
+ const { entities, issues } = collectRelatedEntities(entityRefs, index);
199
+ const entityIds = new Set(entities.map((entity) => entity.entityId));
200
+ for (const entity of entities) {
201
+ const current = entity.current();
202
+ const entityIssues = validateEntity(entity, entityIds);
203
+ issues.push(...entityIssues);
204
+ if (isMediaAssetVariantKind(current.entityKind)) issues.push(...validateMediaAssetIdentity(entity, index));
205
+ if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
206
+ if (current.entityKind === "sequence-marker") {
207
+ const marker = entity;
208
+ issues.push(...validateMarkerUse(entity, index));
209
+ issues.push(...validateMarkerRanges(marker, {
210
+ source: (left, right) => options.compareMarkerPoints(marker, "source", left, right),
211
+ target: (left, right) => options.compareMarkerPoints(marker, "target", left, right)
212
+ }));
213
+ issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
214
+ }
215
+ if (current.entityKind === "clip") {
216
+ issues.push(...validateClipAdmission(entity, index));
217
+ issues.push(...validateClipPlacement(entity, index));
218
+ }
219
+ if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
220
+ if (current.entityKind === "caption") issues.push(...validateCaptionAsset(entity, index));
221
+ issues.push(...validateSequenceComposition(entity));
222
+ }
223
+ issues.push(...validateClipAnchorCycles(entities, index));
224
+ return issues;
225
+ }
226
+ function validateMarkerUse(marker, index) {
227
+ const relations = [...index.relationsOf(marker)];
228
+ const clipEdges = ofKind(relations, "clip-marker");
229
+ const axVideoEdges = ofKind(relations, "axvideo-marker");
230
+ const contentEdges = ofKind(relations, "marker-content");
231
+ const timelineEdges = ofKind(relations, "marker-timeline");
232
+ const issues = [];
233
+ if (clipEdges.length + axVideoEdges.length !== 1) issues.push({
234
+ code: "marker_container_xor",
235
+ entityId: marker.entityId,
236
+ message: "Sequence Marker must have exactly one Clip XOR AXVideo container relation"
237
+ });
238
+ if (contentEdges.length + timelineEdges.length !== 1) issues.push({
239
+ code: "marker_content_xor",
240
+ entityId: marker.entityId,
241
+ message: "Sequence Marker must have exactly one Sequence content XOR Timeline relation"
242
+ });
243
+ if (clipEdges.length === 1 && timelineEdges.length === 1 || axVideoEdges.length === 1 && contentEdges.length === 1) issues.push({
244
+ code: "marker_pair_mismatch",
245
+ entityId: marker.entityId,
246
+ message: "Only Clip+Content or AXVideo+Timeline Marker relation pairs are valid"
247
+ });
248
+ return issues;
249
+ }
250
+ function validateClipAdmission(clip, index) {
251
+ const markerEdges = ofKind([...index.relationsOf(clip)], "clip-marker");
252
+ if (markerEdges.length !== 1) return [{
253
+ code: "clip_marker_cardinality",
254
+ entityId: clip.entityId,
255
+ message: "Clip must have exactly one authoritative Clip-Marker Relation"
256
+ }];
257
+ const marker = markerEdges[0]?.other(clip)?.deref();
258
+ if (marker == null) return [{
259
+ code: "clip_content_cardinality",
260
+ entityId: clip.entityId,
261
+ message: "Clip must resolve one live Sequence Marker and one content Relation"
262
+ }];
263
+ const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
264
+ if (contentEdges.length !== 1) return [{
265
+ code: "clip_content_cardinality",
266
+ entityId: clip.entityId,
267
+ message: "Clip Marker must resolve exactly one content Relation"
268
+ }];
269
+ const content = contentEdges[0]?.other(marker)?.deref()?.current();
270
+ if (content == null) return [{
271
+ code: "clip_content_cardinality",
272
+ entityId: clip.entityId,
273
+ message: "Clip Marker content Relation must resolve one live entity"
274
+ }];
275
+ if (!hasSequence(content)) return [{
276
+ code: "clip_content_not_sequence",
277
+ entityId: clip.entityId,
278
+ message: `Clip Marker resolves to non-Sequence entity "${content.entityId}"`
279
+ }];
280
+ return [];
281
+ }
282
+ function validateAXVideoAdmission(axVideo, index) {
283
+ const markerEdges = ofKind([...index.relationsOf(axVideo)], "axvideo-marker");
284
+ if (markerEdges.length === 1 && markerEdges[0]?.other(axVideo)?.deref() != null) return [];
285
+ return [{
286
+ code: "axvideo_marker_cardinality",
287
+ entityId: axVideo.entityId,
288
+ message: "AXVideo must have exactly one live AXVideo-Marker Relation"
289
+ }];
290
+ }
291
+ function validateCaptionAsset(caption, index) {
292
+ const assets = ofKind([...index.relationsOf(caption)], "physical-asset").map((relation) => relation.other(caption)?.deref()?.current()).filter((entity) => entity?.entityKind === "asset");
293
+ if (assets.length > 0) {
294
+ const captionText = caption.current().text;
295
+ if (!assets.some((asset) => {
296
+ const inline = asset.inline;
297
+ return isRecord(inline) && inline.mediaType === "text/plain" && inline.text !== captionText;
298
+ })) return [];
299
+ return [{
300
+ code: "caption_inline_asset_mismatch",
301
+ entityId: caption.entityId,
302
+ message: "Caption text must match the text/plain inline Physical Asset"
303
+ }];
304
+ }
305
+ return [{
306
+ code: "caption_asset_required",
307
+ entityId: caption.entityId,
308
+ message: "Caption must have a Physical Asset Relation"
309
+ }];
310
+ }
311
+ function validateClipPlacement(clip, index) {
312
+ const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
313
+ if (marker?.current().entityKind !== "sequence-marker") return [];
314
+ const markerValue = marker.current();
315
+ const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
316
+ const hasOrder = clip.current().order !== void 0;
317
+ const hasTarget = markerValue.targetRange !== void 0;
318
+ const hasAnchorOffset = markerValue.anchorOffset !== void 0;
319
+ const issues = [];
320
+ if (anchors.length > 1) issues.push({
321
+ code: "clip_anchor_cardinality",
322
+ entityId: clip.entityId,
323
+ message: "A Clip may follow at most one host Clip"
324
+ });
325
+ const hasAnchor = anchors.length === 1;
326
+ if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
327
+ code: "clip_placement_invalid",
328
+ entityId: clip.entityId,
329
+ message: "Clip placement must be exactly one of Clip.order, Marker.targetRange, or clip-anchor with Marker.anchorOffset"
330
+ });
331
+ if (markerValue.durationPolicy === "timeline") {
332
+ const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
333
+ const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
334
+ const trackRole = track?.entityKind === "track" ? track.role : void 0;
335
+ if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder) issues.push({
336
+ code: "timeline_duration_policy_invalid",
337
+ entityId: marker.entityId,
338
+ message: "Marker durationPolicy \"timeline\" is only valid for ordered Audio Clips on the bgm Track"
339
+ });
340
+ }
341
+ return issues;
342
+ }
343
+ function validateClipAnchorCycles(entities, index) {
344
+ const hostByChild = /* @__PURE__ */ new Map();
345
+ for (const entity of entities) {
346
+ if (entity.current().entityKind !== "clip") continue;
347
+ for (const relation of ofKind([...index.relationsOf(entity)], "clip-anchor")) {
348
+ if (relation.endpoints[0].deref() !== entity) continue;
349
+ const host = relation.endpoints[1].deref();
350
+ if (host != null) hostByChild.set(entity.entityId, host.entityId);
351
+ }
352
+ }
353
+ const issues = [];
354
+ for (const child of hostByChild.keys()) {
355
+ const seen = /* @__PURE__ */ new Set();
356
+ let current = child;
357
+ while (current !== void 0 && !seen.has(current)) {
358
+ seen.add(current);
359
+ current = hostByChild.get(current);
360
+ }
361
+ if (current === void 0) continue;
362
+ issues.push({
363
+ code: "clip_anchor_cycle",
364
+ entityId: child,
365
+ message: "clip-anchor Relations must form an acyclic dependency graph"
366
+ });
367
+ }
368
+ return issues;
369
+ }
370
+ function validateMarkerRanges(marker, compare) {
371
+ const current = marker.current();
372
+ const issues = [];
373
+ if (!(compare.source(current.sourceRange.start, current.sourceRange.end) < 0)) issues.push({
374
+ code: "marker_source_range_empty",
375
+ entityId: marker.entityId,
376
+ message: "Sequence Marker sourceRange must be a non-empty half-open interval"
377
+ });
378
+ if (current.targetRange != null && !(compare.target(current.targetRange.start, current.targetRange.end) < 0)) issues.push({
379
+ code: "marker_target_range_empty",
380
+ entityId: marker.entityId,
381
+ message: "Sequence Marker targetRange must be a non-empty half-open interval"
382
+ });
383
+ return issues;
384
+ }
385
+ function validateMarkerSourceBounds(marker, index, compare) {
386
+ const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
387
+ if (contentEdges.length !== 1) return [];
388
+ const content = contentEdges[0]?.other(marker)?.deref()?.current();
389
+ if (content == null || !hasSequence(content)) return [];
390
+ const sourceRange = marker.current().sourceRange;
391
+ const startVsExtent = compare(sourceRange.start, content.extent.start);
392
+ const endVsExtent = content.extent.kind === "bounded" ? compare(sourceRange.end, content.extent.end) : void 0;
393
+ if (!Number.isNaN(startVsExtent) && startVsExtent >= 0 && (endVsExtent == null || !Number.isNaN(endVsExtent) && endVsExtent <= 0)) return [];
394
+ return [{
395
+ code: "marker_source_out_of_bounds",
396
+ entityId: marker.entityId,
397
+ message: `Sequence Marker sourceRange must stay within content "${content.entityId}" extent`
398
+ }];
399
+ }
400
+ function validateSequenceComposition(entity) {
401
+ const current = entity.current();
402
+ const expected = expectedSequenceShape(current.entityKind);
403
+ if (expected == null) return [];
404
+ if (hasSequence(current) && current.extent.kind === expected.extent && current.sampling === expected.sampling) return [];
405
+ return [{
406
+ code: "invalid_sequence_composition",
407
+ entityId: current.entityId,
408
+ message: `${current.entityKind} must compose ${expected.extent} / ${expected.sampling} Sequence semantics`
409
+ }];
410
+ }
411
+ function validateEntity(entity, entityIds = /* @__PURE__ */ new Set()) {
412
+ const current = entity.current();
413
+ const issues = [];
414
+ if (isReservedEntityKind(current.entityKind)) issues.push({
415
+ code: "forbidden_entity_kind",
416
+ entityId: current.entityId,
417
+ message: `Entity kind "${current.entityKind}" is explicitly outside the Medeo DSL`
418
+ });
419
+ for (const problem of validateKnownEntityPayload(current)) issues.push({
420
+ code: "invalid_entity_payload",
421
+ entityId: current.entityId,
422
+ message: `Entity "${current.entityKind}" ${problem}`
423
+ });
424
+ const peerIdPaths = [...collectPeerEntityIdPaths(current, current.entityKind), ...collectPeerEntityValuePaths(current, current.entityKind, current.entityId, entityIds)];
425
+ const uniquePeerIdPaths = [...new Set(peerIdPaths)].sort();
426
+ if (uniquePeerIdPaths.length > 0) issues.push({
427
+ code: "peer_entity_id_field",
428
+ entityId: current.entityId,
429
+ message: `Entity embeds forbidden peer-ID field/value path(s): ${uniquePeerIdPaths.join(", ")}`
430
+ });
431
+ return issues;
432
+ }
433
+ function validateKnownEntityPayload(entity) {
434
+ const value = entity;
435
+ const problems = [];
436
+ if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
437
+ switch (entity.entityKind) {
438
+ case "track":
439
+ validateOptionalField(value, "hidden", "boolean", problems);
440
+ validateOptionalField(value, "role", "string", problems);
441
+ validateOptionalFiniteNumber(value, "order", problems);
442
+ break;
443
+ case "video":
444
+ case "audio":
445
+ case "voice":
446
+ case "caption":
447
+ validateSequencePayload(value, "bounded", "native", problems);
448
+ if (entity.entityKind !== "caption") validateMediaAssetFields(value, problems);
449
+ if (entity.entityKind === "voice") validateVoicePayload(value, problems);
450
+ if (entity.entityKind === "caption") validateCaptionPayload(value, problems);
451
+ break;
452
+ case "image":
453
+ validateSequencePayload(value, "unbounded", "constant", problems);
454
+ validateMediaAssetFields(value, problems);
455
+ break;
456
+ case "axvideo":
457
+ validateSequencePayload(value, "bounded", "derived", problems);
458
+ break;
459
+ case "sequence-marker":
460
+ validateMarkerPayload(value, problems);
461
+ break;
462
+ case "audio-script":
463
+ case "phonetic-script":
464
+ validateScriptPayload(value, problems);
465
+ break;
466
+ case "timeline":
467
+ case "viewport": break;
468
+ case "clip":
469
+ validateOptionalFiniteNumber(value, "order", problems);
470
+ validateOptionalFiniteNumber(value, "volume", problems);
471
+ if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
472
+ break;
473
+ case "asset":
474
+ validateAssetPayload(value, problems);
475
+ break;
476
+ default: break;
477
+ }
478
+ return problems;
479
+ }
480
+ function validateSequencePayload(value, expectedExtent, expectedSampling, problems) {
481
+ const extent = value.extent;
482
+ if (!isRecord(extent)) problems.push("extent must be an object");
483
+ else {
484
+ if (extent.kind !== expectedExtent) problems.push(`extent.kind must be "${expectedExtent}"`);
485
+ if (!Object.hasOwn(extent, "start") || extent.start === void 0) problems.push("extent.start is required");
486
+ if (expectedExtent === "bounded" && (!Object.hasOwn(extent, "end") || extent.end === void 0)) problems.push("extent.end is required for bounded sequences");
487
+ if (expectedExtent === "unbounded" && Object.hasOwn(extent, "end")) problems.push("extent.end is forbidden for unbounded sequences");
488
+ }
489
+ if (value.sampling !== expectedSampling) problems.push(`sampling must be "${expectedSampling}"`);
490
+ if (!Object.hasOwn(value, "coordinateSpace") || value.coordinateSpace === void 0) problems.push("coordinateSpace is required");
491
+ }
492
+ function validateMarkerPayload(value, problems) {
493
+ validateRange(value.sourceRange, "sourceRange", true, problems);
494
+ if (Object.hasOwn(value, "targetRange") && value.targetRange !== void 0) validateRange(value.targetRange, "targetRange", true, problems);
495
+ const duration = value.duration;
496
+ if (!isRecord(duration)) problems.push("duration must be an object");
497
+ else if (duration.mode === "fixed") {
498
+ if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
499
+ } else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
500
+ if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
501
+ if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
502
+ }
503
+ function validateAssetPayload(value, problems) {
504
+ validateExternalLocator(value, problems);
505
+ validateStorageKey(value, problems);
506
+ const inline = value.inline;
507
+ if (inline !== void 0) if (!isRecord(inline)) problems.push("inline must be an object when present");
508
+ else {
509
+ if (inline.mediaType !== "text/plain") problems.push("inline.mediaType must be \"text/plain\"");
510
+ if (typeof inline.text !== "string") problems.push("inline.text must be a string");
511
+ }
512
+ if (value.external !== void 0 && value.inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
513
+ }
514
+ /**
515
+ * Media variants own their Asset locator directly: `external` is required and
516
+ * inline text stays an Asset-kind-only value so the two physical-location
517
+ * vocabularies cannot mix.
518
+ */
519
+ function validateMediaAssetFields(value, problems) {
520
+ if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
521
+ else validateExternalLocator(value, problems);
522
+ validateStorageKey(value, problems);
523
+ if (value.inline !== void 0) problems.push("inline is an Asset-kind field; media variants use external");
524
+ }
525
+ function validateExternalLocator(value, problems) {
526
+ const external = value.external;
527
+ if (external === void 0) return;
528
+ if (!isRecord(external)) {
529
+ problems.push("external must be an object when present");
530
+ return;
531
+ }
532
+ if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
533
+ if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
534
+ }
535
+ function validateStorageKey(value, problems) {
536
+ if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
537
+ }
538
+ function validateVoicePayload(value, problems) {
539
+ const voice = value.voice;
540
+ if (voice === void 0) return;
541
+ if (!isRecord(voice)) {
542
+ problems.push("voice must be an object when present");
543
+ return;
544
+ }
545
+ if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
546
+ if (typeof voice.key !== "string" || voice.key.trim() === "") problems.push("voice.key must be a non-empty string");
547
+ if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
548
+ }
549
+ function validateCaptionPayload(value, problems) {
550
+ if (value.text !== void 0 && typeof value.text !== "string") problems.push("text must be a string when present");
551
+ const style = value.style;
552
+ if (style === void 0) return;
553
+ if (!isRecord(style)) {
554
+ problems.push("style must be an object when present");
555
+ return;
556
+ }
557
+ const font = style.font;
558
+ if (font !== void 0) if (!isRecord(font)) problems.push("style.font must be an object when present");
559
+ else {
560
+ if (font.system !== "font-library") problems.push("style.font.system must be \"font-library\"");
561
+ if (typeof font.key !== "string" || font.key.trim() === "") problems.push("style.font.key must be a non-empty string");
562
+ }
563
+ for (const key of [
564
+ "fontSize",
565
+ "fontWeight",
566
+ "entranceAnimationDurationMs",
567
+ "strokeWidth",
568
+ "positionX",
569
+ "positionY"
570
+ ]) validateOptionalFiniteNumber(style, key, problems, `style.${key}`);
571
+ for (const key of [
572
+ "fontColor",
573
+ "entranceAnimation",
574
+ "strokeColor"
575
+ ]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
576
+ }
577
+ function validateRange(value, path, required, problems) {
578
+ if (!isRecord(value)) {
579
+ if (required) problems.push(`${path} must be an object`);
580
+ return;
581
+ }
582
+ if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
583
+ if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
584
+ }
585
+ function validateScriptPayload(value, problems) {
586
+ if (!Array.isArray(value.segments)) {
587
+ problems.push("segments must be an array");
588
+ return;
589
+ }
590
+ for (const [index, segment] of value.segments.entries()) {
591
+ if (!isRecord(segment)) {
592
+ problems.push(`segments[${index}] must be an object`);
593
+ continue;
594
+ }
595
+ if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
596
+ if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
597
+ if (segment.language !== void 0 && typeof segment.language !== "string") problems.push(`segments[${index}].language must be a string when present`);
598
+ }
599
+ }
600
+ function validateOptionalField(value, key, expectedType, problems) {
601
+ if (value[key] !== void 0 && typeof value[key] !== expectedType) problems.push(`${key} must be a ${expectedType} when present`);
602
+ }
603
+ function validateOptionalFiniteNumber(value, key, problems, label = key) {
604
+ if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isFinite(value[key]))) problems.push(`${label} must be a finite number when present`);
605
+ }
606
+ function collectPeerEntityIdPaths(value, entityKind) {
607
+ const paths = [];
608
+ visitPeerEntityIdPaths(value, entityKind, "", /* @__PURE__ */ new Set(), paths);
609
+ return paths;
610
+ }
611
+ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths) {
612
+ if (typeof value !== "object" || value == null) return;
613
+ if (ancestors.has(value)) return;
614
+ ancestors.add(value);
615
+ if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityIdPaths(item, entityKind, `${parentPath}[${index}]`, ancestors, paths);
616
+ else for (const [key, child] of Object.entries(value)) {
617
+ const path = parentPath.length === 0 ? key : `${parentPath}.${key}`;
618
+ if (!(parentPath.length === 0 && key === "entityId") && !isOwnedLocalIdPath(entityKind, path) && isEntityIdFieldName(key)) paths.push(path);
619
+ visitPeerEntityIdPaths(child, entityKind, path, ancestors, paths);
620
+ }
621
+ ancestors.delete(value);
622
+ }
623
+ function collectPeerEntityValuePaths(value, entityKind, ownEntityId, entityIds) {
624
+ const paths = [];
625
+ visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, "", /* @__PURE__ */ new Set(), paths);
626
+ return paths;
627
+ }
628
+ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path, ancestors, paths) {
629
+ if (typeof value === "string") {
630
+ if (value !== ownEntityId && entityIds.has(value) && !isOwnedLocalEntityValuePath(entityKind, path)) paths.push(path);
631
+ return;
632
+ }
633
+ if (typeof value !== "object" || value == null || ancestors.has(value)) return;
634
+ ancestors.add(value);
635
+ if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
636
+ else for (const [key, child] of Object.entries(value)) {
637
+ if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
638
+ visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
639
+ }
640
+ ancestors.delete(value);
641
+ }
642
+ function isOwnedLocalEntityValuePath(entityKind, path) {
643
+ if (entityKind === "track" && path === "role") return true;
644
+ if ([
645
+ "video",
646
+ "audio",
647
+ "voice",
648
+ "image",
649
+ "caption",
650
+ "axvideo"
651
+ ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
652
+ if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
653
+ if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey|inline\.(?:mediaType|text))$/.test(path)) return true;
654
+ if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
655
+ if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
656
+ if (entityKind === "caption" && (path === "text" || path.startsWith("style."))) return true;
657
+ if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
658
+ return false;
659
+ }
660
+ function isOwnedLocalIdPath(entityKind, path) {
661
+ if (path === "lifecycle.actorId") return true;
662
+ if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
663
+ if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
664
+ return false;
665
+ }
666
+ function isEntityIdFieldName(key) {
667
+ return key.endsWith("Id") || key.endsWith("Ids") || key.endsWith("ID") || key.endsWith("IDs") || /_ids?$/i.test(key);
668
+ }
669
+ function isRecord(value) {
670
+ return typeof value === "object" && value != null && !Array.isArray(value);
671
+ }
672
+ function collectRelatedEntities(entityRefs, index) {
673
+ const byId = /* @__PURE__ */ new Map();
674
+ const queue = [...entityRefs];
675
+ const issues = [];
676
+ while (queue.length > 0) {
677
+ const entity = queue.shift();
678
+ if (entity == null) continue;
679
+ const existing = byId.get(entity.entityId);
680
+ if (existing != null) {
681
+ if (existing !== entity) issues.push({
682
+ code: "duplicate_entity_id",
683
+ entityId: entity.entityId,
684
+ message: `Entity id "${entity.entityId}" has more than one live EntityRef`
685
+ });
686
+ continue;
687
+ }
688
+ byId.set(entity.entityId, entity);
689
+ for (const relation of index.relationsOf(entity)) for (const endpoint of relation.endpoints) {
690
+ const ref = endpoint.deref();
691
+ if (ref != null && !byId.has(ref.entityId)) queue.push(ref);
692
+ }
693
+ }
694
+ return {
695
+ entities: [...byId.values()],
696
+ issues
697
+ };
698
+ }
699
+ function expectedSequenceShape(kind) {
700
+ if (kind === "video" || kind === "audio" || kind === "voice" || kind === "caption") return {
701
+ extent: "bounded",
702
+ sampling: "native"
703
+ };
704
+ if (kind === "image") return {
705
+ extent: "unbounded",
706
+ sampling: "constant"
707
+ };
708
+ if (kind === "axvideo") return {
709
+ extent: "bounded",
710
+ sampling: "derived"
711
+ };
712
+ }
713
+ function ofKind(relations, kind) {
714
+ return relations.filter((relation) => relation.kind === kind);
715
+ }
716
+ //#endregion
717
+ //#region ../medeo-dsl/src/json-values.ts
718
+ const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
719
+ function isJsonObject(value) {
720
+ return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
721
+ }
722
+ function isJsonValue(value, ancestors) {
723
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
724
+ if (typeof value === "number") return Number.isFinite(value);
725
+ if (typeof value !== "object") return false;
726
+ if (!Array.isArray(value) && !isPlainObject(value)) return false;
727
+ if (ancestors.has(value)) return false;
728
+ ancestors.add(value);
729
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
730
+ ancestors.delete(value);
731
+ return valid;
732
+ }
733
+ /** Recognize an ordinary object from any VM realm without admitting class instances. */
734
+ function isPlainObject(value) {
735
+ try {
736
+ const prototype = Object.getPrototypeOf(value);
737
+ if (prototype === null) return true;
738
+ if (Object.getPrototypeOf(prototype) !== null) return false;
739
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
740
+ return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
741
+ } catch {
742
+ return false;
743
+ }
744
+ }
745
+ //#endregion
746
+ //#region ../medeo-dsl/src/relation-specs.ts
747
+ const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
748
+ const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
749
+ const clipMarkerRelationSpec = emptySpec("clip-marker", "clip", "sequence-marker");
750
+ const axVideoMarkerRelationSpec = emptySpec("axvideo-marker", "axvideo", "sequence-marker");
751
+ const markerTimelineRelationSpec = emptySpec("marker-timeline", "sequence-marker", "timeline");
752
+ const markerContentRelationSpec = Object.freeze({
753
+ kind: "marker-content",
754
+ validateEndpoints: (endpoints) => hasMarkerAndSequence(endpoints),
755
+ validateMetadata: isEmptyMetadata
756
+ });
757
+ const physicalAssetRelationSpec = Object.freeze({
758
+ kind: "physical-asset",
759
+ validateEndpoints: (endpoints) => hasAssetAndSequence(endpoints),
760
+ validateMetadata: isAssetBinding
761
+ });
762
+ /** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
763
+ const generatedRelationSpec = Object.freeze({
764
+ kind: "generated",
765
+ validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
766
+ validateMetadata: isEmptyMetadata
767
+ });
768
+ const phoneticScriptProvenanceRelationSpec = metadataSpec("phonetic-script-provenance", "phonetic-script", "audio-script", isSegmentAlignmentMetadata);
769
+ const captionProvenanceRelationSpec = metadataSpec("caption-provenance", "caption", "audio-script", isSegmentAlignmentMetadata);
770
+ const captionAlignmentRelationSpec = Object.freeze({
771
+ kind: "caption-alignment",
772
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
773
+ validateMetadata: isCaptionAlignmentMetadata
774
+ });
775
+ /** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
776
+ const clipAnchorRelationSpec = Object.freeze({
777
+ kind: "clip-anchor",
778
+ validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
779
+ validateMetadata: isEmptyMetadata
780
+ });
781
+ /** `audio-script-render(output, script)` means endpoint 0 was rendered from endpoint 1. */
782
+ const audioScriptRenderRelationSpec = Object.freeze({
783
+ kind: "audio-script-render",
784
+ validateEndpoints: (endpoints) => {
785
+ const outputKind = endpoints[0].current().entityKind;
786
+ return (outputKind === "audio" || outputKind === "voice") && endpoints[1].current().entityKind === "audio-script";
787
+ },
788
+ validateMetadata: isEmptyMetadata
789
+ });
790
+ /** Built-in kinds are reserved; callers may add specs only under new names. */
791
+ const builtInRelationSpecs = Object.freeze([
792
+ timelineTrackRelationSpec,
793
+ trackClipRelationSpec,
794
+ clipMarkerRelationSpec,
795
+ markerContentRelationSpec,
796
+ axVideoMarkerRelationSpec,
797
+ markerTimelineRelationSpec,
798
+ physicalAssetRelationSpec,
799
+ generatedRelationSpec,
800
+ phoneticScriptProvenanceRelationSpec,
801
+ captionProvenanceRelationSpec,
802
+ captionAlignmentRelationSpec,
803
+ clipAnchorRelationSpec,
804
+ audioScriptRenderRelationSpec
805
+ ]);
806
+ function emptySpec(kind, a, b) {
807
+ return metadataSpec(kind, a, b, isEmptyMetadata);
808
+ }
809
+ function metadataSpec(kind, a, b, validateMetadata) {
810
+ return Object.freeze({
811
+ kind,
812
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set([a]), new Set([b])),
813
+ validateMetadata
814
+ });
815
+ }
816
+ function hasKinds(endpoints, aKinds, bKinds) {
817
+ const first = endpoints[0].current().entityKind;
818
+ const second = endpoints[1].current().entityKind;
819
+ return aKinds.has(first) && bKinds.has(second) || aKinds.has(second) && bKinds.has(first);
820
+ }
821
+ function hasMarkerAndSequence(endpoints) {
822
+ const first = endpoints[0].current();
823
+ const second = endpoints[1].current();
824
+ return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
825
+ }
826
+ function hasAssetAndSequence(endpoints) {
827
+ const first = endpoints[0].current();
828
+ const second = endpoints[1].current();
829
+ return first.entityKind === "asset" && hasSequence(second) || second.entityKind === "asset" && hasSequence(first);
830
+ }
831
+ function isGeneratedMedia(entity) {
832
+ return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voice";
833
+ }
834
+ function isEmptyMetadata(value) {
835
+ return isJsonObject(value) && Object.keys(value).length === 0;
836
+ }
837
+ function isAssetBinding(value) {
838
+ if (!isJsonObject(value)) return false;
839
+ const orderingKey = /(order|ordinal|position|rank|index|z[_-]?index)/i;
840
+ return Object.keys(value).every((key) => !orderingKey.test(key));
841
+ }
842
+ function isSegmentAlignmentMetadata(value) {
843
+ return isJsonObject(value) && Object.hasOwn(value, "segmentAlignment");
844
+ }
845
+ function isCaptionAlignmentMetadata(value) {
846
+ return isJsonObject(value) && Object.hasOwn(value, "alignment");
847
+ }
848
+ //#endregion
849
+ //#region ../medeo-dsl/src/relations.ts
850
+ function createEntityRef(entity) {
851
+ return {
852
+ entityId: entity.entityId,
853
+ current: () => entity
854
+ };
855
+ }
856
+ //#endregion
857
+ //#region ../medeo-dsl/src/relation-index.ts
858
+ var RelationEdge = class {
859
+ relationId;
860
+ kind;
861
+ metadata;
862
+ trace;
863
+ endpoints;
864
+ constructor(relationId, kind, endpoints, metadata, trace) {
865
+ this.relationId = relationId;
866
+ this.kind = kind;
867
+ this.metadata = metadata;
868
+ this.trace = trace;
869
+ this.endpoints = [new WeakRef(endpoints[0]), new WeakRef(endpoints[1])];
870
+ }
871
+ other(entity) {
872
+ const first = this.endpoints[0].deref();
873
+ const second = this.endpoints[1].deref();
874
+ if (first === entity) return this.endpoints[1];
875
+ if (second === entity) return this.endpoints[0];
876
+ }
877
+ toRow() {
878
+ const first = this.endpoints[0].deref();
879
+ const second = this.endpoints[1].deref();
880
+ if (first == null || second == null) return void 0;
881
+ return {
882
+ relationId: this.relationId,
883
+ endpoint0EntityId: first.entityId,
884
+ endpoint1EntityId: second.entityId,
885
+ relationKind: this.kind,
886
+ metadata: this.metadata,
887
+ trace: this.trace
888
+ };
889
+ }
890
+ isStale() {
891
+ return this.endpoints[0].deref() == null || this.endpoints[1].deref() == null;
892
+ }
893
+ };
894
+ /** Endpoint-agnostic secondary index. Relation rows remain the persistence authority. */
895
+ var BiRelationIndex = class {
896
+ byEntity = /* @__PURE__ */ new WeakMap();
897
+ canonicalRefs = /* @__PURE__ */ new Map();
898
+ byRelationId = /* @__PURE__ */ new Map();
899
+ link(input) {
900
+ if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
901
+ if (input.spec.kind === "clip-anchor" || input.spec.kind === "audio-script-render") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
902
+ return this.linkValidated(input);
903
+ }
904
+ /** Author `generated(output, input)` without exposing positional arguments. */
905
+ linkGenerated(input) {
906
+ return this.linkValidated({
907
+ relationId: input.relationId,
908
+ spec: generatedRelationSpec,
909
+ endpoints: [input.output, input.input],
910
+ metadata: {},
911
+ trace: input.trace
912
+ });
913
+ }
914
+ /** Author `clip-anchor(child, host)` without exposing positional arguments. */
915
+ linkClipAnchor(input) {
916
+ return this.linkValidated({
917
+ relationId: input.relationId,
918
+ spec: clipAnchorRelationSpec,
919
+ endpoints: [input.child, input.host],
920
+ metadata: {},
921
+ trace: input.trace
922
+ });
923
+ }
924
+ /** Author `audio-script-render(output, script)` without exposing positional arguments. */
925
+ linkAudioScriptRender(input) {
926
+ return this.linkValidated({
927
+ relationId: input.relationId,
928
+ spec: audioScriptRenderRelationSpec,
929
+ endpoints: [input.output, input.script],
930
+ metadata: {},
931
+ trace: input.trace
932
+ });
933
+ }
934
+ /**
935
+ * Rehydrate a persisted row after resolving its spec and endpoint refs.
936
+ *
937
+ * This is a storage-boundary escape hatch, not an authoring API: persisted
938
+ * positions already are the semantic assertion made by their relation kind.
939
+ */
940
+ linkRuntime(input) {
941
+ return this.linkValidated(input);
942
+ }
943
+ linkValidated(input) {
944
+ const [first, second] = input.endpoints;
945
+ if (first.entityId === second.entityId) throw new Error("A Relation cannot connect an entity to itself");
946
+ this.assertRelationIdAvailable(input.relationId);
947
+ const builtInSpec = builtInSpecByKind.get(input.spec.kind);
948
+ if (builtInSpec != null && builtInSpec !== input.spec) throw new Error(`Relation kind "${input.spec.kind}" must use its built-in specification`);
949
+ if (isForbiddenAuthoritativeRelation(input.spec.kind, input.endpoints)) throw new Error("A direct Clip-Content Relation is derived-only and cannot be authoritative");
950
+ if (!input.spec.validateEndpoints(input.endpoints)) throw new Error(`Relation "${input.spec.kind}" received invalid endpoints`);
951
+ if (!input.spec.validateMetadata(input.metadata)) throw new Error(`Relation "${input.spec.kind}" received invalid metadata`);
952
+ this.assertCanonicalRefAvailable(first);
953
+ this.assertCanonicalRefAvailable(second);
954
+ const relation = new RelationEdge(input.relationId, input.spec.kind, input.endpoints, input.metadata, input.trace ?? {});
955
+ this.rememberCanonicalRef(first);
956
+ this.rememberCanonicalRef(second);
957
+ this.add(first, relation);
958
+ this.add(second, relation);
959
+ this.byRelationId.set(input.relationId, new WeakRef(relation));
960
+ return relation;
961
+ }
962
+ relationsOf(entity) {
963
+ this.registerCanonicalRef(entity);
964
+ const relations = this.byEntity.get(entity);
965
+ if (relations == null) return /* @__PURE__ */ new Set();
966
+ for (const relation of relations) if (relation instanceof RelationEdge && relation.isStale()) this.unlink(relation);
967
+ return new Set(relations);
968
+ }
969
+ unlink(relation) {
970
+ for (const endpoint of relation.endpoints) {
971
+ const ref = endpoint.deref();
972
+ if (ref != null) this.byEntity.get(ref)?.delete(relation);
973
+ }
974
+ if (this.byRelationId.get(relation.relationId)?.deref() === relation) this.byRelationId.delete(relation.relationId);
975
+ }
976
+ add(entity, relation) {
977
+ const relations = this.byEntity.get(entity) ?? /* @__PURE__ */ new Set();
978
+ relations.add(relation);
979
+ this.byEntity.set(entity, relations);
980
+ }
981
+ registerCanonicalRef(entity) {
982
+ this.assertCanonicalRefAvailable(entity);
983
+ this.rememberCanonicalRef(entity);
984
+ }
985
+ assertCanonicalRefAvailable(entity) {
986
+ const existing = this.canonicalRefs.get(entity.entityId)?.deref();
987
+ if (existing != null && existing !== entity) throw new Error(`Entity "${entity.entityId}" already has a live canonical EntityRef`);
988
+ }
989
+ rememberCanonicalRef(entity) {
990
+ this.canonicalRefs.set(entity.entityId, new WeakRef(entity));
991
+ }
992
+ assertRelationIdAvailable(relationId) {
993
+ if (this.byRelationId.get(relationId)?.deref() != null) throw new Error(`Relation id "${relationId}" already exists`);
994
+ this.byRelationId.delete(relationId);
995
+ }
996
+ };
997
+ const builtInSpecByKind = new Map(builtInRelationSpecs.map((spec) => [spec.kind, spec]));
998
+ function isForbiddenAuthoritativeRelation(kind, endpoints) {
999
+ if (kind === "clip-content") return true;
1000
+ const first = endpoints[0].current();
1001
+ const second = endpoints[1].current();
1002
+ return first.entityKind === "clip" && hasSequence(second) || second.entityKind === "clip" && hasSequence(first);
1003
+ }
1004
+ //#endregion
1005
+ //#region ../medeo-dsl/src/entity-relation-rows.ts
1006
+ var InvalidEntityRelationRowsError = class extends Error {
1007
+ issues;
1008
+ constructor(issues) {
1009
+ super(`Invalid Medeo entity/relation rows:\n- ${issues.join("\n- ")}`);
1010
+ this.issues = issues;
1011
+ this.name = "InvalidEntityRelationRowsError";
1012
+ }
1013
+ };
1014
+ /** Decodes database rows into flat entities and validates the complete relation set. */
1015
+ function decodeEntityRelationRows(rows, options) {
1016
+ const issues = [];
1017
+ const entitiesById = /* @__PURE__ */ new Map();
1018
+ const extensionEntityKinds = new Set(options.entityKinds ?? []);
1019
+ for (const row of rows.entities) {
1020
+ const entity = decodeEntityRow(row, extensionEntityKinds, issues);
1021
+ if (entity == null) continue;
1022
+ const ref = createEntityRef(entity);
1023
+ if (entitiesById.has(ref.entityId)) {
1024
+ issues.push(`duplicate entity id "${ref.entityId}"`);
1025
+ continue;
1026
+ }
1027
+ entitiesById.set(ref.entityId, ref);
1028
+ }
1029
+ const specsByKind = collectRelationSpecs(options.relationSpecs ?? [], issues);
1030
+ const relationIndex = new BiRelationIndex();
1031
+ const relations = [];
1032
+ const relationIds = /* @__PURE__ */ new Set();
1033
+ for (const row of rows.relations) {
1034
+ let relationId;
1035
+ try {
1036
+ relationId = createRelationId(row.relationId);
1037
+ } catch (error) {
1038
+ issues.push(errorMessage(error));
1039
+ continue;
1040
+ }
1041
+ if (relationIds.has(relationId)) {
1042
+ issues.push(`duplicate relation id "${relationId}"`);
1043
+ continue;
1044
+ }
1045
+ relationIds.add(relationId);
1046
+ if (!isTrimmedNonEmpty(row.relationKind)) {
1047
+ issues.push(`relation "${relationId}" has an empty or untrimmed kind`);
1048
+ continue;
1049
+ }
1050
+ const spec = specsByKind.get(row.relationKind);
1051
+ if (spec == null) {
1052
+ issues.push(`relation "${relationId}" has no registered spec for kind "${row.relationKind}"`);
1053
+ continue;
1054
+ }
1055
+ let endpoint0EntityId;
1056
+ let endpoint1EntityId;
1057
+ try {
1058
+ endpoint0EntityId = createEntityId(row.endpoint0EntityId);
1059
+ endpoint1EntityId = createEntityId(row.endpoint1EntityId);
1060
+ } catch (error) {
1061
+ issues.push(errorMessage(error));
1062
+ continue;
1063
+ }
1064
+ const endpoint0 = entitiesById.get(endpoint0EntityId);
1065
+ const endpoint1 = entitiesById.get(endpoint1EntityId);
1066
+ if (endpoint0 == null || endpoint1 == null) {
1067
+ const missing = [endpoint0 == null ? endpoint0EntityId : void 0, endpoint1 == null ? endpoint1EntityId : void 0].filter((value) => value != null).join(", ");
1068
+ issues.push(`relation "${relationId}" references missing entity id(s): ${missing}`);
1069
+ continue;
1070
+ }
1071
+ if (!isJsonObject(row.metadata) || !isJsonObject(row.trace)) {
1072
+ issues.push(`relation "${relationId}" metadata and trace must contain only JSON values`);
1073
+ continue;
1074
+ }
1075
+ try {
1076
+ relations.push(relationIndex.linkRuntime({
1077
+ relationId,
1078
+ spec,
1079
+ endpoints: [endpoint0, endpoint1],
1080
+ metadata: row.metadata,
1081
+ trace: row.trace
1082
+ }));
1083
+ } catch (error) {
1084
+ issues.push(errorMessage(error));
1085
+ }
1086
+ }
1087
+ if (issues.length === 0) for (const issue of validateEntityRelationSet([...entitiesById.values()], relationIndex, options)) issues.push(`${issue.code}: ${issue.message}`);
1088
+ if (issues.length > 0) throw new InvalidEntityRelationRowsError(issues);
1089
+ return {
1090
+ entitiesById,
1091
+ relationIndex,
1092
+ relations
1093
+ };
1094
+ }
1095
+ function decodeEntityRow(row, extensionEntityKinds, issues) {
1096
+ let entityId;
1097
+ try {
1098
+ entityId = createEntityId(row.entityId);
1099
+ } catch (error) {
1100
+ issues.push(errorMessage(error));
1101
+ return;
1102
+ }
1103
+ if (!isTrimmedNonEmpty(row.entityKind)) {
1104
+ issues.push(`entity "${entityId}" has an empty or untrimmed kind`);
1105
+ return;
1106
+ }
1107
+ if (isReservedEntityKind(row.entityKind)) {
1108
+ issues.push(`entity "${entityId}" uses reserved kind "${row.entityKind}"`);
1109
+ return;
1110
+ }
1111
+ if (!isKnownEntityKind(row.entityKind) && !extensionEntityKinds.has(row.entityKind)) {
1112
+ issues.push(`entity "${entityId}" has unregistered extension kind "${row.entityKind}"`);
1113
+ return;
1114
+ }
1115
+ if (!isJsonObject(row.payload)) {
1116
+ issues.push(`entity "${entityId}" payload must contain only JSON values`);
1117
+ return;
1118
+ }
1119
+ const reserved = ["entityId", "entityKind"].filter((key) => Object.hasOwn(row.payload, key));
1120
+ if (reserved.length > 0) {
1121
+ issues.push(`entity "${entityId}" payload contains reserved field(s): ${reserved.join(", ")}`);
1122
+ return;
1123
+ }
1124
+ return {
1125
+ ...row.payload,
1126
+ entityId,
1127
+ entityKind: row.entityKind
1128
+ };
1129
+ }
1130
+ function collectRelationSpecs(extensionSpecs, issues) {
1131
+ const specs = /* @__PURE__ */ new Map();
1132
+ for (const spec of builtInRelationSpecs) specs.set(spec.kind, spec);
1133
+ for (const spec of extensionSpecs) {
1134
+ if (!isTrimmedNonEmpty(spec.kind)) {
1135
+ issues.push("extension relation spec has an empty or untrimmed kind");
1136
+ continue;
1137
+ }
1138
+ if (specs.has(spec.kind)) {
1139
+ issues.push(`relation spec kind "${spec.kind}" is already registered`);
1140
+ continue;
1141
+ }
1142
+ specs.set(spec.kind, spec);
1143
+ }
1144
+ return specs;
1145
+ }
1146
+ function isTrimmedNonEmpty(value) {
1147
+ return value.length > 0 && value.trim() === value;
1148
+ }
1149
+ function errorMessage(error) {
1150
+ return error instanceof Error ? error.message : String(error);
1151
+ }
1152
+ //#endregion
1153
+ //#region ../medeo-dsl/src/rows.ts
1154
+ function entityToRow(entity) {
1155
+ const { entityId, entityKind, ...payload } = entity;
1156
+ if (!isJsonObject(payload)) throw new Error(`Entity "${entityId}" payload must contain only JSON values`);
1157
+ return {
1158
+ entityId,
1159
+ entityKind,
1160
+ payload
1161
+ };
1162
+ }
1163
+ //#endregion
1164
+ //#region src/entity/entity-sandbox.ts
1165
+ const ASSET_SOURCE_KEY = "external";
1166
+ const MEMOTA_SYSTEM = "memota";
1167
+ /** Mutable entity/relation draft whose only durable product is an explicit command plan. */
1168
+ var EntitySandbox = class {
1169
+ original;
1170
+ idFactory;
1171
+ onCommand;
1172
+ onTruncate;
1173
+ state;
1174
+ commands = [];
1175
+ entities;
1176
+ relations;
1177
+ constructor(options) {
1178
+ this.original = cloneSnapshot(options.state ?? {
1179
+ revision: 0,
1180
+ entities: [],
1181
+ relations: []
1182
+ });
1183
+ this.state = cloneSnapshot(this.original);
1184
+ this.idFactory = options.idFactory;
1185
+ this.onCommand = options.onCommand;
1186
+ this.onTruncate = options.onTruncate;
1187
+ this.entities = this.buildEntityFacade();
1188
+ this.relations = this.buildRelationFacade();
1189
+ }
1190
+ get commandCount() {
1191
+ return this.commands.length;
1192
+ }
1193
+ getCommands() {
1194
+ return this.commands;
1195
+ }
1196
+ /** Host-owned fixed structure is journaled through the same CAS graph as model edits. */
1197
+ ensureFoundation(timelinePayload = {}) {
1198
+ const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload);
1199
+ this.appendResourceRows(foundation.rows);
1200
+ }
1201
+ rollbackTo(index) {
1202
+ if (!Number.isInteger(index) || index < 0 || index > this.commands.length) throw new Error(`rollbackTo: entity checkpoint index ${index} is past journal length ${this.commands.length}`);
1203
+ const prefix = this.commands.slice(0, index);
1204
+ this.state = cloneSnapshot(this.original);
1205
+ for (const command of prefix) this.apply(command, false);
1206
+ this.commands.length = 0;
1207
+ this.commands.push(...prefix);
1208
+ this.onTruncate?.(index);
1209
+ }
1210
+ buildPlan() {
1211
+ const rows = toDslRows(this.state);
1212
+ decodeEntityRelationRows(rows, numericMarkerComparators);
1213
+ assertCanonicalEditorResources(rows);
1214
+ const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
1215
+ const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
1216
+ return {
1217
+ base_revision: this.original.revision,
1218
+ commands: this.commands.slice(),
1219
+ rows: cloneSnapshot(this.state),
1220
+ deleted_entity_ids: this.original.entities.map((entity) => entity.entity_id).filter((entityId) => !currentEntityIds.has(entityId)).sort(),
1221
+ deleted_relation_ids: this.original.relations.map((relation) => relation.relation_id).filter((relationId) => !currentRelationIds.has(relationId)).sort()
1222
+ };
1223
+ }
1224
+ renderPreview() {
1225
+ const lines = [`Entity plan: base_revision=${this.original.revision} commands=${this.commands.length} entities=${this.state.entities.length} relations=${this.state.relations.length}`];
1226
+ for (const command of this.commands) switch (command.kind) {
1227
+ case "create-entity":
1228
+ lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
1229
+ break;
1230
+ case "update-entity":
1231
+ lines.push(`~ entity ${command.entity_id} payload`);
1232
+ break;
1233
+ case "delete-entity":
1234
+ lines.push(`- entity ${command.entity_id}`);
1235
+ break;
1236
+ case "unlink-relation":
1237
+ lines.push(`- relation ${command.relation_id}`);
1238
+ break;
1239
+ case "link-relation":
1240
+ if (command.relation.relation_kind === "generated") lines.push(`+ relation ${command.relation.relation_id} generated(output=${command.relation.endpoint_0_entity_id}, input=${command.relation.endpoint_1_entity_id})`);
1241
+ else lines.push(`+ relation ${command.relation.relation_id} kind=${command.relation.relation_kind} endpoints=${command.relation.endpoint_0_entity_id},${command.relation.endpoint_1_entity_id}`);
1242
+ break;
1243
+ }
1244
+ return lines.join("\n");
1245
+ }
1246
+ buildEntityFacade() {
1247
+ return {
1248
+ list: () => clone(this.state.entities),
1249
+ get: (entityId) => {
1250
+ const entity = this.state.entities.find((candidate) => candidate.entity_id === entityId);
1251
+ return entity == null ? null : clone(entity);
1252
+ },
1253
+ findByAssetId: (assetId) => {
1254
+ assertTrimmed(assetId, "assetId");
1255
+ return clone(this.state.entities.filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isImportedMemotaAsset(entity.payload, assetId)));
1256
+ },
1257
+ create: (input) => this.createEntity(input),
1258
+ update: (input) => this.updateEntity(input),
1259
+ delete: (input) => this.deleteEntity(input),
1260
+ ensureMedia: (fact) => this.ensureMedia(fact)
1261
+ };
1262
+ }
1263
+ buildRelationFacade() {
1264
+ return {
1265
+ list: () => clone(this.state.relations),
1266
+ of: (entityId, relationKind) => {
1267
+ assertTrimmed(entityId, "entityId");
1268
+ if (relationKind !== void 0 && !builtInRelationSpecs.some((spec) => spec.kind === relationKind)) throw new Error(`Unknown Relation kind "${relationKind}"`);
1269
+ return clone(this.state.relations.filter((relation) => (relation.endpoint_0_entity_id === entityId || relation.endpoint_1_entity_id === entityId) && (relationKind === void 0 || relation.relation_kind === relationKind)));
1270
+ },
1271
+ link: (input) => this.link(input),
1272
+ linkGenerated: (input) => this.linkGenerated(input),
1273
+ linkClipAnchor: (input) => this.linkClipAnchor(input),
1274
+ linkAudioScriptRender: (input) => this.linkAudioScriptRender(input),
1275
+ unlink: (input) => this.unlinkRelation(input)
1276
+ };
1277
+ }
1278
+ createEntity(input) {
1279
+ if (!isKnownEntityKind(input.entity_kind)) throw new Error(`Unknown or extension Entity kind "${String(input.entity_kind)}"`);
1280
+ const payload = clone(input.payload);
1281
+ if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
1282
+ if (input.entity_kind === "timeline" || input.entity_kind === "track") {
1283
+ const matches = this.state.entities.filter((entity) => entity.entity_kind === input.entity_kind && (input.entity_kind === "timeline" || entity.payload.role === payload.role));
1284
+ if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
1285
+ if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
1286
+ }
1287
+ const external = payload.external;
1288
+ if (isMediaAssetVariantKind(input.entity_kind) && isJsonObject(external) && (external.system === "memota" || external.system === "memota-speech") && typeof external.key === "string") {
1289
+ const matches = this.state.entities.filter((entity) => isMediaAssetVariantKind(entity.entity_kind) && isJsonObject(entity.payload.external) && entity.payload.external.system === external.system && entity.payload.external.key === external.key);
1290
+ if (matches.length > 1) throw new Error(`Ambiguous Asset bindings for ${external.key}`);
1291
+ const existing = matches[0];
1292
+ if (existing !== void 0) {
1293
+ if (existing.entity_kind !== input.entity_kind) throw new Error(`Asset kind conflicts for ${external.key}; cannot change ${existing.entity_kind} to ${input.entity_kind}`);
1294
+ return this.reuseEntity(existing, input, payload);
1295
+ }
1296
+ }
1297
+ const entityId = input.entity_id ?? this.idFactory("entity");
1298
+ assertTrimmed(entityId, "entity_id");
1299
+ const entity = {
1300
+ entity_id: entityId,
1301
+ entity_kind: input.entity_kind,
1302
+ payload
1303
+ };
1304
+ entityToRow({
1305
+ ...entity.payload,
1306
+ entityId: createEntityId(entityId),
1307
+ entityKind: entity.entity_kind
1308
+ });
1309
+ this.record({
1310
+ kind: "create-entity",
1311
+ entity
1312
+ });
1313
+ return entityId;
1314
+ }
1315
+ reuseEntity(existing, input, payload) {
1316
+ if (input.entity_id !== void 0 && input.entity_id !== existing.entity_id) throw new Error(`Entity already exists; reuse ${existing.entity_id}`);
1317
+ for (const [key, value] of Object.entries(payload)) if (existing.payload[key] !== void 0 && !sameJson(existing.payload[key], value)) throw new Error(`Resource facts conflict for ${existing.entity_id}: ${key}`);
1318
+ const merged = {
1319
+ ...existing.payload,
1320
+ ...payload
1321
+ };
1322
+ if (!sameJson(existing.payload, merged)) this.updateEntity({
1323
+ entity_id: existing.entity_id,
1324
+ payload: merged
1325
+ });
1326
+ return existing.entity_id;
1327
+ }
1328
+ updateEntity(input) {
1329
+ assertTrimmed(input.entity_id, "entity_id");
1330
+ const payload = clone(input.payload);
1331
+ if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
1332
+ this.record({
1333
+ kind: "update-entity",
1334
+ entity_id: input.entity_id,
1335
+ payload
1336
+ });
1337
+ }
1338
+ deleteEntity(input) {
1339
+ assertTrimmed(input.entity_id, "entity_id");
1340
+ this.record({
1341
+ kind: "delete-entity",
1342
+ entity_id: input.entity_id
1343
+ });
1344
+ }
1345
+ ensureMedia(fact) {
1346
+ const checkpoint = this.commandCount;
1347
+ try {
1348
+ const imported = importMediaAsset(toDslRows(this.state), fact, this.idFactory);
1349
+ this.appendResourceRows(imported.rows);
1350
+ return { contentEntityId: imported.contentEntityId };
1351
+ } catch (error) {
1352
+ this.rollbackTo(checkpoint);
1353
+ throw error;
1354
+ }
1355
+ }
1356
+ appendResourceRows(rows) {
1357
+ for (const row of rows.entities) {
1358
+ const existing = this.state.entities.find((entity) => entity.entity_id === row.entityId);
1359
+ if (existing === void 0) this.createEntity({
1360
+ entity_id: row.entityId,
1361
+ entity_kind: row.entityKind,
1362
+ payload: row.payload
1363
+ });
1364
+ else if (!sameJson(existing.payload, row.payload)) this.updateEntity({
1365
+ entity_id: row.entityId,
1366
+ payload: clone(row.payload)
1367
+ });
1368
+ }
1369
+ for (const row of rows.relations) {
1370
+ if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
1371
+ if (row.relationKind !== "timeline-track") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
1372
+ this.link({
1373
+ relation_id: row.relationId,
1374
+ relation_kind: row.relationKind,
1375
+ endpoint_0_entity_id: row.endpoint0EntityId,
1376
+ endpoint_1_entity_id: row.endpoint1EntityId,
1377
+ metadata: clone(row.metadata),
1378
+ trace: clone(row.trace)
1379
+ });
1380
+ }
1381
+ }
1382
+ link(input) {
1383
+ if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
1384
+ const spec = builtInRelationSpecs.find((candidate) => candidate.kind === input.relation_kind);
1385
+ if (spec == null) throw new Error(`Unknown Relation kind "${String(input.relation_kind)}"`);
1386
+ const relation = this.relationFromInput(input, input.relation_kind);
1387
+ const [first, second] = this.refsFor(relation);
1388
+ new BiRelationIndex().link({
1389
+ relationId: createRelationId(relation.relation_id),
1390
+ spec,
1391
+ endpoints: [first, second],
1392
+ metadata: relation.metadata,
1393
+ trace: relation.trace
1394
+ });
1395
+ this.record({
1396
+ kind: "link-relation",
1397
+ relation
1398
+ });
1399
+ return relation.relation_id;
1400
+ }
1401
+ linkGenerated(input) {
1402
+ const relation = this.relationFromInput({
1403
+ ...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
1404
+ endpoint_0_entity_id: input.output_entity_id,
1405
+ endpoint_1_entity_id: input.input_entity_id,
1406
+ metadata: {},
1407
+ ...input.trace !== void 0 ? { trace: input.trace } : {}
1408
+ }, "generated");
1409
+ const [output, source] = this.refsFor(relation);
1410
+ new BiRelationIndex().linkGenerated({
1411
+ relationId: createRelationId(relation.relation_id),
1412
+ output,
1413
+ input: source,
1414
+ trace: relation.trace
1415
+ });
1416
+ this.record({
1417
+ kind: "link-relation",
1418
+ relation
1419
+ });
1420
+ return relation.relation_id;
1421
+ }
1422
+ linkClipAnchor(input) {
1423
+ const relation = this.relationFromInput({
1424
+ ...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
1425
+ endpoint_0_entity_id: input.child_clip_entity_id,
1426
+ endpoint_1_entity_id: input.host_clip_entity_id,
1427
+ metadata: {},
1428
+ ...input.trace !== void 0 ? { trace: input.trace } : {}
1429
+ }, "clip-anchor");
1430
+ const [child, host] = this.refsFor(relation);
1431
+ new BiRelationIndex().linkClipAnchor({
1432
+ relationId: createRelationId(relation.relation_id),
1433
+ child,
1434
+ host,
1435
+ trace: relation.trace
1436
+ });
1437
+ this.record({
1438
+ kind: "link-relation",
1439
+ relation
1440
+ });
1441
+ return relation.relation_id;
1442
+ }
1443
+ linkAudioScriptRender(input) {
1444
+ const relation = this.relationFromInput({
1445
+ ...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
1446
+ endpoint_0_entity_id: input.output_entity_id,
1447
+ endpoint_1_entity_id: input.script_entity_id,
1448
+ metadata: {},
1449
+ ...input.trace !== void 0 ? { trace: input.trace } : {}
1450
+ }, "audio-script-render");
1451
+ const [output, script] = this.refsFor(relation);
1452
+ new BiRelationIndex().linkAudioScriptRender({
1453
+ relationId: createRelationId(relation.relation_id),
1454
+ output,
1455
+ script,
1456
+ trace: relation.trace
1457
+ });
1458
+ this.record({
1459
+ kind: "link-relation",
1460
+ relation
1461
+ });
1462
+ return relation.relation_id;
1463
+ }
1464
+ unlinkRelation(input) {
1465
+ assertTrimmed(input.relation_id, "relation_id");
1466
+ this.record({
1467
+ kind: "unlink-relation",
1468
+ relation_id: input.relation_id
1469
+ });
1470
+ }
1471
+ relationFromInput(input, relationKind) {
1472
+ const relationId = input.relation_id ?? this.idFactory("relation");
1473
+ assertTrimmed(relationId, "relation_id");
1474
+ assertTrimmed(input.endpoint_0_entity_id, "endpoint_0_entity_id");
1475
+ assertTrimmed(input.endpoint_1_entity_id, "endpoint_1_entity_id");
1476
+ const metadata = clone(input.metadata ?? {});
1477
+ const trace = clone(input.trace ?? {});
1478
+ if (!isJsonObject(metadata) || !isJsonObject(trace)) throw new Error("Relation metadata and trace must contain only JSON values");
1479
+ return {
1480
+ relation_id: relationId,
1481
+ relation_kind: relationKind,
1482
+ endpoint_0_entity_id: input.endpoint_0_entity_id,
1483
+ endpoint_1_entity_id: input.endpoint_1_entity_id,
1484
+ metadata,
1485
+ trace
1486
+ };
1487
+ }
1488
+ refsFor(relation) {
1489
+ const first = this.state.entities.find((entity) => entity.entity_id === relation.endpoint_0_entity_id);
1490
+ const second = this.state.entities.find((entity) => entity.entity_id === relation.endpoint_1_entity_id);
1491
+ if (first == null || second == null) {
1492
+ const missing = [first == null ? relation.endpoint_0_entity_id : null, second == null ? relation.endpoint_1_entity_id : null].filter((value) => value != null).join(", ");
1493
+ throw new Error(`Relation references missing Entity id(s): ${missing}`);
1494
+ }
1495
+ return [createEntityRef(toDslEntity(first)), createEntityRef(toDslEntity(second))];
1496
+ }
1497
+ record(command) {
1498
+ this.apply(command, true);
1499
+ this.commands.push(clone(command));
1500
+ this.onCommand?.(clone(command));
1501
+ }
1502
+ apply(command, enforceIdentity) {
1503
+ switch (command.kind) {
1504
+ case "create-entity": {
1505
+ if (enforceIdentity && this.state.entities.some((entity) => entity.entity_id === command.entity.entity_id)) throw new Error(`Entity id "${command.entity.entity_id}" already exists`);
1506
+ const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
1507
+ if (enforceIdentity && original != null && original.entity_kind !== command.entity.entity_kind) throw new Error(`Entity id "${command.entity.entity_id}" was originally kind "${original.entity_kind}" and cannot be recreated as "${command.entity.entity_kind}"`);
1508
+ this.state.entities.push(clone(command.entity));
1509
+ return;
1510
+ }
1511
+ case "update-entity": {
1512
+ const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
1513
+ if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1514
+ const current = this.state.entities[index];
1515
+ if (current == null) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1516
+ this.state.entities[index] = {
1517
+ ...current,
1518
+ payload: clone(command.payload)
1519
+ };
1520
+ return;
1521
+ }
1522
+ case "delete-entity": {
1523
+ const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
1524
+ if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1525
+ const incidentRelationIds = this.state.relations.filter((relation) => relation.endpoint_0_entity_id === command.entity_id || relation.endpoint_1_entity_id === command.entity_id).map((relation) => relation.relation_id).sort();
1526
+ if (incidentRelationIds.length > 0) throw new Error(`Entity id "${command.entity_id}" still has incident Relation id(s): ${incidentRelationIds.join(", ")}`);
1527
+ this.state.entities.splice(index, 1);
1528
+ return;
1529
+ }
1530
+ case "link-relation":
1531
+ if (enforceIdentity && this.state.relations.some((relation) => relation.relation_id === command.relation.relation_id)) throw new Error(`Relation id "${command.relation.relation_id}" already exists`);
1532
+ if (enforceIdentity) {
1533
+ const original = this.original.relations.find((relation) => relation.relation_id === command.relation.relation_id);
1534
+ if (original != null && (original.relation_kind !== command.relation.relation_kind || original.endpoint_0_entity_id !== command.relation.endpoint_0_entity_id || original.endpoint_1_entity_id !== command.relation.endpoint_1_entity_id)) throw new Error(`Relation id "${command.relation.relation_id}" cannot change its kind or persisted endpoint positions`);
1535
+ }
1536
+ this.state.relations.push(clone(command.relation));
1537
+ return;
1538
+ case "unlink-relation": {
1539
+ const index = this.state.relations.findIndex((relation) => relation.relation_id === command.relation_id);
1540
+ if (index < 0) throw new Error(`Relation id "${command.relation_id}" does not exist`);
1541
+ this.state.relations.splice(index, 1);
1542
+ return;
1543
+ }
1544
+ }
1545
+ }
1546
+ };
1547
+ function isImportedMemotaAsset(payload, assetId) {
1548
+ const external = payload[ASSET_SOURCE_KEY];
1549
+ return external != null && !Array.isArray(external) && typeof external === "object" && (external.system === MEMOTA_SYSTEM || external.system === "memota-speech") && external.key === assetId;
1550
+ }
1551
+ function toDslEntity(entity) {
1552
+ return {
1553
+ ...clone(entity.payload),
1554
+ entityId: createEntityId(entity.entity_id),
1555
+ entityKind: entity.entity_kind
1556
+ };
1557
+ }
1558
+ function toDslRows(state) {
1559
+ return {
1560
+ entities: state.entities.map((entity) => ({
1561
+ entityId: createEntityId(entity.entity_id),
1562
+ entityKind: entity.entity_kind,
1563
+ payload: clone(entity.payload)
1564
+ })),
1565
+ relations: state.relations.map((relation) => ({
1566
+ relationId: createRelationId(relation.relation_id),
1567
+ relationKind: relation.relation_kind,
1568
+ endpoint0EntityId: createEntityId(relation.endpoint_0_entity_id),
1569
+ endpoint1EntityId: createEntityId(relation.endpoint_1_entity_id),
1570
+ metadata: clone(relation.metadata),
1571
+ trace: clone(relation.trace)
1572
+ }))
1573
+ };
1574
+ }
1575
+ function cloneSnapshot(state) {
1576
+ return clone(state);
1577
+ }
1578
+ function clone(value) {
1579
+ return structuredClone(value);
1580
+ }
1581
+ function sameJson(left, right) {
1582
+ if (left === right) return true;
1583
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
1584
+ if (!isJsonObject(left) || !isJsonObject(right)) return false;
1585
+ const keys = Object.keys(left);
1586
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
1587
+ }
1588
+ function assertTrimmed(value, label) {
1589
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
1590
+ }
1591
+ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left, right) => {
1592
+ if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("Sequence Marker points require finite numeric coordinates in the entity sandbox");
1593
+ return left - right;
1594
+ } };
1595
+ //#endregion
1596
+ //#region src/sandbox/script-session.ts
1597
+ const LOG_LINE_MAX = 2e3;
1598
+ const LOG_LINE_CAP = 1e3;
1599
+ const LOG_BYTE_CAP = 64 * 1024;
1600
+ const TRUNCATE_MARK = "[truncated]";
1601
+ const LOG_TRUNCATED = "[log truncated]";
1602
+ const ENTITY_CHECKPOINT_INDEX = Symbol("entityCheckpointIndex");
1603
+ /** Session core for one forked document; globals stay identity-stable across rollback. */
1604
+ var EditSandboxSession = class {
1605
+ original;
1606
+ idFactory;
1607
+ onEntry;
1608
+ onLog;
1609
+ onTruncate;
1610
+ entitySandbox;
1611
+ current;
1612
+ /** Adapter journal length already accounted for — new slices are real commits. */
1613
+ adapterJournalSeen = 0;
1614
+ entries = [];
1615
+ logs = [];
1616
+ logBytes = 0;
1617
+ logCapped = false;
1618
+ edit;
1619
+ timeline;
1620
+ entities;
1621
+ relations;
1622
+ console;
1623
+ checkpoint;
1624
+ rollbackTo;
1625
+ constructor(document, options) {
1626
+ this.original = structuredClone(document);
1627
+ this.idFactory = options?.idFactory;
1628
+ this.onEntry = options?.onEntry;
1629
+ this.onLog = options?.onLog;
1630
+ this.onTruncate = options?.onTruncate;
1631
+ this.entitySandbox = new EntitySandbox({
1632
+ state: options?.entityState,
1633
+ idFactory: options?.domainIdFactory ?? (() => {
1634
+ throw new Error("Entity id factory is unavailable in this sandbox host");
1635
+ }),
1636
+ onCommand: options?.onEntityCommand,
1637
+ onTruncate: options?.onEntityTruncate
1638
+ });
1639
+ this.current = this.boot(structuredClone(this.original));
1640
+ this.adapterJournalSeen = this.current.adapter.journal.length;
1641
+ this.edit = this.buildEditFacade();
1642
+ this.timeline = this.buildTimelineFacade();
1643
+ this.entities = this.entitySandbox.entities;
1644
+ this.relations = this.entitySandbox.relations;
1645
+ this.console = this.buildConsoleShim();
1646
+ this.checkpoint = () => {
1647
+ const checkpoint = { index: this.entries.length };
1648
+ Object.defineProperty(checkpoint, ENTITY_CHECKPOINT_INDEX, {
1649
+ value: this.entitySandbox.commandCount,
1650
+ enumerable: false
1651
+ });
1652
+ return checkpoint;
1653
+ };
1654
+ this.rollbackTo = (cp) => this.doRollbackTo(cp);
1655
+ }
1656
+ /** Assemble a ChangePlan from the self-maintained journal + current preview. */
1657
+ buildPlan(baseVersion) {
1658
+ const entityCommands = this.entitySandbox.getCommands();
1659
+ if (this.entries.length > 0 && entityCommands.length > 0) throw new Error("A sandbox plan cannot mix timeline and entity mutations; run and commit them as separate plans");
1660
+ const entityPlan = this.entitySandbox.buildPlan();
1661
+ const planKind = entityCommands.length > 0 ? "entities" : "timeline";
1662
+ return {
1663
+ plan_kind: planKind,
1664
+ doc_id: this.original.meta.draft_id ?? "",
1665
+ base_version: baseVersion,
1666
+ ops: this.entries.slice(),
1667
+ entity_base_revision: entityPlan.base_revision,
1668
+ entity_commands: entityPlan.commands,
1669
+ ...planKind === "entities" ? { entity_rows: entityPlan.rows } : {},
1670
+ ...planKind === "entities" ? {
1671
+ deleted_entity_ids: entityPlan.deleted_entity_ids,
1672
+ deleted_relation_ids: entityPlan.deleted_relation_ids
1673
+ } : {},
1674
+ preview: planKind === "entities" ? this.entitySandbox.renderPreview() : renderPreview(this.current.adapter.snapshot(), this.entries),
1675
+ logs: this.logs.slice()
1676
+ };
1677
+ }
1678
+ getEntries() {
1679
+ return this.entries;
1680
+ }
1681
+ getLogs() {
1682
+ return this.logs;
1683
+ }
1684
+ boot(document) {
1685
+ const sandbox = createEditSandbox(document, this.idFactory != null ? { idFactory: this.idFactory } : void 0);
1686
+ return {
1687
+ adapter: sandbox.adapter,
1688
+ editor: sandbox.editor
1689
+ };
1690
+ }
1691
+ doRollbackTo(cp) {
1692
+ if (cp.index > this.entries.length) throw new Error(`rollbackTo: checkpoint index ${cp.index} is past journal length ${this.entries.length}`);
1693
+ const prefix = this.entries.slice(0, cp.index);
1694
+ const next = this.boot(structuredClone(this.original));
1695
+ replayJournalSync(next.adapter, prefix);
1696
+ this.entries.length = 0;
1697
+ this.entries.push(...prefix);
1698
+ this.current = next;
1699
+ this.adapterJournalSeen = next.adapter.journal.length;
1700
+ this.onTruncate?.(prefix.length);
1701
+ const entityIndex = cp[ENTITY_CHECKPOINT_INDEX];
1702
+ if (entityIndex !== void 0) this.entitySandbox.rollbackTo(entityIndex);
1703
+ }
1704
+ captureNewEntries() {
1705
+ const journal = this.current.adapter.journal;
1706
+ if (journal.length <= this.adapterJournalSeen) return;
1707
+ const fresh = journal.slice(this.adapterJournalSeen);
1708
+ this.adapterJournalSeen = journal.length;
1709
+ for (const entry of fresh) {
1710
+ this.entries.push(entry);
1711
+ this.onEntry?.(entry);
1712
+ }
1713
+ }
1714
+ appendLog(line) {
1715
+ if (this.logCapped) return;
1716
+ if (this.logs.length >= LOG_LINE_CAP || this.logBytes >= LOG_BYTE_CAP) {
1717
+ this.logs.push(LOG_TRUNCATED);
1718
+ this.logCapped = true;
1719
+ this.onLog?.(LOG_TRUNCATED);
1720
+ return;
1721
+ }
1722
+ let out = line;
1723
+ if (out.length > LOG_LINE_MAX) out = `${out.slice(0, LOG_LINE_MAX - 11)}${TRUNCATE_MARK}`;
1724
+ this.logs.push(out);
1725
+ this.logBytes += out.length;
1726
+ this.onLog?.(out);
1727
+ }
1728
+ buildConsoleShim() {
1729
+ const write = (...args) => {
1730
+ this.appendLog(args.map(formatLogArg).join(" "));
1731
+ };
1732
+ return {
1733
+ log: write,
1734
+ info: write,
1735
+ warn: write,
1736
+ error: write
1737
+ };
1738
+ }
1739
+ buildEditFacade() {
1740
+ const wrap = (method) => async (input) => {
1741
+ await method(this.current.editor, input);
1742
+ this.captureNewEntries();
1743
+ };
1744
+ return {
1745
+ addSpeeches: wrap((e, i) => e.addSpeeches(i)),
1746
+ addVideoClips: async (input) => {
1747
+ const needsAppend = input.before_clip_id == null && input.after_clip_id == null && input.clips.some((clip) => clip.start_ms == null);
1748
+ const appendAt = this.timeline.snapshot().timeline?.duration_ms ?? 0;
1749
+ const normalized = needsAppend ? {
1750
+ ...input,
1751
+ clips: input.clips.map((clip) => clip.start_ms == null ? {
1752
+ ...clip,
1753
+ start_ms: appendAt
1754
+ } : clip)
1755
+ } : input;
1756
+ await this.current.editor.addVideoClips(normalized);
1757
+ this.captureNewEntries();
1758
+ },
1759
+ adjustBgmVolume: wrap((e, i) => e.adjustBgmVolume(i)),
1760
+ adjustSpeechVolume: wrap((e, i) => e.adjustSpeechVolume(i)),
1761
+ adjustVideoClipDuration: wrap((e, i) => e.adjustVideoClipDuration(i)),
1762
+ adjustVideoClipVolume: wrap((e, i) => e.adjustVideoClipVolume(i)),
1763
+ changeSpeechScript: wrap((e, i) => e.changeSpeechScript(i)),
1764
+ changeSpeechVoice: wrap((e, i) => e.changeSpeechVoice(i)),
1765
+ deleteBgm: wrap((e, i) => e.deleteBgm(i)),
1766
+ deleteSpeeches: wrap((e, i) => e.deleteSpeeches(i)),
1767
+ deleteVideoClips: wrap((e, i) => e.deleteVideoClips(i)),
1768
+ moveSpeeches: wrap((e, i) => e.moveSpeeches(i)),
1769
+ moveVideoClips: wrap((e, i) => e.moveVideoClips(i)),
1770
+ moveVideoClipsByAnchor: wrap((e, i) => e.moveVideoClipsByAnchor(i)),
1771
+ replaceVideoClipContent: wrap((e, i) => e.replaceVideoClipContent(i)),
1772
+ replaceVideoClipSequence: wrap((e, i) => e.replaceVideoClipSequence(i)),
1773
+ setBgm: wrap((e, i) => e.setBgm(i)),
1774
+ setCaptionStyle: wrap((e, i) => e.setCaptionStyle(i)),
1775
+ setCaptionVisibility: wrap((e, i) => e.setCaptionVisibility(i)),
1776
+ setVideoClipSpeedShift: wrap((e, i) => e.setVideoClipSpeedShift(i))
1777
+ };
1778
+ }
1779
+ buildTimelineFacade() {
1780
+ return {
1781
+ snapshot: () => fromVideoDocument(this.current.adapter.snapshot()),
1782
+ clipsInRange: (startMs, endMs) => this.clipsInRange(startMs, endMs),
1783
+ part: (id) => this.part(id)
1784
+ };
1785
+ }
1786
+ clipsInRange(startMs, endMs) {
1787
+ const document = this.current.adapter.snapshot();
1788
+ const solved = solveVideoDocument(document);
1789
+ const library = document.part_library ?? {};
1790
+ const main = document.tracks?.find((track) => track.parts_kind === "video_clip");
1791
+ const out = [];
1792
+ for (const item of main?.items ?? []) {
1793
+ const id = item.part_id;
1794
+ if (id == null) continue;
1795
+ const clip = library[id]?.video_clip;
1796
+ if (clip == null) continue;
1797
+ const start = solved.absByPartId.get(id) ?? 0;
1798
+ const duration = effectiveVideoClipDurationMs(clip);
1799
+ const end = start + duration;
1800
+ const mid = start + duration / 2;
1801
+ if (!(mid >= startMs && mid < endMs)) continue;
1802
+ out.push({
1803
+ id,
1804
+ start_ms: start,
1805
+ end_ms: end,
1806
+ duration_ms: duration,
1807
+ speed_shift: clip.speed_shift,
1808
+ volume: clip.volume,
1809
+ media_id: clip.origin_media_id
1810
+ });
1811
+ }
1812
+ return out;
1813
+ }
1814
+ part(id) {
1815
+ const document = this.current.adapter.snapshot();
1816
+ const part = (document.part_library ?? {})[id];
1817
+ if (part == null) return null;
1818
+ let lane = "main";
1819
+ let kind = "video_clip";
1820
+ for (const track of document.tracks ?? []) {
1821
+ if (!(track.items ?? []).some((item) => item.part_id === id)) continue;
1822
+ const partsKind = track.parts_kind ?? "video_clip";
1823
+ kind = partsKind;
1824
+ lane = partsKind === "video_clip" ? "main" : partsKind;
1825
+ break;
1826
+ }
1827
+ const solved = solveVideoDocument(document);
1828
+ const start = solved.absByPartId.get(id) ?? 0;
1829
+ let duration = 0;
1830
+ if (part.video_clip != null) duration = effectiveVideoClipDurationMs(part.video_clip);
1831
+ else if (part.speech != null) duration = part.speech.media_duration_ms ?? 0;
1832
+ else if (part.caption != null) duration = part.caption.initial_duration_ms ?? 0;
1833
+ else if (part.bgm != null) duration = solved.durationMs;
1834
+ return {
1835
+ id,
1836
+ kind,
1837
+ lane,
1838
+ start_ms: start,
1839
+ end_ms: start + duration,
1840
+ duration_ms: duration,
1841
+ part
1842
+ };
1843
+ }
1844
+ };
1845
+ function formatLogArg(value) {
1846
+ if (typeof value === "string") return value;
1847
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return String(value);
1848
+ try {
1849
+ return JSON.stringify(value);
1850
+ } catch {
1851
+ return "[unstringifiable]";
1852
+ }
1853
+ }
1854
+ /**
1855
+ * Synchronous journal replay for rollback. Editor methods are `async` only for
1856
+ * interface uniformity — their bodies complete before the Promise is returned,
1857
+ * so voiding the call applies mutations in-order without yielding.
1858
+ */
1859
+ function replayJournalSync(adapter, journal) {
1860
+ const queue = [];
1861
+ const idFactory = (_prefix) => {
1862
+ const id = queue.shift();
1863
+ if (id == null) throw new Error("unrecorded id");
1864
+ return id;
1865
+ };
1866
+ const editor = new SemanticEditor(adapter, new SchemaValidator(), idFactory);
1867
+ for (const entry of journal) {
1868
+ queue.push(...entry.generated_ids ?? []);
1869
+ const payload = entry.payload;
1870
+ switch (entry.kind) {
1871
+ case "MoveVideoClips":
1872
+ editor.moveVideoClips(payload);
1873
+ break;
1874
+ case "MoveVideoClipsByAnchor":
1875
+ editor.moveVideoClipsByAnchor(payload);
1876
+ break;
1877
+ case "DeleteVideoClips":
1878
+ editor.deleteVideoClips(payload);
1879
+ break;
1880
+ case "AddVideoClips":
1881
+ editor.addVideoClips(payload);
1882
+ break;
1883
+ case "AdjustVideoClipVolume":
1884
+ editor.adjustVideoClipVolume(payload);
1885
+ break;
1886
+ case "SetVideoClipSpeedShift":
1887
+ editor.setVideoClipSpeedShift(payload);
1888
+ break;
1889
+ case "ReplaceVideoClipContent":
1890
+ editor.replaceVideoClipContent(payload);
1891
+ break;
1892
+ case "ReplaceVideoClipSequence":
1893
+ editor.replaceVideoClipSequence(payload);
1894
+ break;
1895
+ case "AdjustVideoClipDuration":
1896
+ editor.adjustVideoClipDuration(payload);
1897
+ break;
1898
+ case "AddSpeeches":
1899
+ editor.addSpeeches(payload);
1900
+ break;
1901
+ case "DeleteSpeeches":
1902
+ editor.deleteSpeeches(payload);
1903
+ break;
1904
+ case "MoveSpeeches":
1905
+ editor.moveSpeeches(payload);
1906
+ break;
1907
+ case "ChangeSpeechScript":
1908
+ editor.changeSpeechScript(payload);
1909
+ break;
1910
+ case "ChangeSpeechVoice":
1911
+ editor.changeSpeechVoice(payload);
1912
+ break;
1913
+ case "AdjustSpeechVolume":
1914
+ editor.adjustSpeechVolume(payload);
1915
+ break;
1916
+ case "SetCaptionVisibility":
1917
+ editor.setCaptionVisibility(payload);
1918
+ break;
1919
+ case "SetCaptionStyle":
1920
+ editor.setCaptionStyle(payload);
1921
+ break;
1922
+ case "SetBgm":
1923
+ editor.setBgm(payload);
1924
+ break;
1925
+ case "DeleteBgm":
1926
+ editor.deleteBgm(payload);
1927
+ break;
1928
+ case "AdjustBgmVolume":
1929
+ editor.adjustBgmVolume(payload);
1930
+ break;
1931
+ default: {
1932
+ const _exhaustive = entry.kind;
1933
+ throw new Error(`replayJournalSync: unsupported kind ${String(_exhaustive)}`);
1934
+ }
1935
+ }
1936
+ if (queue.length > 0) throw new Error("unconsumed ids");
1937
+ }
1938
+ }
1939
+ //#endregion
1940
+ export { createRelationId as a, renderPreview as c, createEntityId as i, renderCompactProjection as l, EntitySandbox as n, isMediaAssetVariantKind as o, toDslRows as r, collectAffectedPartIds as s, EditSandboxSession as t };
1941
+
1942
+ //# sourceMappingURL=script-session-B4fLNe-p.mjs.map