@mengine/medeo-tool 1.2.1-alpha.7 → 1.2.1-alpha.9

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.
@@ -196,11 +196,15 @@ function validateEntityRelationSet(entityRefs, index, options) {
196
196
  }));
197
197
  issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
198
198
  }
199
- if (current.entityKind === "clip") issues.push(...validateClipAdmission(entity, index));
199
+ if (current.entityKind === "clip") {
200
+ issues.push(...validateClipAdmission(entity, index));
201
+ issues.push(...validateClipPlacement(entity, index));
202
+ }
200
203
  if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
201
204
  if (current.entityKind === "caption") issues.push(...validateCaptionAsset(entity, index));
202
205
  issues.push(...validateSequenceComposition(entity));
203
206
  }
207
+ issues.push(...validateClipAnchorCycles(entities, index));
204
208
  return issues;
205
209
  }
206
210
  function validateMarkerUse(marker, index) {
@@ -269,13 +273,84 @@ function validateAXVideoAdmission(axVideo, index) {
269
273
  }];
270
274
  }
271
275
  function validateCaptionAsset(caption, index) {
272
- if (ofKind([...index.relationsOf(caption)], "physical-asset").some((relation) => relation.other(caption)?.deref()?.current().entityKind === "asset")) return [];
276
+ const assets = ofKind([...index.relationsOf(caption)], "physical-asset").map((relation) => relation.other(caption)?.deref()?.current()).filter((entity) => entity?.entityKind === "asset");
277
+ if (assets.length > 0) {
278
+ const captionText = caption.current().text;
279
+ if (!assets.some((asset) => {
280
+ const inline = asset.inline;
281
+ return isRecord(inline) && inline.mediaType === "text/plain" && inline.text !== captionText;
282
+ })) return [];
283
+ return [{
284
+ code: "caption_inline_asset_mismatch",
285
+ entityId: caption.entityId,
286
+ message: "Caption text must match the text/plain inline Physical Asset"
287
+ }];
288
+ }
273
289
  return [{
274
290
  code: "caption_asset_required",
275
291
  entityId: caption.entityId,
276
292
  message: "Caption must have a Physical Asset Relation"
277
293
  }];
278
294
  }
295
+ function validateClipPlacement(clip, index) {
296
+ const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
297
+ if (marker?.current().entityKind !== "sequence-marker") return [];
298
+ const markerValue = marker.current();
299
+ const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
300
+ const hasOrder = clip.current().order !== void 0;
301
+ const hasTarget = markerValue.targetRange !== void 0;
302
+ const hasAnchorOffset = markerValue.anchorOffset !== void 0;
303
+ const issues = [];
304
+ if (anchors.length > 1) issues.push({
305
+ code: "clip_anchor_cardinality",
306
+ entityId: clip.entityId,
307
+ message: "A Clip may follow at most one host Clip"
308
+ });
309
+ const hasAnchor = anchors.length === 1;
310
+ if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
311
+ code: "clip_placement_invalid",
312
+ entityId: clip.entityId,
313
+ message: "Clip placement must be exactly one of Clip.order, Marker.targetRange, or clip-anchor with Marker.anchorOffset"
314
+ });
315
+ if (markerValue.durationPolicy === "timeline") {
316
+ const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
317
+ const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
318
+ const trackRole = track?.entityKind === "track" ? track.role : void 0;
319
+ if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder) issues.push({
320
+ code: "timeline_duration_policy_invalid",
321
+ entityId: marker.entityId,
322
+ message: "Marker durationPolicy \"timeline\" is only valid for ordered Audio Clips on the bgm Track"
323
+ });
324
+ }
325
+ return issues;
326
+ }
327
+ function validateClipAnchorCycles(entities, index) {
328
+ const hostByChild = /* @__PURE__ */ new Map();
329
+ for (const entity of entities) {
330
+ if (entity.current().entityKind !== "clip") continue;
331
+ for (const relation of ofKind([...index.relationsOf(entity)], "clip-anchor")) {
332
+ if (relation.endpoints[0].deref() !== entity) continue;
333
+ const host = relation.endpoints[1].deref();
334
+ if (host != null) hostByChild.set(entity.entityId, host.entityId);
335
+ }
336
+ }
337
+ const issues = [];
338
+ for (const child of hostByChild.keys()) {
339
+ const seen = /* @__PURE__ */ new Set();
340
+ let current = child;
341
+ while (current !== void 0 && !seen.has(current)) {
342
+ seen.add(current);
343
+ current = hostByChild.get(current);
344
+ }
345
+ if (current === void 0) continue;
346
+ issues.push({
347
+ code: "clip_anchor_cycle",
348
+ entityId: child,
349
+ message: "clip-anchor Relations must form an acyclic dependency graph"
350
+ });
351
+ }
352
+ return issues;
353
+ }
279
354
  function validateMarkerRanges(marker, compare) {
280
355
  const current = marker.current();
281
356
  const issues = [];
@@ -330,7 +405,7 @@ function validateEntity(entity, entityIds = /* @__PURE__ */ new Set()) {
330
405
  entityId: current.entityId,
331
406
  message: `Entity "${current.entityKind}" ${problem}`
332
407
  });
