@mengine/medeo-tool 1.3.1-alpha.8 → 1.4.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,5 @@
1
1
  //#region src/sandbox/generated/entity-edit-sandbox-api.d.ts
2
2
  /** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */
3
- interface AudioMediaAssetFact {
4
- readonly assetId: string;
5
- readonly kind: 'audio';
6
- readonly durationMs: number;
7
- readonly storageKey: string;
8
- }
9
3
  interface BoundedDerivedSequencePayload extends JsonObject {
10
4
  extent: {
11
5
  kind: 'bounded';
@@ -25,6 +19,32 @@ interface BoundedNativeSequencePayload extends JsonObject {
25
19
  sampling: 'native';
26
20
  coordinateSpace: JsonValue;
27
21
  }
22
+ /** Business editing surface. Infrastructure Assets are assembled by the host. */
23
+ interface BusinessEntityFacade {
24
+ list(): SandboxEntity[];
25
+ get(entityId: string): SandboxEntity | null;
26
+ readCaptionContent(entityId: string): ComposedScriptContent;
27
+ readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;
28
+ create(input: Exclude<CreateEntityInput, {
29
+ entity_kind: 'asset';
30
+ }>): string;
31
+ update(input: UpdateEntityInput): void;
32
+ declareFields(input: UpdateEntityInput): void;
33
+ delete(input: DeleteEntityInput): void;
34
+ }
35
+ /** Physical Asset bindings are maintained outside the sandbox. */
36
+ interface BusinessRelationFacade {
37
+ list(): SandboxRelation[];
38
+ of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];
39
+ link(input: Exclude<LinkRelationInput, {
40
+ relation_kind: 'physical-asset';
41
+ }>): string;
42
+ linkGenerated(input: LinkGeneratedRelationInput): string;
43
+ linkClipAnchor(input: LinkClipAnchorRelationInput): string;
44
+ linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;
45
+ linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;
46
+ unlink(input: UnlinkRelationInput): void;
47
+ }
28
48
  interface CaptionFontDescriptor {
29
49
  readonly system: 'font-library';
30
50
  readonly key: string;
@@ -114,28 +134,6 @@ interface DeleteVoiceoverInput {
114
134
  readonly voiceoverClipEntityIds: readonly string[];
115
135
  }
116
136
  type EmptyRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'audio-script-marker';
117
- interface EntityFacade {
118
- /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */
119
- list(): SandboxEntity[];
120
- get(entityId: string): SandboxEntity | null;
121
- /** Find document resources by external Memota asset id, including directly composed media variants. */
122
- findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];
123
- /** Assemble selected Caption text; missing composition is an error. */
124
- readCaptionContent(entityId: string): ComposedScriptContent;
125
- /** Assemble base text and pronunciation fields before generating Voice. */
126
- readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;
127
- create(input: CreateEntityInput): string;
128
- /** Patch assembled fields, routing inherited fields to their declaring entity. */
129
- update(input: UpdateEntityInput): void;
130
- /** Explicitly declare own fields, overriding unambiguous bases without modifying them. Ordinary edits use update. */
131
- declareFields(input: UpdateEntityInput): void;
132
- /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */
133
- delete(input: DeleteEntityInput): void;
134
- /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */
135
- ensureMedia(fact: MediaAssetFact): {
136
- contentEntityId: string;
137
- };
138
- }
139
137
  type EntityId = string;
140
138
  interface EntityPayloadByKind {
141
139
  axvideo: BoundedDerivedSequencePayload;
@@ -145,12 +143,10 @@ interface EntityPayloadByKind {
145
143
  role?: string;
146
144
  };
147
145
  clip: JsonObject;
148
- /** Physical resource fields; never a copy of Caption content. */
149
- asset: JsonObject;
150
- video: BoundedNativeSequencePayload & MediaAssetPayload;
151
- audio: BoundedNativeSequencePayload & MediaAssetPayload;
152
- voice: BoundedNativeSequencePayload & MediaAssetPayload;
153
- image: UnboundedConstantSequencePayload & MediaAssetPayload;
146
+ video: BoundedNativeSequencePayload;
147
+ audio: BoundedNativeSequencePayload;
148
+ voice: BoundedNativeSequencePayload;
149
+ image: UnboundedConstantSequencePayload;
154
150
  'sequence-marker': JsonObject & {
155
151
  sourceRange: {
156
152
  start: number;
@@ -191,18 +187,18 @@ interface EntityPayloadByKind {
191
187
  };
192
188
  }
193
189
  interface EntityStoreSnapshot {
190
+ /** Causal compiler baseline; required for publishing edits. */
191
+ loroSnapshot?: string;
194
192
  revision: number;
193
+ /** Current AudioScript version attached to the project; initialized projects always attach a script, possibly empty. */
195
194
  audioScriptEntityId: string | null;
196
195
  entities: SandboxEntity[];
197
196
  relations: SandboxRelation[];
198
197
  }
199
- interface ImageMediaAssetFact {
200
- readonly assetId: string;
201
- readonly kind: 'image';
202
- readonly storageKey?: string;
203
- }
204
198
  interface InsertCaptionClipInput {
205
199
  readonly timelineEntityId: string;
200
+ /** Existing generation identity for newly materialized Caption content, distinct from its Clip. */
201
+ readonly captionEntityId?: string;
206
202
  /** Stable placed caption identity, distinct from the Caption content identity. */
207
203
  readonly captionClipEntityId?: string;
208
204
  /** Existing bases composed by this variant; includes an AudioScript text owner. */
@@ -223,21 +219,6 @@ interface InsertClipInput {
223
219
  readonly targetRange?: SequenceRange<number>;
224
220
  readonly clipPayload?: JsonObject;
225
221
  }
226
- interface InsertMediaClipInput {
227
- readonly timelineEntityId: string;
228
- readonly clipEntityId?: string;
229
- readonly media: VisualMediaAssetFact;
230
- /** Source/display window in whole milliseconds. Images use this as their finite display span. */
231
- readonly sourceRange: SequenceRange<number>;
232
- readonly placement: ClipPlacement;
233
- readonly volume?: number;
234
- }
235
- interface InsertMediaClipsInput {
236
- readonly timelineEntityId: string;
237
- readonly clips: readonly ReplacementMediaClipInput[];
238
- /** One placement decision for the whole input-ordered block. */
239
- readonly insertion: MediaClipInsertion;
240
- }
241
222
  interface InsertPlacedClipInput {
242
223
  readonly trackEntityId: string;
243
224
  readonly contentEntityId: string;
@@ -253,8 +234,8 @@ interface JsonObject {
253
234
  }
254
235
  type JsonPrimitive = string | number | boolean | null;
255
236
  type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
256
- type KnownEntityKind = 'axvideo' | 'timeline' | 'track' | 'clip' | 'asset' | 'video' | 'audio' | 'voice' | 'image' | 'sequence-marker' | 'viewport' | 'audio-script' | 'phonetic-script' | 'caption';
257
- type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'audio-script-source' | 'audio-script-marker';
237
+ type KnownEntityKind = 'axvideo' | 'timeline' | 'track' | 'clip' | 'video' | 'audio' | 'voice' | 'image' | 'sequence-marker' | 'viewport' | 'audio-script' | 'phonetic-script' | 'caption';
238
+ type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'generated' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'audio-script-source' | 'audio-script-marker';
258
239
  interface LinearClipSpeed {
259
240
  readonly kind: 'linear';
260
241
  readonly rate: number;
@@ -305,25 +286,6 @@ type LinkRelationInput = (LinkRelationBase & {
305
286
  alignment: JsonValue;
306
287
  };
307
288
  });
308
- /** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */
309
- type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;
310
- type MediaAssetPayload = JsonObject & {
311
- external: {
312
- system: 'memota' | 'memota-speech';
313
- key: string;
314
- };
315
- storageKey?: string;
316
- };
317
- type MediaClipInsertion = {
318
- readonly kind: 'before';
319
- readonly clipEntityId: string;
320
- } | {
321
- readonly kind: 'after';
322
- readonly clipEntityId: string;
323
- } | {
324
- readonly kind: 'firstStart';
325
- readonly startMs: number;
326
- };
327
289
  interface MoveClipInput {
328
290
  readonly clipEntityId: string;
329
291
  readonly trackEntityId: string;
@@ -350,23 +312,6 @@ interface PatchCaptionStyleInput {
350
312
  readonly timelineEntityId: string;
351
313
  readonly style: CaptionStyleFields;
352
314
  }
353
- interface RelationFacade {
354
- list(): SandboxRelation[];
355
- /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */
356
- of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];
357
- /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */
358
- link(input: LinkRelationInput): string;
359
- /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */
360
- linkGenerated(input: LinkGeneratedRelationInput): string;
361
- /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */
362
- linkClipAnchor(input: LinkClipAnchorRelationInput): string;
363
- /** Author ordered phonetic-script-render(output,script) without positional endpoint ambiguity. */
364
- linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;
365
- /** Author ordered audio-script-source(script,source) without positional endpoint ambiguity. */
366
- linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;
367
- /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */
368
- unlink(input: UnlinkRelationInput): void;
369
- }
370
315
  interface ReplaceClipContentInput {
371
316
  readonly clipEntityId: string;
372
317
  /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */
@@ -376,25 +321,6 @@ interface ReplaceClipContentInput {
376
321
  readonly targetRange?: SequenceRange<number>;
377
322
  readonly timeRemapping?: JsonValue;
378
323
  }
379
- interface ReplaceMediaClipInput {
380
- readonly clipEntityId: string;
381
- readonly media: VisualMediaAssetFact;
382
- readonly sourceRange: SequenceRange<number>;
383
- }
384
- interface ReplaceSequentialClipsInput {
385
- readonly timelineEntityId: string;
386
- readonly oldClipEntityIds: readonly string[];
387
- readonly newClips: readonly ReplacementMediaClipInput[];
388
- readonly onAnchored: 'remap' | 'cascade';
389
- }
390
- interface ReplacementMediaClipInput {
391
- readonly clipEntityId?: string;
392
- readonly media: VisualMediaAssetFact;
393
- readonly sourceRange: SequenceRange<number>;
394
- readonly volume?: number;
395
- }
396
- /** Asset identity, either an old physical-only row or a directly composed media variant. */
397
- type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';
398
324
  interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {
399
325
  entity_id: string;
400
326
  entity_kind: K;
@@ -429,12 +355,6 @@ type SequentialClipAnchor = {
429
355
  } | {
430
356
  readonly position: 'trackStart';
431
357
  };
432
- interface SetBgmInput {
433
- readonly timelineEntityId: string;
434
- readonly bgmClipEntityId: string;
435
- readonly media: AudioMediaAssetFact;
436
- readonly volume: number;
437
- }
438
358
  interface SetCaptionVisibilityInput {
439
359
  readonly timelineEntityId: string;
440
360
  readonly hidden: boolean;
@@ -490,67 +410,8 @@ interface UpdateEntityInput {
490
410
  entity_id: string;
491
411
  payload: JsonObject;
492
412
  }
493
- interface VideoMediaAssetFact {
494
- readonly assetId: string;
495
- readonly kind: 'video';
496
- readonly durationMs: number;
497
- readonly storageKey?: string;
498
- }
499
- type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;
500
- interface VoiceDescriptor {
501
- readonly system: 'voice-library';
502
- readonly key: string;
503
- readonly name?: string;
504
- }
505
- interface VoiceMediaAssetFact {
506
- /** Stable external speech result id, independent of the placed Clip id. */
507
- readonly assetId: string;
508
- readonly kind: 'voice';
509
- readonly durationMs: number;
510
- readonly storageKey: string;
511
- /** Present for synthesized voice, absent for original recorded audio. */
512
- readonly voice?: VoiceDescriptor;
513
- }
514
- interface VoiceoverCaptionFact {
515
- /** Stable placed caption identity supplied by the materialized side effect. */
516
- readonly captionClipEntityId: string;
517
- /** Directly held bases; includes the AudioScript used by the Voice. */
518
- readonly baseEntityIds: readonly string[];
519
- /** Ordered selection of AudioScript segments; caption text is never passed inline. */
520
- readonly selections: readonly CaptionSegmentSelection[];
521
- readonly startMs: number;
522
- readonly durationMs: number;
523
- readonly style?: CaptionStyleFields;
524
- }
525
- type VoiceoverTakeInput = {
526
- readonly timelineEntityId: string; /** Stable placed speech identity, distinct from media.assetId. */
527
- readonly voiceoverClipEntityId: string;
528
- readonly media: VoiceMediaAssetFact; /** Existing pronunciation variant; its composed AudioScript stays the text owner. */
529
- readonly phoneticScriptEntityId: string;
530
- readonly volume: number;
531
- readonly captions: readonly VoiceoverCaptionFact[];
532
- } & ({
533
- readonly placement: ClipPlacement;
534
- readonly hostClipEntityId?: never;
535
- readonly anchorOffset?: never;
536
- } | {
537
- readonly placement?: never;
538
- readonly hostClipEntityId: string;
539
- readonly anchorOffset: number;
540
- });
541
- interface VoiceoverTakeResult {
542
- readonly voiceoverClipEntityId: string;
543
- readonly voiceEntityId: string;
544
- /** The pronunciation variant the Voice was rendered from. */
545
- readonly phoneticScriptEntityId: string;
546
- /** The base-text owner resolved from the PhoneticScript baseEntityIds. */
547
- readonly audioScriptEntityId: string;
548
- readonly captionClipEntityIds: readonly string[];
549
- }
550
413
  /** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */
551
414
  interface EditApi {
552
- /** Select the document AudioScript, or clear the optional association with null. */
553
- setDocumentAudioScript(entityId: string | null): void;
554
415
  insertClip(input: InsertClipInput): ClipEntityId;
555
416
  insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;
556
417
  updateClipMarker(input: UpdateClipMarkerInput): void;
@@ -558,39 +419,35 @@ interface EditApi {
558
419
  moveSequentialClips(input: MoveSequentialClipsInput): void;
559
420
  moveClip(input: MoveClipInput): void;
560
421
  replaceClipContent(input: ReplaceClipContentInput): void;
561
- insertMediaClip(input: InsertMediaClipInput): ClipEntityId;
562
- insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];
563
- replaceMediaClip(input: ReplaceMediaClipInput): void;
564
422
  setClipVolume(input: SetClipVolumeInput): void;
565
423
  setClipSpeed(input: SetClipSpeedInput): void;
566
424
  trimClip(input: TrimClipInput): void;
567
- replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];
568
425
  deleteClip(input: DeleteClipInput): void;
569
426
  deleteClipTree(input: DeleteClipTreeInput): void;
570
427
  updateClip(input: UpdateClipInput): void;
571
- upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;
572
428
  moveVoiceover(input: MoveVoiceoverInput): void;
573
429
  moveClipsToStarts(input: MoveClipsToStartsInput): void;
574
430
  deleteVoiceover(input: DeleteVoiceoverInput): void;
575
- setBgm(input: SetBgmInput): ClipEntityId;
576
431
  deleteBgm(input: DeleteBgmInput): void;
577
432
  setCaptionVisibility(input: SetCaptionVisibilityInput): void;
578
433
  patchCaptionStyle(input: PatchCaptionStyleInput): void;
579
434
  insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;
580
435
  }
581
436
  interface TimelineApi {
582
- snapshot(): EntityStoreSnapshot;
437
+ snapshot(): EntityStoreSnapshot & {
438
+ audioScriptEntityId: string;
439
+ };
583
440
  }
584
441
  interface SandboxCheckpoint {
585
442
  readonly index: number;
586
443
  }
587
444
  declare const edit: EditApi;
588
445
  declare const timeline: TimelineApi;
589
- declare const entities: EntityFacade;
590
- declare const relations: RelationFacade;
446
+ declare const entities: BusinessEntityFacade;
447
+ declare const relations: BusinessRelationFacade;
591
448
  declare function checkpoint(): SandboxCheckpoint;
592
449
  declare function rollbackTo(cp: SandboxCheckpoint): void;
593
450
  declare const inputs: Readonly<Record<string, unknown>>;
594
451
  //#endregion
595
- export type { AudioMediaAssetFact, BoundedDerivedSequencePayload, BoundedNativeSequencePayload, CaptionFontDescriptor, CaptionSegmentSelection, CaptionStyleFields, CaptionTextRange, CaptionTextSelection, ClipEntityId, ClipPlacement, ComposedPhoneticContent, ComposedScriptContent, CreateEntityInput, DeleteBgmInput, DeleteClipInput, DeleteClipTreeInput, DeleteEntityInput, DeleteVoiceoverInput, EditApi, EmptyRelationKind, EntityFacade, EntityId, EntityPayloadByKind, EntityStoreSnapshot, ImageMediaAssetFact, InsertCaptionClipInput, InsertClipInput, InsertMediaClipInput, InsertMediaClipsInput, InsertPlacedClipInput, JsonObject, JsonPrimitive, JsonValue, KnownEntityKind, KnownRelationKind, LinearClipSpeed, LinkAudioScriptSourceRelationInput, LinkClipAnchorRelationInput, LinkGeneratedRelationInput, LinkPhoneticScriptRenderRelationInput, LinkRelationInput, MediaAssetFact, MediaAssetPayload, MediaClipInsertion, MoveClipInput, MoveClipsToStartsInput, MoveSequentialClipsInput, MoveVoiceoverInput, PatchCaptionStyleInput, RelationFacade, ReplaceClipContentInput, ReplaceMediaClipInput, ReplaceSequentialClipsInput, ReplacementMediaClipInput, ResourceEntityKind, SandboxCheckpoint, SandboxEntity, SandboxRelation, ScriptTextSegment, SequenceDuration, SequenceRange, SequentialClipAnchor, SetBgmInput, SetCaptionVisibilityInput, SetClipPlacementInput, SetClipSpeedInput, SetClipVolumeInput, StoredEntityPayload, TimelineApi, TrimClipInput, UnboundedConstantSequencePayload, UnlinkRelationInput, UpdateClipInput, UpdateClipMarkerInput, UpdateEntityInput, VideoMediaAssetFact, VisualMediaAssetFact, VoiceDescriptor, VoiceMediaAssetFact, VoiceoverCaptionFact, VoiceoverTakeInput, VoiceoverTakeResult, checkpoint, edit, entities, inputs, relations, rollbackTo, timeline };
452
+ export type { BoundedDerivedSequencePayload, BoundedNativeSequencePayload, BusinessEntityFacade, BusinessRelationFacade, CaptionFontDescriptor, CaptionSegmentSelection, CaptionStyleFields, CaptionTextRange, CaptionTextSelection, ClipEntityId, ClipPlacement, ComposedPhoneticContent, ComposedScriptContent, CreateEntityInput, DeleteBgmInput, DeleteClipInput, DeleteClipTreeInput, DeleteEntityInput, DeleteVoiceoverInput, EditApi, EmptyRelationKind, EntityId, EntityPayloadByKind, EntityStoreSnapshot, InsertCaptionClipInput, InsertClipInput, InsertPlacedClipInput, JsonObject, JsonPrimitive, JsonValue, KnownEntityKind, KnownRelationKind, LinearClipSpeed, LinkAudioScriptSourceRelationInput, LinkClipAnchorRelationInput, LinkGeneratedRelationInput, LinkPhoneticScriptRenderRelationInput, LinkRelationInput, MoveClipInput, MoveClipsToStartsInput, MoveSequentialClipsInput, MoveVoiceoverInput, PatchCaptionStyleInput, ReplaceClipContentInput, SandboxCheckpoint, SandboxEntity, SandboxRelation, ScriptTextSegment, SequenceDuration, SequenceRange, SequentialClipAnchor, SetCaptionVisibilityInput, SetClipPlacementInput, SetClipSpeedInput, SetClipVolumeInput, StoredEntityPayload, TimelineApi, TrimClipInput, UnboundedConstantSequencePayload, UnlinkRelationInput, UpdateClipInput, UpdateClipMarkerInput, UpdateEntityInput, checkpoint, edit, entities, inputs, relations, rollbackTo, timeline };
596
453
  //# sourceMappingURL=sandbox-api.d.mts.map
@@ -183,7 +183,7 @@ function createId(value, label) {
183
183
  //#region ../medeo-dsl/src/json-values.ts
184
184
  const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
185
185
  function isJsonObject(value) {
186
- return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
186
+ return typeof value === "object" && value !== null && !Array.isArray(value) && isJsonValue(value, /* @__PURE__ */ new Set());
187
187
  }
188
188
  function isJsonValue(value, ancestors) {
189
189
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
@@ -422,9 +422,8 @@ function entityToRow(entity) {
422
422
  //#region ../medeo-dsl/src/invariants.ts
423
423
  /**
424
424
  * Media variants own their Asset identity directly: a `physical-asset`
425
- * Relation may not bind them. Caption text is composed domain content, so
426
- * Captions do not bind Physical Assets either; the Relation vocabulary stays
427
- * open for future physical pipeline semantics.
425
+ * Relation may not bind them. Caption may associate with a physical text Asset
426
+ * independently of its AudioScript composition.
428
427
  */
429
428
  function validateMediaAssetIdentity(entity, index) {
430
429
  return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
@@ -583,11 +582,6 @@ function validatePhoneticComposition(phonetic, index, entities = []) {
583
582
  }
584
583
  function validateVariantComposition(variant, index, entities, kind) {
585
584
  const issues = [];
586
- if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
587
- code: "caption_physical_asset_forbidden",
588
- entityId: variant.entityId,
589
- message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
590
- });
591
585
  const rows = {
592
586
  entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
593
587
  relations: []
@@ -1523,6 +1517,98 @@ function errorMessage(error) {
1523
1517
  return error instanceof Error ? error.message : String(error);
1524
1518
  }
1525
1519
  //#endregion
1520
+ //#region src/sandbox/business-facades.ts
1521
+ /** Resource locators are host-owned even when stored on a typed media entity. */
1522
+ const infrastructureFields = new Set(["external", "storageKey"]);
1523
+ function businessEntity(entity) {
1524
+ return {
1525
+ ...entity,
1526
+ payload: Object.fromEntries(Object.entries(entity.payload).filter(([key]) => !infrastructureFields.has(key)))
1527
+ };
1528
+ }
1529
+ function businessState(state) {
1530
+ const assets = new Set(state.entities.filter((row) => row.entity_kind === "asset").map((row) => row.entity_id));
1531
+ return {
1532
+ revision: state.revision,
1533
+ audioScriptEntityId: state.audioScriptEntityId,
1534
+ entities: state.entities.filter((row) => !assets.has(row.entity_id)).map(businessEntity),
1535
+ relations: state.relations.filter((row) => row.relation_kind !== "physical-asset" && !assets.has(row.endpoint_0_entity_id) && !assets.has(row.endpoint_1_entity_id))
1536
+ };
1537
+ }
1538
+ /** Keep infrastructure rows available to assembly without exposing their management to scripts. */
1539
+ function businessFacades(entities, relations) {
1540
+ const isAsset = (id) => entities.get(id)?.entity_kind === "asset";
1541
+ const assertBusiness = (id) => {
1542
+ if (isAsset(id)) throw new Error("Asset entities are managed by host assembly");
1543
+ };
1544
+ const assertBusinessPayload = (payload) => {
1545
+ for (const field of infrastructureFields) if (Object.hasOwn(payload, field)) throw new Error(`Field ${field} is managed by host assembly`);
1546
+ if (Array.isArray(payload.baseEntityIds)) {
1547
+ for (const id of payload.baseEntityIds) if (typeof id === "string") assertBusiness(id);
1548
+ }
1549
+ };
1550
+ const visibleRelation = (relation) => relation.relation_kind !== "physical-asset" && !isAsset(relation.endpoint_0_entity_id) && !isAsset(relation.endpoint_1_entity_id);
1551
+ return {
1552
+ entities: {
1553
+ list: () => entities.list().filter((entity) => entity.entity_kind !== "asset").map(businessEntity),
1554
+ get: (id) => {
1555
+ const entity = entities.get(id);
1556
+ return entity && !isAsset(id) ? businessEntity(entity) : null;
1557
+ },
1558
+ create: (input) => {
1559
+ if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
1560
+ assertBusinessPayload(input.payload);
1561
+ return entities.create(input);
1562
+ },
1563
+ update: (input) => {
1564
+ assertBusiness(input.entity_id);
1565
+ assertBusinessPayload(input.payload);
1566
+ entities.update(input);
1567
+ },
1568
+ declareFields: (input) => {
1569
+ assertBusiness(input.entity_id);
1570
+ assertBusinessPayload(input.payload);
1571
+ entities.declareFields(input);
1572
+ },
1573
+ delete: (input) => {
1574
+ assertBusiness(input.entity_id);
1575
+ const incident = relations.of(input.entity_id);
1576
+ if (incident.some(visibleRelation)) {
1577
+ entities.delete(input);
1578
+ return;
1579
+ }
1580
+ for (const relation of incident) relations.unlink({ relation_id: relation.relation_id });
1581
+ entities.delete(input);
1582
+ },
1583
+ readCaptionContent: (input) => entities.readCaptionContent(input),
1584
+ readPhoneticScriptContent: (input) => entities.readPhoneticScriptContent(input)
1585
+ },
1586
+ relations: {
1587
+ list: () => relations.list().filter(visibleRelation),
1588
+ of: (id, kind) => relations.of(id, kind).filter(visibleRelation),
1589
+ link: (input) => {
1590
+ if (input.relation_kind === "physical-asset") throw new Error("Asset bindings are managed by host assembly");
1591
+ assertBusiness(input.endpoint_0_entity_id);
1592
+ assertBusiness(input.endpoint_1_entity_id);
1593
+ return relations.link(input);
1594
+ },
1595
+ unlink: (input) => {
1596
+ const relation = relations.list().find((row) => row.relation_id === input.relation_id);
1597
+ if (relation && !visibleRelation(relation)) throw new Error("Asset bindings are managed by host assembly");
1598
+ relations.unlink(input);
1599
+ },
1600
+ linkGenerated: (input) => {
1601
+ assertBusiness(input.output_entity_id);
1602
+ assertBusiness(input.input_entity_id);
1603
+ return relations.linkGenerated(input);
1604
+ },
1605
+ linkClipAnchor: (input) => relations.linkClipAnchor(input),
1606
+ linkPhoneticScriptRender: (input) => relations.linkPhoneticScriptRender(input),
1607
+ linkAudioScriptSource: (input) => relations.linkAudioScriptSource(input)
1608
+ }
1609
+ };
1610
+ }
1611
+ //#endregion
1526
1612
  //#region src/entity/entity-sandbox.ts
1527
1613
  const ASSET_SOURCE_KEY = "external";
1528
1614
  const MEMOTA_SYSTEM = "memota";
@@ -1560,16 +1646,11 @@ var EntitySandbox = class {
1560
1646
  ensureFoundation(timelinePayload = {}) {
1561
1647
  const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload);
1562
1648
  this.appendResourceRows(foundation.rows);
1649
+ if (this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = foundation.audioScriptEntityId;
1563
1650
  }
1564
1651
  get audioScriptEntityId() {
1565
1652
  return this.state.audioScriptEntityId;
1566
1653
  }
1567
- setDocumentAudioScript(audioScriptEntityId) {
1568
- this.record({
1569
- kind: "set-document-audio-script",
1570
- audioScriptEntityId
1571
- });
1572
- }
1573
1654
  rollbackTo(index) {
1574
1655
  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}`);
1575
1656
  const prefix = this.commands.slice(0, index);
@@ -1582,11 +1663,13 @@ var EntitySandbox = class {
1582
1663
  buildPlan() {
1583
1664
  const rows = toDslRows(this.state);
1584
1665
  decodeEntityRelationRows(rows, numericMarkerComparators);
1585
- assertCanonicalEditorResources(rows);
1586
- readDocumentAudioScript({
1587
- rows,
1588
- audioScriptEntityId: this.state.audioScriptEntityId
1589
- });
1666
+ if (this.state.revision !== 0 || rows.entities.length !== 0 || this.commands.length !== 0) {
1667
+ assertCanonicalEditorResources(rows);
1668
+ readDocumentAudioScript({
1669
+ rows,
1670
+ audioScriptEntityId: this.state.audioScriptEntityId
1671
+ });
1672
+ }
1590
1673
  const currentEntityIds = new Set(this.state.entities.map((entity) => entity.entity_id));
1591
1674
  const currentRelationIds = new Set(this.state.relations.map((relation) => relation.relation_id));
1592
1675
  return {
@@ -1600,9 +1683,6 @@ var EntitySandbox = class {
1600
1683
  renderPreview() {
1601
1684
  const lines = [`Entity plan: base_revision=${this.original.revision} commands=${this.commands.length} entities=${this.state.entities.length} relations=${this.state.relations.length}`];
1602
1685
  for (const command of this.commands) switch (command.kind) {
1603
- case "set-document-audio-script":
1604
- lines.push(`~ document audioScriptEntityId=${command.audioScriptEntityId}`);
1605
- break;
1606
1686
  case "create-entity":
1607
1687
  lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
1608
1688
  break;
@@ -1722,6 +1802,10 @@ var EntitySandbox = class {
1722
1802
  }
1723
1803
  const entityId = input.entity_id ?? this.idFactory("entity");
1724
1804
  assertTrimmed(entityId, "entity_id");
1805
+ if (input.entity_kind === "audio-script") {
1806
+ const existing = this.state.entities.find((entity) => entity.entity_kind === "audio-script");
1807
+ if (existing !== void 0 && existing.entity_id !== entityId) throw new Error(`Editor requires exactly one AudioScript; edit ${existing.entity_id} instead of creating ${entityId}`);
1808
+ }
1725
1809
  const entity = {
1726
1810
  entity_id: entityId,
1727
1811
  entity_kind: input.entity_kind,
@@ -1949,18 +2033,13 @@ var EntitySandbox = class {
1949
2033
  }
1950
2034
  apply(command, enforceIdentity) {
1951
2035
  switch (command.kind) {
1952
- case "set-document-audio-script":
1953
- readDocumentAudioScript({
1954
- rows: toDslRows(this.state),
1955
- audioScriptEntityId: command.audioScriptEntityId
1956
- });
1957
- this.state.audioScriptEntityId = command.audioScriptEntityId;
1958
- return;
1959
2036
  case "create-entity": {
2037
+ if (command.entity.entity_kind === "audio-script" && this.state.entities.some((entity) => entity.entity_kind === "audio-script")) throw new Error("The project AudioScript already exists; edit its segments instead");
1960
2038
  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`);
1961
2039
  const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
1962
2040
  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}"`);
1963
2041
  this.state.entities.push(clone(command.entity));
2042
+ if (command.entity.entity_kind === "audio-script" && this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = command.entity.entity_id;
1964
2043
  return;
1965
2044
  }
1966
2045
  case "update-entity": {
@@ -1975,7 +2054,7 @@ var EntitySandbox = class {
1975
2054
  return;
1976
2055
  }
1977
2056
  case "delete-entity": {
1978
- if (this.state.audioScriptEntityId === command.entity_id) throw new Error("Clear or switch the document AudioScript association before deleting its entity");
2057
+ if (this.state.entities.find((entity) => entity.entity_id === command.entity_id)?.entity_kind === "audio-script") throw new Error("The document AudioScript is a fixed project identity and cannot be deleted");
1979
2058
  const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
1980
2059
  if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
1981
2060
  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();
@@ -2393,6 +2472,6 @@ function replayJournalSync(adapter, journal) {
2393
2472
  }
2394
2473
  }
2395
2474
  //#endregion
2396
- 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 };
2475
+ export { createEntityId as a, collectAffectedPartIds as c, businessState as i, renderPreview as l, EntitySandbox as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EditSandboxSession as t, renderCompactProjection as u };
2397
2476
 
2398
- //# sourceMappingURL=script-session-C2uQHYt4.mjs.map
2477
+ //# sourceMappingURL=script-session-DTq_VPEA.mjs.map