333
- const peerIdPaths = [...collectPeerEntityIdPaths(current, current.entityKind), ...collectPeerEntityValuePaths(current, current.entityId, entityIds)];
408
+ const peerIdPaths = [...collectPeerEntityIdPaths(current, current.entityKind), ...collectPeerEntityValuePaths(current, current.entityKind, current.entityId, entityIds)];
334
409
  const uniquePeerIdPaths = [...new Set(peerIdPaths)].sort();
335
410
  if (uniquePeerIdPaths.length > 0) issues.push({
336
411
  code: "peer_entity_id_field",
@@ -347,12 +422,15 @@ function validateKnownEntityPayload(entity) {
347
422
  case "track":
348
423
  validateOptionalField(value, "hidden", "boolean", problems);
349
424
  validateOptionalField(value, "role", "string", problems);
425
+ validateOptionalFiniteNumber(value, "order", problems);
350
426
  break;
351
427
  case "video":
352
428
  case "audio":
353
429
  case "voice":
354
430
  case "caption":
355
431
  validateSequencePayload(value, "bounded", "native", problems);
432
+ if (entity.entityKind === "voice") validateVoicePayload(value, problems);
433
+ if (entity.entityKind === "caption") validateCaptionPayload(value, problems);
356
434
  break;
357
435
  case "image":
358
436
  validateSequencePayload(value, "unbounded", "constant", problems);
@@ -368,9 +446,15 @@ function validateKnownEntityPayload(entity) {
368
446
  validateScriptPayload(value, problems);
369
447
  break;
370
448
  case "timeline":
449
+ case "viewport": break;
371
450
  case "clip":
451
+ validateOptionalFiniteNumber(value, "order", problems);
452
+ validateOptionalFiniteNumber(value, "volume", problems);
453
+ if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
454
+ break;
372
455
  case "asset":
373
- case "viewport": break;
456
+ validateAssetPayload(value, problems);
457
+ break;
374
458
  default: break;
375
459
  }
376
460
  return problems;
@@ -395,6 +479,63 @@ function validateMarkerPayload(value, problems) {
395
479
  else if (duration.mode === "fixed") {
396
480
  if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
397
481
  } else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
482
+ if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
483
+ if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
484
+ }
485
+ function validateAssetPayload(value, problems) {
486
+ const external = value.external;
487
+ if (external !== void 0) if (!isRecord(external)) problems.push("external must be an object when present");
488
+ else {
489
+ if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
490
+ if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
491
+ }
492
+ if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
493
+ const inline = value.inline;
494
+ if (inline !== void 0) if (!isRecord(inline)) problems.push("inline must be an object when present");
495
+ else {
496
+ if (inline.mediaType !== "text/plain") problems.push("inline.mediaType must be \"text/plain\"");
497
+ if (typeof inline.text !== "string") problems.push("inline.text must be a string");
498
+ }
499
+ if (external !== void 0 && inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
500
+ }
501
+ function validateVoicePayload(value, problems) {
502
+ const voice = value.voice;
503
+ if (voice === void 0) return;
504
+ if (!isRecord(voice)) {
505
+ problems.push("voice must be an object when present");
506
+ return;
507
+ }
508
+ if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
509
+ if (typeof voice.key !== "string" || voice.key.trim() === "") problems.push("voice.key must be a non-empty string");
510
+ if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
511
+ }
512
+ function validateCaptionPayload(value, problems) {
513
+ if (value.text !== void 0 && typeof value.text !== "string") problems.push("text must be a string when present");
514
+ const style = value.style;
515
+ if (style === void 0) return;
516
+ if (!isRecord(style)) {
517
+ problems.push("style must be an object when present");
518
+ return;
519
+ }
520
+ const font = style.font;
521
+ if (font !== void 0) if (!isRecord(font)) problems.push("style.font must be an object when present");
522
+ else {
523
+ if (font.system !== "font-library") problems.push("style.font.system must be \"font-library\"");
524
+ if (typeof font.key !== "string" || font.key.trim() === "") problems.push("style.font.key must be a non-empty string");
525
+ }
526
+ for (const key of [
527
+ "fontSize",
528
+ "fontWeight",
529
+ "entranceAnimationDurationMs",
530
+ "strokeWidth",
531
+ "positionX",
532
+ "positionY"
533
+ ]) validateOptionalFiniteNumber(style, key, problems, `style.${key}`);
534
+ for (const key of [
535
+ "fontColor",
536
+ "entranceAnimation",
537
+ "strokeColor"
538
+ ]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
398
539
  }
399
540
  function validateRange(value, path, required, problems) {
400
541
  if (!isRecord(value)) {
@@ -422,6 +563,9 @@ function validateScriptPayload(value, problems) {
422
563
  function validateOptionalField(value, key, expectedType, problems) {
423
564
  if (value[key] !== void 0 && typeof value[key] !== expectedType) problems.push(`${key} must be a ${expectedType} when present`);
424
565
  }
566
+ function validateOptionalFiniteNumber(value, key, problems, label = key) {
567
+ if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isFinite(value[key]))) problems.push(`${label} must be a finite number when present`);
568
+ }
425
569
  function collectPeerEntityIdPaths(value, entityKind) {
426
570
  const paths = [];
427
571
  visitPeerEntityIdPaths(value, entityKind, "", /* @__PURE__ */ new Set(), paths);
@@ -439,25 +583,42 @@ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths)
439
583
  }
440
584
  ancestors.delete(value);
441
585
  }
442
- function collectPeerEntityValuePaths(value, ownEntityId, entityIds) {
586
+ function collectPeerEntityValuePaths(value, entityKind, ownEntityId, entityIds) {
443
587
  const paths = [];
444
- visitPeerEntityValues(value, ownEntityId, entityIds, "", /* @__PURE__ */ new Set(), paths);
588
+ visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, "", /* @__PURE__ */ new Set(), paths);
445
589
  return paths;
446
590
  }
447
- function visitPeerEntityValues(value, ownEntityId, entityIds, path, ancestors, paths) {
591
+ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path, ancestors, paths) {
448
592
  if (typeof value === "string") {
449
- if (value !== ownEntityId && entityIds.has(value)) paths.push(path);
593
+ if (value !== ownEntityId && entityIds.has(value) && !isOwnedLocalEntityValuePath(entityKind, path)) paths.push(path);
450
594
  return;
451
595
  }
452
596
  if (typeof value !== "object" || value == null || ancestors.has(value)) return;
453
597
  ancestors.add(value);
454
- if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
598
+ if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
455
599
  else for (const [key, child] of Object.entries(value)) {
456
600
  if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
457
- visitPeerEntityValues(child, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
601
+ visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
458
602
  }
459
603
  ancestors.delete(value);
460
604
  }
605
+ function isOwnedLocalEntityValuePath(entityKind, path) {
606
+ if (entityKind === "track" && path === "role") return true;
607
+ if ([
608
+ "video",
609
+ "audio",
610
+ "voice",
611
+ "image",
612
+ "caption",
613
+ "axvideo"
614
+ ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
615
+ if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
616
+ if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey|inline\.(?:mediaType|text))$/.test(path)) return true;
617
+ if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
618
+ if (entityKind === "caption" && (path === "text" || path.startsWith("style."))) return true;
619
+ if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
620
+ return false;
621
+ }
461
622
  function isOwnedLocalIdPath(entityKind, path) {
462
623
  if (path === "lifecycle.actorId") return true;
463
624
  if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
@@ -516,6 +677,7 @@ function ofKind(relations, kind) {
516
677
  }
517
678
  //#endregion
518
679
  //#region ../medeo-dsl/src/json-values.ts
680
+ const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
519
681
  function isJsonObject(value) {
520
682
  return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
521
683
  }
@@ -523,14 +685,25 @@ function isJsonValue(value, ancestors) {
523
685
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
524
686
  if (typeof value === "number") return Number.isFinite(value);
525
687
  if (typeof value !== "object") return false;
526
- const prototype = Object.getPrototypeOf(value);
527
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
688
+ if (!Array.isArray(value) && !isPlainObject(value)) return false;
528
689
  if (ancestors.has(value)) return false;
529
690
  ancestors.add(value);
530
691
  const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
531
692
  ancestors.delete(value);
532
693
  return valid;
533
694
  }
695
+ /** Recognize an ordinary object from any VM realm without admitting class instances. */
696
+ function isPlainObject(value) {
697
+ try {
698
+ const prototype = Object.getPrototypeOf(value);
699
+ if (prototype === null) return true;
700
+ if (Object.getPrototypeOf(prototype) !== null) return false;
701
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
702
+ return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
703
+ } catch {
704
+ return false;
705
+ }
706
+ }
534
707
  //#endregion
535
708
  //#region ../medeo-dsl/src/relation-specs.ts
536
709
  const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
@@ -561,6 +734,21 @@ const captionAlignmentRelationSpec = Object.freeze({
561
734
  validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
562
735
  validateMetadata: isCaptionAlignmentMetadata
563
736
  });
737
+ /** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
738
+ const clipAnchorRelationSpec = Object.freeze({
739
+ kind: "clip-anchor",
740
+ validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
741
+ validateMetadata: isEmptyMetadata
742
+ });
743
+ /** `audio-script-render(output, script)` means endpoint 0 was rendered from endpoint 1. */
744
+ const audioScriptRenderRelationSpec = Object.freeze({
745
+ kind: "audio-script-render",
746
+ validateEndpoints: (endpoints) => {
747
+ const outputKind = endpoints[0].current().entityKind;
748
+ return (outputKind === "audio" || outputKind === "voice") && endpoints[1].current().entityKind === "audio-script";
749
+ },
750
+ validateMetadata: isEmptyMetadata
751
+ });
564
752
  /** Built-in kinds are reserved; callers may add specs only under new names. */
565
753
  const builtInRelationSpecs = Object.freeze([
566
754
  timelineTrackRelationSpec,
@@ -573,7 +761,9 @@ const builtInRelationSpecs = Object.freeze([
573
761
  generatedRelationSpec,
574
762
  phoneticScriptProvenanceRelationSpec,
575
763
  captionProvenanceRelationSpec,
576
- captionAlignmentRelationSpec
764
+ captionAlignmentRelationSpec,
765
+ clipAnchorRelationSpec,
766
+ audioScriptRenderRelationSpec
577
767
  ]);
578
768
  function emptySpec(kind, a, b) {
579
769
  return metadataSpec(kind, a, b, isEmptyMetadata);
@@ -670,6 +860,7 @@ var BiRelationIndex = class {
670
860
  byRelationId = /* @__PURE__ */ new Map();
671
861
  link(input) {
672
862
  if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
863
+ 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`);
673
864
  return this.linkValidated(input);
674
865
  }
675
866
  /** Author `generated(output, input)` without exposing positional arguments. */
@@ -682,6 +873,26 @@ var BiRelationIndex = class {
682
873
  trace: input.trace
683
874
  });
684
875
  }
876
+ /** Author `clip-anchor(child, host)` without exposing positional arguments. */
877
+ linkClipAnchor(input) {
878
+ return this.linkValidated({
879
+ relationId: input.relationId,
880
+ spec: clipAnchorRelationSpec,
881
+ endpoints: [input.child, input.host],
882
+ metadata: {},
883
+ trace: input.trace
884
+ });
885
+ }
886
+ /** Author `audio-script-render(output, script)` without exposing positional arguments. */
887
+ linkAudioScriptRender(input) {
888
+ return this.linkValidated({
889
+ relationId: input.relationId,
890
+ spec: audioScriptRenderRelationSpec,
891
+ endpoints: [input.output, input.script],
892
+ metadata: {},
893
+ trace: input.trace
894
+ });
895
+ }
685
896
  /**
686
897
  * Rehydrate a persisted row after resolving its spec and endpoint refs.
687
898
  *
@@ -955,17 +1166,36 @@ var EntitySandbox = class {
955
1166
  }
956
1167
  buildPlan() {
957
1168
  decodeEntityRelationRows(toDslRows(this.state), numericMarkerComparators);
1169
+ const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
1170
+ const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
958
1171
  return {
959
1172
  base_revision: this.original.revision,
960
1173
  commands: this.commands.slice(),
961
- rows: cloneSnapshot(this.state)
1174
+ rows: cloneSnapshot(this.state),
1175
+ deleted_entity_ids: this.original.entities.map((entity) => entity.entity_id).filter((entityId) => !currentEntityIds.has(entityId)).sort(),
1176
+ deleted_relation_ids: this.original.relations.map((relation) => relation.relation_id).filter((relationId) => !currentRelationIds.has(relationId)).sort()
962
1177
  };
963
1178
  }
964
1179
  renderPreview() {
965
1180
  const lines = [`Entity plan: base_revision=${this.original.revision} commands=${this.commands.length} entities=${this.state.entities.length} relations=${this.state.relations.length}`];
966
- for (const command of this.commands) if (command.kind === "create-entity") lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
967
- else 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})`);
968
- 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}`);
1181
+ for (const command of this.commands) switch (command.kind) {
1182
+ case "create-entity":
1183
+ lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
1184
+ break;
1185
+ case "update-entity":
1186
+ lines.push(`~ entity ${command.entity_id} payload`);
1187
+ break;
1188
+ case "delete-entity":
1189
+ lines.push(`- entity ${command.entity_id}`);
1190
+ break;
1191
+ case "unlink-relation":
1192
+ lines.push(`- relation ${command.relation_id}`);
1193
+ break;
1194
+ case "link-relation":
1195
+ 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})`);
1196
+ 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}`);
1197
+ break;
1198
+ }
969
1199
  return lines.join("\n");
970
1200
  }
971
1201
  buildEntityFacade() {
@@ -980,6 +1210,8 @@ var EntitySandbox = class {
980
1210
  return clone(this.state.entities.filter((entity) => entity.entity_kind === "asset" && isImportedMemotaAsset(entity.payload, assetId)));
981
1211
  },
982
1212
  create: (input) => this.createEntity(input),
1213
+ update: (input) => this.updateEntity(input),
1214
+ delete: (input) => this.deleteEntity(input),
983
1215
  importAsset: (input) => this.importAsset(input)
984
1216
  };
985
1217
  }
@@ -992,7 +1224,10 @@ var EntitySandbox = class {
992
1224
  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)));
993
1225
  },
994
1226
  link: (input) => this.link(input),
995
- linkGenerated: (input) => this.linkGenerated(input)
1227
+ linkGenerated: (input) => this.linkGenerated(input),
1228
+ linkClipAnchor: (input) => this.linkClipAnchor(input),
1229
+ linkAudioScriptRender: (input) => this.linkAudioScriptRender(input),
1230
+ unlink: (input) => this.unlinkRelation(input)
996
1231
  };
997
1232
  }
998
1233
  createEntity(input) {
@@ -1017,6 +1252,23 @@ var EntitySandbox = class {
1017
1252
  });
1018
1253
  return entityId;
1019
1254
  }
1255
+ updateEntity(input) {
1256
+ assertTrimmed(input.entity_id, "entity_id");
1257
+ const payload = clone(input.payload);
1258
+ if (!isJsonObject(payload)) throw new Error("Entity payload must contain only JSON values");
1259
+ this.record({
1260
+ kind: "update-entity",
1261
+ entity_id: input.entity_id,
1262
+ payload
1263
+ });
1264
+ }
1265
+ deleteEntity(input) {
1266
+ assertTrimmed(input.entity_id, "entity_id");
1267
+ this.record({
1268
+ kind: "delete-entity",
1269
+ entity_id: input.entity_id
1270
+ });
1271
+ }
1020
1272
  importAsset(input) {
1021
1273
  assertTrimmed(input.asset_id, "asset_id");
1022
1274
  const payload = input.payload === void 0 ? {} : clone(input.payload);
@@ -1073,6 +1325,55 @@ var EntitySandbox = class {
1073
1325
  });
1074
1326
  return relation.relation_id;
1075
1327
  }
1328
+ linkClipAnchor(input) {
1329
+ const relation = this.relationFromInput({
1330
+ ...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
1331
+ endpoint_0_entity_id: input.child_clip_entity_id,
1332
+ endpoint_1_entity_id: input.host_clip_entity_id,
1333
+ metadata: {},
1334
+ ...input.trace !== void 0 ? { trace: input.trace } : {}
1335
+ }, "clip-anchor");
1336
+ const [child, host] = this.refsFor(relation);
1337
+ new BiRelationIndex().linkClipAnchor({
1338
+ relationId: createRelationId(relation.relation_id),
1339
+ child,
1340
+ host,
1341
+ trace: relation.trace
1342
+ });
1343
+ this.record({
1344
+ kind: "link-relation",
1345
+ relation
1346
+ });
1347
+ return relation.relation_id;
1348
+ }
1349
+ linkAudioScriptRender(input) {
1350
+ const relation = this.relationFromInput({
1351
+ ...input.relation_id !== void 0 ? { relation_id: input.relation_id } : {},
1352
+ endpoint_0_entity_id: input.output_entity_id,
1353
+ endpoint_1_entity_id: input.script_entity_id,
1354
+ metadata: {},
1355
+ ...input.trace !== void 0 ? { trace: input.trace } : {}
1356
+ }, "audio-script-render");
1357
+ const [output, script] = this.refsFor(relation);
1358
+ new BiRelationIndex().linkAudioScriptRender({
1359
+ relationId: createRelationId(relation.relation_id),
1360
+ output,
1361
+ script,
1362
+ trace: relation.trace
1363
+ });
1364
+ this.record({
1365
+ kind: "link-relation",
1366
+ relation
1367
+ });
1368
+ return relation.relation_id;
1369
+ }
1370
+ unlinkRelation(input) {
1371
+ assertTrimmed(input.relation_id, "relation_id");
1372
+ this.record({
1373
+ kind: "unlink-relation",
1374
+ relation_id: input.relation_id
1375
+ });
1376
+ }
1076
1377
  relationFromInput(input, relationKind) {
1077
1378
  const relationId = input.relation_id ?? this.idFactory("relation");
1078
1379
  assertTrimmed(relationId, "relation_id");
@@ -1105,13 +1406,48 @@ var EntitySandbox = class {
1105
1406
  this.onCommand?.(clone(command));
1106
1407
  }
1107
1408
  apply(command, enforceIdentity) {
1108
- if (command.kind === "create-entity") {
1109
- 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`);
1110
- this.state.entities.push(clone(command.entity));
1111
- return;
1409
+ switch (command.kind) {
1410
+ case "create-entity": {
1411
+ 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`);
1412
+ const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
1413
+ 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}"`);
1414
+ this.state.entities.push(clone(command.entity));
1415
+ return;
1416
+ }
1417
+ case "update-entity": {
1418
+ const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
1419
+ if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1420
+ const current = this.state.entities[index];
1421
+ if (current == null) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1422
+ this.state.entities[index] = {
1423
+ ...current,
1424
+ payload: clone(command.payload)
1425
+ };
1426
+ return;
1427
+ }
1428
+ case "delete-entity": {
1429
+ const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
1430
+ if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1431
+ 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();
1432
+ if (incidentRelationIds.length > 0) throw new Error(`Entity id "${command.entity_id}" still has incident Relation id(s): ${incidentRelationIds.join(", ")}`);
1433
+ this.state.entities.splice(index, 1);
1434
+ return;
1435
+ }
1436
+ case "link-relation":
1437
+ 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`);
1438
+ if (enforceIdentity) {
1439
+ const original = this.original.relations.find((relation) => relation.relation_id === command.relation.relation_id);
1440
+ 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`);
1441
+ }
1442
+ this.state.relations.push(clone(command.relation));
1443
+ return;
1444
+ case "unlink-relation": {
1445
+ const index = this.state.relations.findIndex((relation) => relation.relation_id === command.relation_id);
1446
+ if (index < 0) throw new Error(`Relation id "${command.relation_id}" does not exist`);
1447
+ this.state.relations.splice(index, 1);
1448
+ return;
1449
+ }
1112
1450
  }
1113
- 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`);
1114
- this.state.relations.push(clone(command.relation));
1115
1451
  }
1116
1452
  };
1117
1453
  function isImportedMemotaAsset(payload, assetId) {
@@ -1230,6 +1566,10 @@ var EditSandboxSession = class {
1230
1566
  entity_base_revision: entityPlan.base_revision,
1231
1567
  entity_commands: entityPlan.commands,
1232
1568
  ...planKind === "entities" ? { entity_rows: entityPlan.rows } : {},
1569
+ ...planKind === "entities" ? {
1570
+ deleted_entity_ids: entityPlan.deleted_entity_ids,
1571
+ deleted_relation_ids: entityPlan.deleted_relation_ids
1572
+ } : {},
1233
1573
  preview: planKind === "entities" ? this.entitySandbox.renderPreview() : renderPreview(this.current.adapter.snapshot(), this.entries),
1234
1574
  logs: this.logs.slice()
1235
1575
  };
@@ -1496,6 +1836,6 @@ function replayJournalSync(adapter, journal) {
1496
1836
  }
1497
1837
  }
1498
1838
  //#endregion
1499
- export { renderCompactProjection as i, collectAffectedPartIds as n, renderPreview as r, EditSandboxSession as t };
1839
+ export { collectAffectedPartIds as a, createRelationId as i, EntitySandbox as n, renderPreview as o, createEntityId as r, renderCompactProjection as s, EditSandboxSession as t };
1500
1840
 
1501
- //# sourceMappingURL=script-session-BF44uKv_.mjs.map
1841
+ //# sourceMappingURL=script-session-CHyIUBkO.mjs.map