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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as renderCompactProjection, n as collectAffectedPartIds, r as renderPreview, t as EditSandboxSession } from "./script-session-BF44uKv_.mjs";
2
- import { ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, replayJournal, toVideoDocument } from "@mengine/medeo-client";
1
+ import { a as collectAffectedPartIds, o as renderPreview, s as renderCompactProjection, t as EditSandboxSession } from "./script-session-CHyIUBkO.mjs";
2
+ import { EntityGraphHttpClient, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, migrateLegacyTimelineToEntities, replayJournal, toVideoDocument } from "@mengine/medeo-client";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { randomUUID } from "node:crypto";
5
5
  //#region src/sandbox/node-host.ts
@@ -35,7 +35,8 @@ function runEditScript(options) {
35
35
  script: options.script,
36
36
  inputs: options.inputs,
37
37
  entityState: options.entityState,
38
- idLabel: options.idLabel
38
+ idLabel: options.idLabel,
39
+ entityOnly: options.entityOnly
39
40
  },
40
41
  execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
41
42
  "--experimental-transform-types",
@@ -124,6 +125,8 @@ function runEditScript(options) {
124
125
  entity_base_revision: message.entityBaseRevision,
125
126
  entity_commands: entityCommands.slice(),
126
127
  ...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
128
+ deleted_entity_ids: message.deletedEntityIds,
129
+ deleted_relation_ids: message.deletedRelationIds,
127
130
  preview: message.preview,
128
131
  logs: logs.slice()
129
132
  },
@@ -204,7 +207,9 @@ const KNOWN_RELATION_KINDS = [
204
207
  "generated",
205
208
  "phonetic-script-provenance",
206
209
  "caption-provenance",
207
- "caption-alignment"
210
+ "caption-alignment",
211
+ "clip-anchor",
212
+ "audio-script-render"
208
213
  ];
209
214
  //#endregion
210
215
  //#region src/entity/entity-http-client.ts
@@ -232,7 +237,7 @@ var EntityHttpClient = class {
232
237
  async fetchState() {
233
238
  return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
234
239
  }
235
- async commit(expectedRevision, state) {
240
+ async commit(expectedRevision, state, deletions = {}) {
236
241
  return toSnapshot(await this.requestJson({
237
242
  method: "POST",
238
243
  body: JSON.stringify({
@@ -240,7 +245,9 @@ var EntityHttpClient = class {
240
245
  rows: {
241
246
  entities: state.entities,
242
247
  relations: state.relations
243
- }
248
+ },
249
+ deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
250
+ deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
244
251
  })
245
252
  }), this.options.docId);
246
253
  }
@@ -320,736 +327,278 @@ async function safeReadJson(response) {
320
327
  }
321
328
  }
322
329
  //#endregion
323
- //#region src/sandbox/generated/edit-sandbox-model-context.ts
324
- /**
325
- * @generated by gen:sandbox-dts — DO NOT EDIT MANUALLY
326
- *
327
- * Runtime copy of the sandbox TypeScript disclosure. The model prompt imports
328
- * this value so its interface and the checked-in declaration cannot drift.
329
- */
330
- const EDIT_SANDBOX_API_DTS = [
331
- "/**",
332
- " * @generated by gen:sandbox-dts — DO NOT EDIT MANUALLY",
333
- " *",
334
- " * Schema version: video-document/v0",
335
- " * Semantic ops: 20",
336
- " *",
337
- " * Boundary: zod `superRefine` / custom refine rules are NOT introspectable and",
338
- " * do not appear here. Business mutual-exclusion rules surface via runtime",
339
- " * validation errors (L3 feedback channel).",
340
- " *",
341
- " * @example 读取→计算→批量写",
342
- " * ```ts",
343
- " * const clips = timeline.clipsInRange(0, 10_000);",
344
- " * await edit.setVideoClipSpeedShift({",
345
- " * clips: clips.map((c) => ({ clip_id: c.id, speed_shift: { category: 'linear', mode: 'constant', config: { linear: { speed: 1.5 } } } })),",
346
- " * });",
347
- " * ```",
348
- " *",
349
- " * @example anchored 删除",
350
- " * ```ts",
351
- " * await edit.deleteVideoClips({ clip_ids: ['clip_a'], on_anchored: 'detach' });",
352
- " * ```",
353
- " */",
354
- "",
355
- "/**",
356
- " * A clip's playback-speed fact, the only thing `SetVideoClipSpeedShift` writes.",
357
- " */",
358
- "export interface SpeedShift {",
359
- " category: 'linear' | 'curve';",
360
- " mode: string;",
361
- " config:",
362
- " | {",
363
- " linear: {",
364
- " /**",
365
- " * @constraint positive",
366
- " */",
367
- " speed: number;",
368
- " };",
369
- " }",
370
- " | {",
371
- " curve: {",
372
- " /**",
373
- " * @constraint minLength(2)",
374
- " */",
375
- " keyframes: {",
376
- " /**",
377
- " * @constraint min(0)",
378
- " * @constraint max(1)",
379
- " */",
380
- " position: number;",
381
- " /**",
382
- " * @constraint min(0)",
383
- " */",
384
- " rate: number;",
385
- " /**",
386
- " * Bezier tangent handle (x, y)",
387
- " */",
388
- " in_tangent?: { x: number; y: number };",
389
- " /**",
390
- " * Bezier tangent handle (x, y)",
391
- " */",
392
- " out_tangent?: { x: number; y: number };",
393
- " }[];",
394
- " };",
395
- " };",
396
- "}",
397
- "",
398
- "/**",
399
- " * TTS voice summary attached to a speech",
400
- " */",
401
- "export interface Voice {",
402
- " /**",
403
- " * @constraint minLength(1)",
404
- " */",
405
- " id: string;",
406
- " name: string;",
407
- "}",
408
- "",
409
- "/**",
410
- " * A materialized speech-subtree write (speeches + their captions).",
411
- " */",
412
- "export interface SpeechAssets {",
413
- " /**",
414
- " * Materialized speech parts to write",
415
- " * @constraint minLength(1)",
416
- " */",
417
- " speeches: {",
418
- " /**",
419
- " * The speech part ID (= side-effect speech_parts[].id)",
420
- " * @constraint minLength(1)",
421
- " */",
422
- " speech_id: string;",
423
- " /**",
424
- " * Host video clip part ID the speech anchors to (RFC 02 §4)",
425
- " * @constraint minLength(1)",
426
- " */",
427
- " anchor_part_id: string;",
428
- " /**",
429
- " * Offset within the host clip (speech.abs = host.abs + offset_ms)",
430
- " * @constraint int",
431
- " * @constraint min(0)",
432
- " */",
433
- " offset_ms: number;",
434
- " /**",
435
- " * @constraint minLength(1)",
436
- " */",
437
- " audio_storage_key: string;",
438
- " /**",
439
- " * Duration in milliseconds (> 0)",
440
- " * @constraint int",
441
- " * @constraint positive",
442
- " */",
443
- " duration_ms: number;",
444
- " audio_script: string;",
445
- " /**",
446
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
447
- " * @constraint min(-60)",
448
- " * @constraint max(20)",
449
- " */",
450
- " volume: number;",
451
- " /**",
452
- " * TTS voice summary attached to a speech",
453
- " */",
454
- " voice: {",
455
- " /**",
456
- " * @constraint minLength(1)",
457
- " */",
458
- " id: string;",
459
- " name: string;",
460
- " };",
461
- " /**",
462
- " * @constraint minLength(1)",
463
- " */",
464
- " origin_speech_id: string;",
465
- " /**",
466
- " * Caption part IDs owned by this speech",
467
- " */",
468
- " caption_ids: string[];",
469
- " }[];",
470
- " /**",
471
- " * Materialized caption parts owned by the speeches",
472
- " */",
473
- " captions: {",
474
- " /**",
475
- " * The caption part ID (= side-effect created_caption_parts[].id)",
476
- " * @constraint minLength(1)",
477
- " */",
478
- " caption_id: string;",
479
- " /**",
480
- " * The owning speech part ID",
481
- " * @constraint minLength(1)",
482
- " */",
483
- " speech_part_id: string;",
484
- " text: string;",
485
- " /**",
486
- " * Offset within the host speech (caption.abs = speech.abs + start_ms)",
487
- " * @constraint int",
488
- " * @constraint min(0)",
489
- " */",
490
- " start_ms: number;",
491
- " /**",
492
- " * Duration in milliseconds (> 0)",
493
- " * @constraint int",
494
- " * @constraint positive",
495
- " */",
496
- " duration_ms: number;",
497
- " }[];",
330
+ //#region src/migration-input.ts
331
+ /** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
332
+ function parseMigrationAssetFacts(value) {
333
+ if (!Array.isArray(value)) throw new Error("asset_facts is required for migrate-legacy and must be an array");
334
+ return value.map((item) => {
335
+ if (!record(item)) throw new Error("Each asset_facts entry must be an object");
336
+ const { assetId, kind, durationMs, storageKey, voice } = item;
337
+ if (!nonempty(assetId)) throw new Error("asset_facts.assetId must be a non-empty trimmed string");
338
+ if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "voice") throw new Error("asset_facts.kind must be image, video, audio, or voice");
339
+ if (Object.keys(item).some((key) => ![
340
+ "assetId",
341
+ "kind",
342
+ "durationMs",
343
+ "storageKey",
344
+ "voice"
345
+ ].includes(key))) throw new Error("Unknown asset_facts field");
346
+ if (storageKey !== void 0 && !nonempty(storageKey)) throw new Error("asset_facts.storageKey must be non-empty");
347
+ if (kind === "image") {
348
+ if (durationMs !== void 0 || voice !== void 0) throw new Error("Image facts cannot declare duration or voice");
349
+ return {
350
+ assetId,
351
+ kind,
352
+ ...storageKey === void 0 ? {} : { storageKey }
353
+ };
354
+ }
355
+ if (typeof durationMs !== "number" || !Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error("asset_facts.durationMs must be factual positive whole milliseconds");
356
+ if (kind === "video") {
357
+ if (voice !== void 0) throw new Error("Video facts cannot declare voice");
358
+ return {
359
+ assetId,
360
+ kind,
361
+ durationMs,
362
+ ...storageKey === void 0 ? {} : { storageKey }
363
+ };
364
+ }
365
+ if (!nonempty(storageKey)) throw new Error("Audio and Voice facts require their physical storageKey");
366
+ if (kind === "audio") {
367
+ if (voice !== void 0) throw new Error("Audio facts cannot declare a Voice descriptor");
368
+ return {
369
+ assetId,
370
+ kind,
371
+ durationMs,
372
+ storageKey
373
+ };
374
+ }
375
+ if (!record(voice) || voice.system !== "voice-library" || !nonempty(voice.key) || voice.name !== void 0 && typeof voice.name !== "string" || Object.keys(voice).some((key) => ![
376
+ "system",
377
+ "key",
378
+ "name"
379
+ ].includes(key))) throw new Error("Voice facts require an explicit voice-library descriptor");
380
+ return {
381
+ assetId,
382
+ kind,
383
+ durationMs,
384
+ storageKey,
385
+ voice: {
386
+ system: "voice-library",
387
+ key: voice.key,
388
+ ...voice.name === void 0 ? {} : { name: voice.name }
389
+ }
390
+ };
391
+ });
392
+ }
393
+ function record(value) {
394
+ return value !== null && typeof value === "object" && !Array.isArray(value);
395
+ }
396
+ function nonempty(value) {
397
+ return typeof value === "string" && value.length > 0 && value.trim() === value;
398
+ }
399
+ //#endregion
400
+ //#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
401
+ /** @generated by gen:sandbox-dts. DO NOT EDIT. */
402
+ const ENTITY_EDIT_SANDBOX_API_DTS = [
403
+ "/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
404
+ "export interface AudioMediaAssetFact {",
405
+ " readonly assetId: string;",
406
+ " readonly kind: 'audio';",
407
+ " readonly durationMs: number;",
408
+ " readonly storageKey: string;",
498
409
  "}",
499
- "",
500
- "export interface MoveVideoClipsInput {",
501
- " /**",
502
- " * List of video clips to move to new positions",
503
- " * @constraint minLength(1)",
504
- " */",
505
- " clips: {",
506
- " /**",
507
- " * The video clip part ID to move",
508
- " * @constraint minLength(1)",
509
- " */",
510
- " clip_id: string;",
511
- " /**",
512
- " * New absolute start time in milliseconds on the timeline",
513
- " * @constraint int",
514
- " * @constraint min(0)",
515
- " */",
516
- " new_start_ms: number;",
517
- " /**",
518
- " * Target track ID to move the clip to (optional)",
519
- " * @constraint minLength(1)",
520
- " */",
521
- " new_track_id?: string;",
522
- " }[];",
410
+ "export interface BoundedDerivedSequencePayload extends JsonObject {",
411
+ " extent: {",
412
+ " kind: 'bounded';",
413
+ " start: number;",
414
+ " end: number;",
415
+ " };",
416
+ " sampling: 'derived';",
417
+ " coordinateSpace: JsonValue;",
523
418
  "}",
524
- "",
525
- "/**",
526
- " * Reorder a set of main-track clips relative to a reference clip.",
527
- " */",
528
- "export interface MoveVideoClipsByAnchorInput {",
529
- " /**",
530
- " * Clips to move as one block, keeping their relative order. Need not be contiguous on the track.",
531
- " * @constraint minLength(1)",
532
- " */",
533
- " clip_ids: string[];",
534
- " /**",
535
- " * Where the moved block lands: before/after a reference clip, or at the head of the track",
536
- " */",
537
- " anchor:",
538
- " | {",
539
- " position: 'before';",
540
- " /**",
541
- " * The moved block lands immediately before this clip",
542
- " * @constraint minLength(1)",
543
- " */",
544
- " clip_id: string;",
545
- " }",
546
- " | {",
547
- " position: 'after';",
548
- " /**",
549
- " * The moved block lands immediately after this clip",
550
- " * @constraint minLength(1)",
551
- " */",
552
- " clip_id: string;",
553
- " }",
554
- " | { position: 'track_start' };",
555
- " /**",
556
- " * What happens to speeches anchored to the moved clips (required — see the policy doc)",
557
- " */",
558
- " on_anchored: 'follow' | 'keep_absolute';",
419
+ "export interface BoundedNativeSequencePayload extends JsonObject {",
420
+ " /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
421
+ " extent: {",
422
+ " kind: 'bounded';",
423
+ " start: number;",
424
+ " end: number;",
425
+ " };",
426
+ " sampling: 'native';",
427
+ " coordinateSpace: JsonValue;",
559
428
  "}",
560
- "",
561
- "export interface DeleteVideoClipsInput {",
562
- " /**",
563
- " * List of video clip part IDs to delete from the main track",
564
- " * @constraint minLength(1)",
565
- " */",
566
- " clip_ids: string[];",
567
- " /**",
568
- " * How to treat anchored children (default cascade)",
569
- " */",
570
- " on_anchored?: 'cascade' | 'detach';",
429
+ "export interface CaptionFontDescriptor {",
430
+ " readonly system: 'font-library';",
431
+ " readonly key: string;",
571
432
  "}",
572
- "",
573
- "/**",
574
- " * Add video clips to a track.",
575
- " */",
576
- "export interface AddVideoClipsInput {",
577
- " /**",
578
- " * List of video clips to create",
579
- " * @constraint minLength(1)",
580
- " */",
581
- " clips: {",
582
- " /**",
583
- " * The media asset ID for the video clip",
584
- " * @constraint minLength(1)",
585
- " */",
586
- " media_id: string;",
587
- " /**",
588
- " * Absolute start time in milliseconds on the timeline",
589
- " * @constraint int",
590
- " * @constraint min(0)",
591
- " */",
592
- " start_ms?: number;",
593
- " /**",
594
- " * The source media's intrinsic full length in ms",
595
- " * @constraint int",
596
- " * @constraint positive",
597
- " */",
598
- " media_duration_ms: number;",
599
- " /**",
600
- " * Trim window start in the media (default 0)",
601
- " * @constraint int",
602
- " * @constraint min(0)",
603
- " */",
604
- " play_in?: number;",
605
- " /**",
606
- " * Trim window end in the media (default media_duration_ms)",
607
- " * @constraint int",
608
- " * @constraint positive",
609
- " */",
610
- " play_out?: number;",
611
- " /**",
612
- " * Target track ID (optional, defaults to main track)",
613
- " * @constraint minLength(1)",
614
- " */",
615
- " track_id?: string;",
616
- " }[];",
617
- " /**",
618
- " * Insert new clips before this clip ID",
619
- " * @constraint minLength(1)",
620
- " */",
621
- " before_clip_id?: string;",
622
- " /**",
623
- " * Insert new clips after this clip ID",
624
- " * @constraint minLength(1)",
625
- " */",
626
- " after_clip_id?: string;",
433
+ "export interface CaptionStyleFields {",
434
+ " readonly font?: CaptionFontDescriptor;",
435
+ " readonly fontSize?: number;",
436
+ " readonly fontColor?: string;",
437
+ " readonly fontWeight?: number;",
438
+ " readonly entranceAnimation?: string;",
439
+ " readonly entranceAnimationDurationMs?: number;",
440
+ " readonly strokeColor?: string;",
441
+ " readonly strokeWidth?: number;",
442
+ " readonly positionX?: number;",
443
+ " readonly positionY?: number;",
627
444
  "}",
628
- "",
629
- "export interface AdjustVideoClipVolumeInput {",
630
- " /**",
631
- " * List of video clips with their new volume settings",
632
- " * @constraint minLength(1)",
633
- " */",
634
- " clips: {",
635
- " /**",
636
- " * The video clip part ID to adjust volume for",
637
- " * @constraint minLength(1)",
638
- " */",
639
- " clip_id: string;",
640
- " /**",
641
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
642
- " * @constraint min(-60)",
643
- " * @constraint max(20)",
644
- " */",
645
- " volume: number;",
646
- " }[];",
445
+ "export type ClipEntityId = EntityId;",
446
+ "export type ClipPlacement =",
447
+ " | {",
448
+ " readonly kind: 'sequential';",
449
+ " readonly order: number;",
450
+ " }",
451
+ " | {",
452
+ " readonly kind: 'absolute';",
453
+ " readonly targetRange: SequenceRange<number>;",
454
+ " }",
455
+ " | {",
456
+ " readonly kind: 'anchored';",
457
+ " readonly hostClipEntityId: string;",
458
+ " readonly anchorOffset: number;",
459
+ " };",
460
+ "export type CreateEntityInput = {",
461
+ " [K in KnownEntityKind]: {",
462
+ " entity_id?: string;",
463
+ " entity_kind: K;",
464
+ " payload: EntityPayloadByKind[K];",
465
+ " };",
466
+ "}[KnownEntityKind];",
467
+ "export interface DeleteBgmInput {",
468
+ " readonly timelineEntityId: string;",
647
469
  "}",
648
- "",
649
- "/**",
650
- " * Set the playback speed of existing video clips.",
651
- " */",
652
- "export interface SetVideoClipSpeedShiftInput {",
653
- " /**",
654
- " * Video clips with their new speed settings",
655
- " * @constraint minLength(1)",
656
- " */",
657
- " clips: {",
658
- " /**",
659
- " * The video clip part ID to set speed for",
660
- " * @constraint minLength(1)",
661
- " */",
662
- " clip_id: string;",
663
- " /**",
664
- " * The new speed setting, or null to reset to 1×",
665
- " */",
666
- " speed_shift: {",
667
- " category: 'linear' | 'curve';",
668
- " mode: string;",
669
- " config:",
670
- " | {",
671
- " linear: {",
672
- " /**",
673
- " * @constraint positive",
674
- " */",
675
- " speed: number;",
676
- " };",
677
- " }",
678
- " | {",
679
- " curve: {",
680
- " /**",
681
- " * @constraint minLength(2)",
682
- " */",
683
- " keyframes: {",
684
- " /**",
685
- " * @constraint min(0)",
686
- " * @constraint max(1)",
687
- " */",
688
- " position: number;",
689
- " /**",
690
- " * @constraint min(0)",
691
- " */",
692
- " rate: number;",
693
- " /**",
694
- " * Bezier tangent handle (x, y)",
695
- " */",
696
- " in_tangent?: { x: number; y: number };",
697
- " /**",
698
- " * Bezier tangent handle (x, y)",
699
- " */",
700
- " out_tangent?: { x: number; y: number };",
701
- " }[];",
702
- " };",
703
- " };",
704
- " } | null;",
705
- " }[];",
470
+ "export interface DeleteClipInput {",
471
+ " readonly clipEntityId: string;",
706
472
  "}",
707
- "",
708
- "/**",
709
- " * Replace the media backing existing video clips.",
710
- " */",
711
- "export interface ReplaceVideoClipContentInput {",
712
- " /**",
713
- " * Video clips whose media is being replaced",
714
- " * @constraint minLength(1)",
715
- " */",
716
- " clips: {",
717
- " /**",
718
- " * Existing video clip part ID to re-point",
719
- " * @constraint minLength(1)",
720
- " */",
721
- " clip_id: string;",
722
- " /**",
723
- " * The new media asset ID",
724
- " * @constraint minLength(1)",
725
- " */",
726
- " origin_media_id: string;",
727
- " /**",
728
- " * The new media's intrinsic full length",
729
- " * @constraint int",
730
- " * @constraint positive",
731
- " */",
732
- " media_duration_ms: number;",
733
- " /**",
734
- " * Trim window start in the new media (usually 0)",
735
- " * @constraint int",
736
- " * @constraint min(0)",
737
- " */",
738
- " play_in: number;",
739
- " /**",
740
- " * Trim window end in the new media (usually = media_duration_ms)",
741
- " * @constraint int",
742
- " * @constraint positive",
743
- " */",
744
- " play_out: number;",
745
- " /**",
746
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
747
- " * @constraint min(-60)",
748
- " * @constraint max(20)",
749
- " */",
750
- " volume: number;",
751
- " }[];",
473
+ "export interface DeleteClipTreeInput {",
474
+ " readonly clipEntityIds: readonly string[];",
475
+ " readonly onAnchored: 'cascade' | 'detach';",
752
476
  "}",
753
- "",
754
- "/**",
755
- " * Replace a contiguous run of main-track clips with a new run.",
756
- " */",
757
- "export interface ReplaceVideoClipSequenceInput {",
758
- " /**",
759
- " * The clips being replaced: a contiguous main-track run, listed in timeline order",
760
- " * @constraint minLength(1)",
761
- " */",
762
- " old_clip_ids: string[];",
763
- " /**",
764
- " * The replacement clips, in the order they take on the track",
765
- " * @constraint minLength(1)",
766
- " */",
767
- " new_clips: {",
768
- " /**",
769
- " * The replacement media asset ID. Omit to create an empty placeholder clip.",
770
- " * @constraint minLength(1)",
771
- " */",
772
- " media_id?: string;",
773
- " /**",
774
- " * The source media's intrinsic full length in ms",
775
- " * @constraint int",
776
- " * @constraint positive",
777
- " */",
778
- " media_duration_ms: number;",
779
- " /**",
780
- " * Trim window start in the media (default 0)",
781
- " * @constraint int",
782
- " * @constraint min(0)",
783
- " */",
784
- " play_in?: number;",
785
- " /**",
786
- " * Trim window end in the media (default media_duration_ms)",
787
- " * @constraint int",
788
- " * @constraint positive",
789
- " */",
790
- " play_out?: number;",
791
- " }[];",
792
- " /**",
793
- " * What happens to speeches anchored to the replaced clips (required — see the policy doc)",
794
- " */",
795
- " on_anchored: 'remap' | 'cascade';",
477
+ "export interface DeleteEntityInput {",
478
+ " entity_id: string;",
796
479
  "}",
797
- "",
798
- "/**",
799
- " * Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window).",
800
- " */",
801
- "export interface AdjustVideoClipDurationInput {",
802
- " /**",
803
- " * Video clips with their new trim windows",
804
- " * @constraint minLength(1)",
805
- " */",
806
- " clips: {",
807
- " /**",
808
- " * The video clip part ID to re-trim",
809
- " * @constraint minLength(1)",
810
- " */",
811
- " clip_id: string;",
812
- " /**",
813
- " * New trim window start in the source media",
814
- " * @constraint int",
815
- " * @constraint min(0)",
816
- " */",
817
- " play_in: number;",
818
- " /**",
819
- " * New trim window end in the source media",
820
- " * @constraint int",
821
- " * @constraint positive",
822
- " */",
823
- " play_out: number;",
824
- " }[];",
480
+ "export interface DeleteVoiceoverInput {",
481
+ " readonly voiceoverClipEntityIds: readonly string[];",
825
482
  "}",
826
- "",
827
- "/**",
828
- " * Add speeches (and their captions).",
829
- " */",
830
- "export interface AddSpeechesInput extends SpeechAssets {}",
831
- "",
832
- "/**",
833
- " * Delete speeches with their captions.",
834
- " */",
835
- "export interface DeleteSpeechesInput {",
836
- " /**",
837
- " * Speech part IDs to delete (their captions cascade-delete)",
838
- " * @constraint minLength(1)",
839
- " */",
840
- " speech_ids: string[];",
483
+ "export type EmptyRelationKind =",
484
+ " | 'timeline-track'",
485
+ " | 'track-clip'",
486
+ " | 'clip-marker'",
487
+ " | 'marker-content'",
488
+ " | 'axvideo-marker'",
489
+ " | 'marker-timeline';",
490
+ "export interface EntityFacade {",
491
+ " list(): SandboxEntity[];",
492
+ " get(entityId: string): SandboxEntity | null;",
493
+ " /** Return every explicitly imported Asset entity for a Memota asset id. */",
494
+ " findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
495
+ " create(input: CreateEntityInput): string;",
496
+ " /** Replace one Entity's owned payload without changing its identity or kind. */",
497
+ " update(input: UpdateEntityInput): void;",
498
+ " /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
499
+ " delete(input: DeleteEntityInput): void;",
500
+ " /** Import one physical asset without implying a one-to-one media Entity mapping. */",
501
+ " importAsset(input: ImportAssetInput): string;",
841
502
  "}",
842
- "",
843
- "/**",
844
- " * Move speeches in time.",
845
- " */",
846
- "export interface MoveSpeechesInput {",
847
- " /**",
848
- " * Speeches to move to new positions",
849
- " * @constraint minLength(1)",
850
- " */",
851
- " speeches: {",
852
- " /**",
853
- " * The speech part ID to move",
854
- " * @constraint minLength(1)",
855
- " */",
856
- " speech_id: string;",
857
- " /**",
858
- " * New absolute start time on the timeline",
859
- " * @constraint int",
860
- " * @constraint min(0)",
861
- " */",
862
- " new_start_ms: number;",
863
- " }[];",
503
+ "export type EntityId = string;",
504
+ "export interface EntityPayloadByKind {",
505
+ " axvideo: BoundedDerivedSequencePayload;",
506
+ " timeline: JsonObject;",
507
+ " track: JsonObject & {",
508
+ " hidden?: boolean;",
509
+ " role?: string;",
510
+ " };",
511
+ " clip: JsonObject;",
512
+ " /** Asset-owned metadata. Peer media associations belong in physical-asset Relations. */",
513
+ " asset: JsonObject;",
514
+ " video: BoundedNativeSequencePayload;",
515
+ " audio: BoundedNativeSequencePayload;",
516
+ " voice: BoundedNativeSequencePayload;",
517
+ " image: UnboundedConstantSequencePayload;",
518
+ " 'sequence-marker': JsonObject & {",
519
+ " sourceRange: {",
520
+ " start: number;",
521
+ " end: number;",
522
+ " };",
523
+ " targetRange?: {",
524
+ " start: number;",
525
+ " end: number;",
526
+ " };",
527
+ " duration:",
528
+ " | {",
529
+ " mode: 'from-source';",
530
+ " }",
531
+ " | {",
532
+ " mode: 'fixed';",
533
+ " value: number;",
534
+ " };",
535
+ " timeRemapping?: JsonValue;",
536
+ " anchorOffset?: number;",
537
+ " durationPolicy?: 'timeline';",
538
+ " };",
539
+ " viewport: JsonObject;",
540
+ " 'audio-script': JsonObject & {",
541
+ " segments: ScriptTextSegment[];",
542
+ " };",
543
+ " 'phonetic-script': JsonObject & {",
544
+ " segments: ScriptTextSegment[];",
545
+ " };",
546
+ " caption: BoundedNativeSequencePayload;",
864
547
  "}",
865
- "",
866
- "/**",
867
- " * Change a speech's script or voice.",
868
- " */",
869
- "export interface ChangeSpeechScriptInput extends SpeechAssets {}",
870
- "",
871
- "export interface ChangeSpeechVoiceInput extends SpeechAssets {}",
872
- "",
873
- "export interface AdjustSpeechVolumeInput {",
874
- " /**",
875
- " * List of speeches with their new volume settings",
876
- " * @constraint minLength(1)",
877
- " */",
878
- " speeches: {",
879
- " /**",
880
- " * The speech part ID to adjust volume for",
881
- " * @constraint minLength(1)",
882
- " */",
883
- " speech_id: string;",
884
- " /**",
885
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
886
- " * @constraint min(-60)",
887
- " * @constraint max(20)",
888
- " */",
889
- " volume: number;",
890
- " }[];",
548
+ "export interface EntityStoreSnapshot {",
549
+ " revision: number;",
550
+ " entities: SandboxEntity[];",
551
+ " relations: SandboxRelation[];",
891
552
  "}",
892
- "",
893
- "/**",
894
- " * Toggle caption visibility (the caption track's `is_hidden` flag).",
895
- " */",
896
- "export interface SetCaptionVisibilityInput {",
897
- " /**",
898
- " * Whether the caption track is hidden",
899
- " */",
900
- " is_hidden: boolean;",
553
+ "export interface ImageMediaAssetFact {",
554
+ " readonly assetId: string;",
555
+ " readonly kind: 'image';",
556
+ " readonly storageKey?: string;",
901
557
  "}",
902
- "",
903
- "/**",
904
- " * Set the caption visual style.",
905
- " */",
906
- "export interface SetCaptionStyleInput {",
907
- " /**",
908
- " * Font ID referencing a font from the font library",
909
- " * @constraint minLength(1)",
910
- " */",
911
- " font_id?: string;",
912
- " /**",
913
- " * Font size in points",
914
- " * @constraint positive",
915
- " */",
916
- " font_size?: number;",
917
- " /**",
918
- " * Font color as hex string, e.g. \"#FFFFFF\"",
919
- " * @constraint minLength(1)",
920
- " */",
921
- " font_color?: string;",
922
- " /**",
923
- " * Numeric font weight, e.g. 400 or 700",
924
- " * @constraint int",
925
- " */",
926
- " font_weight?: number;",
927
- " /**",
928
- " * Entrance animation preset ID, e.g. \"fade\" or \"none\"",
929
- " */",
930
- " entrance_animation?: string;",
931
- " /**",
932
- " * Entrance animation duration in ms",
933
- " * @constraint min(0)",
934
- " */",
935
- " entrance_animation_duration_ms?: number;",
936
- " /**",
937
- " * Outline/stroke color as hex string, e.g. \"#000000\"",
938
- " * @constraint minLength(1)",
939
- " */",
940
- " stroke_color?: string;",
941
- " /**",
942
- " * Outline/stroke width in pixels",
943
- " * @constraint min(0)",
944
- " */",
945
- " stroke_width?: number;",
946
- " /**",
947
- " * Caption center X as a fraction (0.0 to 1.0)",
948
- " */",
949
- " position_x?: number;",
950
- " /**",
951
- " * Caption center Y as a fraction (0.0 to 1.0)",
952
- " */",
953
- " position_y?: number;",
558
+ "export interface ImportAssetInput {",
559
+ " asset_id: string;",
560
+ " entity_id?: string;",
561
+ " payload?: JsonObject;",
954
562
  "}",
955
- "",
956
- "/**",
957
- " * Set the document BGM.",
958
- " */",
959
- "export interface SetBgmInput {",
960
- " /**",
961
- " * The bgm part ID to write",
962
- " * @constraint minLength(1)",
963
- " */",
964
- " bgm_id: string;",
965
- " /**",
966
- " * @constraint minLength(1)",
967
- " */",
968
- " audio_storage_key: string;",
969
- " /**",
970
- " * The media asset ID",
971
- " * @constraint minLength(1)",
972
- " */",
973
- " origin_media_id: string;",
974
- " /**",
975
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
976
- " * @constraint min(-60)",
977
- " * @constraint max(20)",
978
- " */",
979
- " volume: number;",
563
+ "export interface InsertClipInput {",
564
+ " readonly trackEntityId: string;",
565
+ " /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
566
+ " readonly contentEntityId: string;",
567
+ " readonly sourceRange: SequenceRange<number>;",
568
+ " readonly duration: SequenceDuration<number>;",
569
+ " readonly targetRange?: SequenceRange<number>;",
570
+ " readonly clipPayload?: JsonObject;",
980
571
  "}",
981
- "",
982
- "/**",
983
- " * Remove the document BGM.",
984
- " */",
985
- "export interface DeleteBgmInput {",
986
- " [key: string]: never;",
572
+ "export interface InsertMediaClipInput {",
573
+ " readonly timelineEntityId: string;",
574
+ " readonly clipEntityId?: string;",
575
+ " readonly media: VisualMediaAssetFact;",
576
+ " /** Source/display window in whole milliseconds. Images use this as their finite display span. */",
577
+ " readonly sourceRange: SequenceRange<number>;",
578
+ " readonly placement: ClipPlacement;",
579
+ " readonly volume?: number;",
987
580
  "}",
988
- "",
989
- "export interface AdjustBgmVolumeInput {",
990
- " /**",
991
- " * List of bgm parts with their new volume settings",
992
- " * @constraint minLength(1)",
993
- " */",
994
- " bgm: {",
995
- " /**",
996
- " * The bgm part ID to adjust volume for",
997
- " * @constraint minLength(1)",
998
- " */",
999
- " bgm_id: string;",
1000
- " /**",
1001
- " * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
1002
- " * @constraint min(-60)",
1003
- " * @constraint max(20)",
1004
- " */",
1005
- " volume: number;",
1006
- " }[];",
581
+ "export interface InsertMediaClipsInput {",
582
+ " readonly timelineEntityId: string;",
583
+ " readonly clips: readonly ReplacementMediaClipInput[];",
584
+ " /** One placement decision for the whole input-ordered block. */",
585
+ " readonly insertion: MediaClipInsertion;",
1007
586
  "}",
1008
- "",
1009
- "/** Agent write surface — one method per SemanticOp kind. */",
1010
- "export interface EditApi {",
1011
- " moveVideoClips(input: MoveVideoClipsInput): Promise<void>;",
1012
- " /** Reorder a set of main-track clips relative to a reference clip. */",
1013
- " moveVideoClipsByAnchor(input: MoveVideoClipsByAnchorInput): Promise<void>;",
1014
- " deleteVideoClips(input: DeleteVideoClipsInput): Promise<void>;",
1015
- " /** Add video clips to a track. */",
1016
- " addVideoClips(input: AddVideoClipsInput): Promise<void>;",
1017
- " adjustVideoClipVolume(input: AdjustVideoClipVolumeInput): Promise<void>;",
1018
- " /** Set the playback speed of existing video clips. */",
1019
- " setVideoClipSpeedShift(input: SetVideoClipSpeedShiftInput): Promise<void>;",
1020
- " /** Replace the media backing existing video clips. */",
1021
- " replaceVideoClipContent(input: ReplaceVideoClipContentInput): Promise<void>;",
1022
- " /** Replace a contiguous run of main-track clips with a new run. */",
1023
- " replaceVideoClipSequence(input: ReplaceVideoClipSequenceInput): Promise<void>;",
1024
- " /** Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window). */",
1025
- " adjustVideoClipDuration(input: AdjustVideoClipDurationInput): Promise<void>;",
1026
- " /** Add speeches (and their captions). */",
1027
- " addSpeeches(input: AddSpeechesInput): Promise<void>;",
1028
- " /** Delete speeches with their captions. */",
1029
- " deleteSpeeches(input: DeleteSpeechesInput): Promise<void>;",
1030
- " /** Move speeches in time. */",
1031
- " moveSpeeches(input: MoveSpeechesInput): Promise<void>;",
1032
- " /** Change a speech's script or voice. */",
1033
- " changeSpeechScript(input: ChangeSpeechScriptInput): Promise<void>;",
1034
- " changeSpeechVoice(input: ChangeSpeechVoiceInput): Promise<void>;",
1035
- " adjustSpeechVolume(input: AdjustSpeechVolumeInput): Promise<void>;",
1036
- " /** Toggle caption visibility (the caption track's `is_hidden` flag). */",
1037
- " setCaptionVisibility(input: SetCaptionVisibilityInput): Promise<void>;",
1038
- " /** Set the caption visual style. */",
1039
- " setCaptionStyle(input: SetCaptionStyleInput): Promise<void>;",
1040
- " /** Set the document BGM. */",
1041
- " setBgm(input: SetBgmInput): Promise<void>;",
1042
- " /** Remove the document BGM. */",
1043
- " deleteBgm(input: DeleteBgmInput): Promise<void>;",
1044
- " adjustBgmVolume(input: AdjustBgmVolumeInput): Promise<void>;",
587
+ "export interface InsertPlacedClipInput {",
588
+ " readonly trackEntityId: string;",
589
+ " readonly contentEntityId: string;",
590
+ " readonly sourceRange: SequenceRange<number>;",
591
+ " readonly duration: SequenceDuration<number>;",
592
+ " readonly placement: ClipPlacement;",
593
+ " readonly clipPayload?: JsonObject;",
594
+ " /** Stable caller-owned placement identity, when one already exists outside the graph. */",
595
+ " readonly clipEntityId?: string;",
1045
596
  "}",
1046
- "",
1047
- "export type JsonPrimitive = string | number | boolean | null;",
1048
- "export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
1049
597
  "export interface JsonObject {",
1050
598
  " [key: string]: JsonValue;",
1051
599
  "}",
1052
- "",
600
+ "export type JsonPrimitive = string | number | boolean | null;",
601
+ "export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
1053
602
  "export type KnownEntityKind =",
1054
603
  " | 'axvideo'",
1055
604
  " | 'timeline'",
@@ -1065,7 +614,6 @@ const EDIT_SANDBOX_API_DTS = [
1065
614
  " | 'audio-script'",
1066
615
  " | 'phonetic-script'",
1067
616
  " | 'caption';",
1068
- "",
1069
617
  "export type KnownRelationKind =",
1070
618
  " | 'timeline-track'",
1071
619
  " | 'track-clip'",
@@ -1077,72 +625,146 @@ const EDIT_SANDBOX_API_DTS = [
1077
625
  " | 'generated'",
1078
626
  " | 'phonetic-script-provenance'",
1079
627
  " | 'caption-provenance'",
1080
- " | 'caption-alignment';",
1081
- "",
1082
- "export interface BoundedNativeSequencePayload extends JsonObject {",
1083
- " /** Use factual recalled coordinates; never invent an end or duration. */",
1084
- " extent: { kind: 'bounded'; start: number; end: number };",
1085
- " sampling: 'native';",
1086
- " coordinateSpace: JsonValue;",
628
+ " | 'caption-alignment'",
629
+ " | 'clip-anchor'",
630
+ " | 'audio-script-render';",
631
+ "export interface LinearClipSpeed {",
632
+ " readonly kind: 'linear';",
633
+ " readonly rate: number;",
634
+ " readonly mode?: string;",
1087
635
  "}",
1088
- "export interface UnboundedConstantSequencePayload extends JsonObject {",
1089
- " extent: { kind: 'unbounded'; start: number };",
1090
- " sampling: 'constant';",
1091
- " coordinateSpace: JsonValue;",
636
+ "export interface LinkAudioScriptRenderRelationInput {",
637
+ " relation_id?: string;",
638
+ " output_entity_id: string;",
639
+ " script_entity_id: string;",
640
+ " trace?: JsonObject;",
1092
641
  "}",
1093
- "export interface BoundedDerivedSequencePayload extends JsonObject {",
1094
- " extent: { kind: 'bounded'; start: number; end: number };",
1095
- " sampling: 'derived';",
1096
- " coordinateSpace: JsonValue;",
642
+ "export interface LinkClipAnchorRelationInput {",
643
+ " relation_id?: string;",
644
+ " child_clip_entity_id: string;",
645
+ " host_clip_entity_id: string;",
646
+ " trace?: JsonObject;",
1097
647
  "}",
1098
- "export type ScriptTextSegment = JsonObject & {",
1099
- " segmentId: string;",
1100
- " text: string;",
1101
- " language?: string;",
1102
- "};",
1103
- "",
1104
- "export interface EntityPayloadByKind {",
1105
- " axvideo: BoundedDerivedSequencePayload;",
1106
- " timeline: JsonObject;",
1107
- " track: JsonObject & { hidden?: boolean; role?: string };",
1108
- " clip: JsonObject;",
1109
- " asset: JsonObject;",
1110
- " video: BoundedNativeSequencePayload;",
1111
- " audio: BoundedNativeSequencePayload;",
1112
- " voice: BoundedNativeSequencePayload;",
1113
- " image: UnboundedConstantSequencePayload;",
1114
- " 'sequence-marker': JsonObject & {",
1115
- " sourceRange: { start: number; end: number };",
1116
- " targetRange?: { start: number; end: number };",
1117
- " duration: { mode: 'from-source' } | { mode: 'fixed'; value: number };",
1118
- " timeRemapping?: JsonValue;",
1119
- " };",
1120
- " viewport: JsonObject;",
1121
- " 'audio-script': JsonObject & { segments: ScriptTextSegment[] };",
1122
- " 'phonetic-script': JsonObject & { segments: ScriptTextSegment[] };",
1123
- " caption: BoundedNativeSequencePayload;",
648
+ "export interface LinkGeneratedRelationInput {",
649
+ " relation_id?: string;",
650
+ " output_entity_id: string;",
651
+ " input_entity_id: string;",
652
+ " trace?: JsonObject;",
1124
653
  "}",
1125
- "",
1126
- "export type CreateEntityInput = {",
1127
- " [K in KnownEntityKind]: {",
1128
- " entity_id?: string;",
1129
- " entity_kind: K;",
1130
- " payload: EntityPayloadByKind[K];",
1131
- " };",
1132
- "}[KnownEntityKind];",
1133
- "",
1134
- "export interface ImportAssetInput {",
1135
- " asset_id: string;",
1136
- " entity_id?: string;",
1137
- " payload?: JsonObject;",
654
+ "interface LinkRelationBase {",
655
+ " relation_id?: string;",
656
+ " endpoint_0_entity_id: string;",
657
+ " endpoint_1_entity_id: string;",
658
+ " trace?: JsonObject;",
659
+ "}",
660
+ "export type LinkRelationInput =",
661
+ " | (LinkRelationBase & {",
662
+ " relation_kind: EmptyRelationKind;",
663
+ " metadata?: {",
664
+ " [key: string]: never;",
665
+ " };",
666
+ " })",
667
+ " | (LinkRelationBase & {",
668
+ " relation_kind: 'physical-asset';",
669
+ " metadata?: JsonObject;",
670
+ " })",
671
+ " | (LinkRelationBase & {",
672
+ " relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
673
+ " metadata: JsonObject & {",
674
+ " segmentAlignment: JsonValue;",
675
+ " };",
676
+ " })",
677
+ " | (LinkRelationBase & {",
678
+ " relation_kind: 'caption-alignment';",
679
+ " metadata: JsonObject & {",
680
+ " alignment: JsonValue;",
681
+ " };",
682
+ " });",
683
+ "export type MediaClipInsertion =",
684
+ " | {",
685
+ " readonly kind: 'before';",
686
+ " readonly clipEntityId: string;",
687
+ " }",
688
+ " | {",
689
+ " readonly kind: 'after';",
690
+ " readonly clipEntityId: string;",
691
+ " }",
692
+ " | {",
693
+ " readonly kind: 'firstStart';",
694
+ " readonly startMs: number;",
695
+ " };",
696
+ "export interface MoveClipInput {",
697
+ " readonly clipEntityId: string;",
698
+ " readonly trackEntityId: string;",
699
+ "}",
700
+ "export interface MoveClipsToStartsInput {",
701
+ " readonly moves: readonly {",
702
+ " readonly clipEntityId: string;",
703
+ " readonly newStartMs: number;",
704
+ " }[];",
705
+ " /** Absolute-time drags preserve every voiceover's current visible landing. */",
706
+ " readonly onAnchored: 'keepAbsolute';",
707
+ "}",
708
+ "export interface MoveSequentialClipsInput {",
709
+ " readonly clipEntityIds: readonly string[];",
710
+ " readonly anchor: SequentialClipAnchor;",
711
+ " readonly onAnchored: 'follow' | 'keepAbsolute';",
712
+ "}",
713
+ "export interface MoveVoiceoverInput {",
714
+ " readonly voiceoverClipEntityId: string;",
715
+ " /** Absolute requested timeline start; MEngine resolves and persists the host relation. */",
716
+ " readonly newStartMs: number;",
717
+ "}",
718
+ "export interface PatchCaptionStyleInput {",
719
+ " readonly timelineEntityId: string;",
720
+ " readonly style: CaptionStyleFields;",
721
+ "}",
722
+ "export interface RelationFacade {",
723
+ " list(): SandboxRelation[];",
724
+ " /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
725
+ " of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
726
+ " /** For physical-asset use sequence media as endpoint 0 and Asset as endpoint 1. */",
727
+ " link(input: LinkRelationInput): string;",
728
+ " /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
729
+ " linkGenerated(input: LinkGeneratedRelationInput): string;",
730
+ " /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
731
+ " linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
732
+ " /** Author ordered audio-script-render(output,script) without positional endpoint ambiguity. */",
733
+ " linkAudioScriptRender(input: LinkAudioScriptRenderRelationInput): string;",
734
+ " /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
735
+ " unlink(input: UnlinkRelationInput): void;",
736
+ "}",
737
+ "export interface ReplaceClipContentInput {",
738
+ " readonly clipEntityId: string;",
739
+ " /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
740
+ " readonly contentEntityId: string;",
741
+ " readonly sourceRange: SequenceRange<number>;",
742
+ " readonly duration: SequenceDuration<number>;",
743
+ " readonly targetRange?: SequenceRange<number>;",
744
+ " readonly timeRemapping?: JsonValue;",
745
+ "}",
746
+ "export interface ReplaceMediaClipInput {",
747
+ " readonly clipEntityId: string;",
748
+ " readonly media: VisualMediaAssetFact;",
749
+ " readonly sourceRange: SequenceRange<number>;",
750
+ "}",
751
+ "export interface ReplaceSequentialClipsInput {",
752
+ " readonly timelineEntityId: string;",
753
+ " readonly oldClipEntityIds: readonly string[];",
754
+ " readonly newClips: readonly ReplacementMediaClipInput[];",
755
+ " readonly onAnchored: 'remap' | 'cascade';",
756
+ "}",
757
+ "export interface ReplacementMediaClipInput {",
758
+ " readonly clipEntityId?: string;",
759
+ " readonly media: VisualMediaAssetFact;",
760
+ " readonly sourceRange: SequenceRange<number>;",
761
+ " readonly volume?: number;",
1138
762
  "}",
1139
- "",
1140
763
  "export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
1141
764
  " entity_id: string;",
1142
765
  " entity_kind: K;",
1143
766
  " payload: EntityPayloadByKind[K];",
1144
767
  "}",
1145
- "",
1146
768
  "export interface SandboxRelation {",
1147
769
  " relation_id: string;",
1148
770
  " relation_kind: KnownRelationKind;",
@@ -1151,155 +773,201 @@ const EDIT_SANDBOX_API_DTS = [
1151
773
  " metadata: JsonObject;",
1152
774
  " trace: JsonObject;",
1153
775
  "}",
1154
- "",
1155
- "export type EmptyRelationKind =",
1156
- " | 'timeline-track'",
1157
- " | 'track-clip'",
1158
- " | 'clip-marker'",
1159
- " | 'marker-content'",
1160
- " | 'axvideo-marker'",
1161
- " | 'marker-timeline';",
1162
- "export type LinkRelationInput =",
776
+ "export type ScriptTextSegment = JsonObject & {",
777
+ " segmentId: string;",
778
+ " text: string;",
779
+ " language?: string;",
780
+ "};",
781
+ "export type SequenceDuration<Span = unknown> =",
1163
782
  " | {",
1164
- " relation_id?: string;",
1165
- " relation_kind: EmptyRelationKind;",
1166
- " endpoint_0_entity_id: string;",
1167
- " endpoint_1_entity_id: string;",
1168
- " metadata?: { [key: string]: never };",
1169
- " trace?: JsonObject;",
783
+ " readonly mode: 'from-source';",
1170
784
  " }",
1171
785
  " | {",
1172
- " relation_id?: string;",
1173
- " relation_kind: 'physical-asset';",
1174
- " /** Canonical endpoint 0 is sequence media; endpoint 1 is Asset. */",
1175
- " endpoint_0_entity_id: string;",
1176
- " endpoint_1_entity_id: string;",
1177
- " metadata?: JsonObject;",
1178
- " trace?: JsonObject;",
1179
- " }",
786
+ " readonly mode: 'fixed';",
787
+ " readonly value: Span;",
788
+ " };",
789
+ "export interface SequenceRange<Point = unknown> {",
790
+ " readonly start: Point;",
791
+ " readonly end: Point;",
792
+ "}",
793
+ "export type SequentialClipAnchor =",
1180
794
  " | {",
1181
- " relation_id?: string;",
1182
- " relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
1183
- " endpoint_0_entity_id: string;",
1184
- " endpoint_1_entity_id: string;",
1185
- " metadata: JsonObject & { segmentAlignment: JsonValue };",
1186
- " trace?: JsonObject;",
795
+ " readonly position: 'before' | 'after';",
796
+ " readonly clipEntityId: string;",
1187
797
  " }",
1188
798
  " | {",
1189
- " relation_id?: string;",
1190
- " relation_kind: 'caption-alignment';",
1191
- " endpoint_0_entity_id: string;",
1192
- " endpoint_1_entity_id: string;",
1193
- " metadata: JsonObject & { alignment: JsonValue };",
1194
- " trace?: JsonObject;",
799
+ " readonly position: 'trackStart';",
1195
800
  " };",
1196
- "",
1197
- "export interface LinkGeneratedRelationInput {",
1198
- " relation_id?: string;",
1199
- " /** Generated output media Entity; persisted as endpoint 0. */",
1200
- " output_entity_id: string;",
1201
- " /** Input media Entity used to generate the output; persisted as endpoint 1. */",
1202
- " input_entity_id: string;",
1203
- " trace?: JsonObject;",
801
+ "export interface SetBgmInput {",
802
+ " readonly timelineEntityId: string;",
803
+ " readonly bgmClipEntityId: string;",
804
+ " readonly media: AudioMediaAssetFact;",
805
+ " readonly volume: number;",
1204
806
  "}",
1205
- "",
1206
- "/** Explicit Entity authoring. Assets and media Entities are not one-to-one. */",
1207
- "export interface EntityApi {",
1208
- " list(): SandboxEntity[];",
1209
- " get(entityId: string): SandboxEntity | null;",
1210
- " /** Call before importAsset; inspect every match and decide whether to reuse one. */",
1211
- " findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
1212
- " create(input: CreateEntityInput): string;",
1213
- " /** Create only an Asset Entity when no existing match should be reused; this does not infer media. */",
1214
- " importAsset(input: ImportAssetInput): string;",
807
+ "export interface SetCaptionVisibilityInput {",
808
+ " readonly timelineEntityId: string;",
809
+ " readonly hidden: boolean;",
1215
810
  "}",
1216
- "",
1217
- "/** Incident reads ignore endpoint position; relation semantics preserve it. */",
1218
- "export interface RelationApi {",
1219
- " list(): SandboxRelation[];",
1220
- " of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
1221
- " link(input: LinkRelationInput): string;",
1222
- " /** Author ordered generated(output,input). */",
1223
- " linkGenerated(input: LinkGeneratedRelationInput): string;",
811
+ "export interface SetClipPlacementInput {",
812
+ " readonly clipEntityId: string;",
813
+ " readonly placement: ClipPlacement;",
1224
814
  "}",
1225
- "",
1226
- "/** Clip hit from `clipsInRange`. */",
1227
- "export interface TimelineClipDescriptor {",
1228
- " id: string;",
1229
- " start_ms: number;",
1230
- " end_ms: number;",
1231
- " duration_ms: number;",
1232
- " speed_shift: unknown;",
1233
- " volume: number | undefined;",
1234
- " media_id: string | undefined;",
815
+ "export interface SetClipSpeedInput {",
816
+ " readonly clipEntityId: string;",
817
+ " readonly timeRemapping: LinearClipSpeed | null;",
1235
818
  "}",
1236
- "",
1237
- "/** Part descriptor from `part(id)`. */",
1238
- "export interface TimelinePartDescriptor {",
1239
- " id: string;",
1240
- " kind: string;",
1241
- " lane: string;",
1242
- " start_ms: number;",
1243
- " end_ms: number;",
1244
- " duration_ms: number;",
1245
- " part: unknown;",
819
+ "export interface SetClipVolumeInput {",
820
+ " readonly clipEntityId: string;",
821
+ " /** Playback gain in decibels. */",
822
+ " readonly volume: number;",
823
+ "}",
824
+ "export interface TrimClipInput {",
825
+ " readonly clipEntityId: string;",
826
+ " readonly sourceRange: SequenceRange<number>;",
827
+ "}",
828
+ "export interface UnboundedConstantSequencePayload extends JsonObject {",
829
+ " extent: {",
830
+ " kind: 'unbounded';",
831
+ " start: number;",
832
+ " };",
833
+ " sampling: 'constant';",
834
+ " coordinateSpace: JsonValue;",
835
+ "}",
836
+ "export interface UnlinkRelationInput {",
837
+ " relation_id: string;",
838
+ "}",
839
+ "export interface UpdateClipInput {",
840
+ " readonly clipEntityId: string;",
841
+ " /** Complete replacement for the Clip-owned payload. */",
842
+ " readonly payload: JsonObject;",
843
+ "}",
844
+ "export interface UpdateClipMarkerInput {",
845
+ " readonly clipEntityId: string;",
846
+ " readonly sourceRange?: SequenceRange<number>;",
847
+ " /** Passing `undefined` explicitly removes the optional target range. */",
848
+ " readonly targetRange?: SequenceRange<number> | undefined;",
849
+ " readonly duration?: SequenceDuration<number>;",
850
+ " /** Passing `undefined` explicitly removes the optional remapping value. */",
851
+ " readonly timeRemapping?: JsonValue | undefined;",
852
+ "}",
853
+ "export interface UpdateEntityInput {",
854
+ " entity_id: string;",
855
+ " payload: JsonObject;",
856
+ "}",
857
+ "export interface VideoMediaAssetFact {",
858
+ " readonly assetId: string;",
859
+ " readonly kind: 'video';",
860
+ " readonly durationMs: number;",
861
+ " readonly storageKey?: string;",
862
+ "}",
863
+ "export type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;",
864
+ "export interface VoiceDescriptor {",
865
+ " readonly system: 'voice-library';",
866
+ " readonly key: string;",
867
+ " readonly name?: string;",
868
+ "}",
869
+ "export interface VoiceMediaAssetFact {",
870
+ " /** Stable external speech result id, independent of the placed Clip id. */",
871
+ " readonly assetId: string;",
872
+ " readonly kind: 'voice';",
873
+ " readonly durationMs: number;",
874
+ " readonly storageKey: string;",
875
+ " readonly voice: VoiceDescriptor;",
876
+ "}",
877
+ "export interface VoiceoverCaptionFact {",
878
+ " /** Stable placed caption identity supplied by the materialized side effect. */",
879
+ " readonly captionClipEntityId: string;",
880
+ " readonly text: string;",
881
+ " readonly startMs: number;",
882
+ " readonly durationMs: number;",
883
+ " readonly style?: CaptionStyleFields;",
884
+ "}",
885
+ "export interface VoiceoverTakeInput {",
886
+ " readonly timelineEntityId: string;",
887
+ " /** Stable placed speech identity, distinct from media.assetId. */",
888
+ " readonly voiceoverClipEntityId: string;",
889
+ " readonly hostClipEntityId: string;",
890
+ " readonly anchorOffset: number;",
891
+ " readonly media: VoiceMediaAssetFact;",
892
+ " /** Complete spoken text; the editor owns the deterministic local script segment identity. */",
893
+ " readonly scriptText: string;",
894
+ " readonly volume: number;",
895
+ " readonly captions: readonly VoiceoverCaptionFact[];",
896
+ "}",
897
+ "export interface VoiceoverTakeResult {",
898
+ " readonly voiceoverClipEntityId: string;",
899
+ " readonly voiceEntityId: string;",
900
+ " readonly audioScriptEntityId: string;",
901
+ " readonly captionClipEntityIds: readonly string[];",
902
+ "}",
903
+ "/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
904
+ "export interface EditApi {",
905
+ " insertClip(input: InsertClipInput): ClipEntityId;",
906
+ " insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
907
+ " updateClipMarker(input: UpdateClipMarkerInput): void;",
908
+ " setClipPlacement(input: SetClipPlacementInput): void;",
909
+ " moveSequentialClips(input: MoveSequentialClipsInput): void;",
910
+ " moveClip(input: MoveClipInput): void;",
911
+ " replaceClipContent(input: ReplaceClipContentInput): void;",
912
+ " insertMediaClip(input: InsertMediaClipInput): ClipEntityId;",
913
+ " insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];",
914
+ " replaceMediaClip(input: ReplaceMediaClipInput): void;",
915
+ " setClipVolume(input: SetClipVolumeInput): void;",
916
+ " setClipSpeed(input: SetClipSpeedInput): void;",
917
+ " trimClip(input: TrimClipInput): void;",
918
+ " replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];",
919
+ " deleteClip(input: DeleteClipInput): void;",
920
+ " deleteClipTree(input: DeleteClipTreeInput): void;",
921
+ " updateClip(input: UpdateClipInput): void;",
922
+ " upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;",
923
+ " moveVoiceover(input: MoveVoiceoverInput): void;",
924
+ " moveClipsToStarts(input: MoveClipsToStartsInput): void;",
925
+ " deleteVoiceover(input: DeleteVoiceoverInput): void;",
926
+ " setBgm(input: SetBgmInput): ClipEntityId;",
927
+ " deleteBgm(input: DeleteBgmInput): void;",
928
+ " setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
929
+ " patchCaptionStyle(input: PatchCaptionStyleInput): void;",
1246
930
  "}",
1247
- "",
1248
- "/** Opaque VideoDraft projection (full IDL lives in host document types). */",
1249
- "export type VideoDraftProjection = {",
1250
- " readonly timeline?: { readonly duration_ms?: number };",
1251
- " readonly [key: string]: unknown;",
1252
- "};",
1253
- "",
1254
- "/** Agent read surface over the forked document. */",
1255
931
  "export interface TimelineApi {",
1256
- " /** Snapshot the current VideoDraft projection. */",
1257
- " snapshot(): VideoDraftProjection;",
1258
- " /** Clips whose midpoint falls in `[startMs, endMs)`. */",
1259
- " clipsInRange(startMs: number, endMs: number): TimelineClipDescriptor[];",
1260
- " /** Look up a part by id, or null if missing. */",
1261
- " part(id: string): TimelinePartDescriptor | null;",
932
+ " snapshot(): EntityStoreSnapshot;",
1262
933
  "}",
1263
- "",
1264
- "/** Opaque checkpoint handle for rollback. */",
1265
934
  "export interface SandboxCheckpoint {",
1266
935
  " readonly index: number;",
1267
936
  "}",
1268
- "",
1269
937
  "export declare const edit: EditApi;",
1270
938
  "export declare const timeline: TimelineApi;",
1271
- "export declare const entities: EntityApi;",
1272
- "export declare const relations: RelationApi;",
1273
- "",
1274
- "/** Capture a rollback point. */",
939
+ "export declare const entities: EntityFacade;",
940
+ "export declare const relations: RelationFacade;",
1275
941
  "export declare function checkpoint(): SandboxCheckpoint;",
1276
- "/** Roll the sandbox document back to a prior checkpoint. */",
1277
942
  "export declare function rollbackTo(cp: SandboxCheckpoint): void;",
1278
- "/** Host-injected, pre-materialized facts. Validate each field before use. */",
1279
943
  "export declare const inputs: Readonly<Record<string, unknown>>;",
1280
944
  ""
1281
945
  ].join("\n");
1282
946
  //#endregion
1283
947
  //#region src/prompt.ts
1284
948
  const MEDEO_TOOL_DESCRIPTION = `
1285
- Edit a Medeo video document and its explicit Entity/Relation state through a deterministic, side-effect-free JavaScript sandbox.
949
+ Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
1286
950
 
1287
951
  Operations:
1288
- - snapshot: return the compact timeline projection and opaque base version.
1289
- - run-edit-script: execute JavaScript against forked timeline and Entity/Relation snapshots. Inspect timeline.*, entities.*, and relations.*; call edit.* for timeline mutations or the explicit entity APIs for domain mutations. The sandbox has no network, storage, clock, or generation access. Pass recalled generation/asset facts through inputs. A successful run returns preview, logs, plan_kind, base versions, and plan_id — not the full journals.
1290
- - commit-plan: commit a cached plan_id. Timeline plans replay into ManualSyncDoc and push one causally complete update; Entity plans replace the authoritative row set through revision CAS. validation=preflight is timeline-only. A failed transport is unconfirmed, never committed; retry the same plan_id.
952
+ - snapshot: return the Entity/Relation state summary and opaque base version.
953
+ - migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
954
+ - run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation, generated Relations and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled generation/asset facts through inputs. A successful run returns preview, logs, base revision and plan_id.
955
+ - commit-plan: commit the complete Entity/Relation plan through revision CAS. The server derives the read-only timeline projection in the same transaction. There is no separate writable timeline plan and no preflight replay into a legacy editor. A failed transport is unconfirmed, never committed; retry the same plan_id.
1291
956
 
1292
957
  Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
1293
958
 
1294
- One plan must mutate exactly one store: timeline or Entity/Relation state. If both are needed, author and commit two separate plans. There is no automatic Asset→Entity projection: select the relevant recalled fact, explicitly import an Asset if useful, explicitly create only known typed Entities, and author relations. Asset and media Entity identity are not one-to-one. relations.linkGenerated({ output_entity_id, input_entity_id }) means generated(output,input); incident lookup with relations.of(entityId) is endpoint-agnostic.
959
+ Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, reuse or create the appropriate media Entity and physical-asset Relation, then pass the media Entity id to edit.insertClip. A raw asset id or URL is not valid contentEntityId. Asset and media Entity identity are not one-to-one. Recall generation history and author known generated(output,input) relations in the same plan; do not invent an input for text-only generation. relations.of(entityId) is endpoint-agnostic.
1295
960
  `.trim();
1296
961
  const MEDEO_TOOL_EXECUTION_RULES = `
1297
962
  The host supplies the current document. Do not ask for, invent, or pass a document id.
1298
- Use timeline.snapshot() for the whole draft projection. Its duration is timeline.snapshot().timeline?.duration_ms; there is no top-level duration_ms.
963
+ timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
1299
964
  Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
1300
965
  Before importing an Asset, call entities.findByAssetId(assetId), inspect every match, and decide whether an existing Entity represents the intended logical asset. Multiple matches are valid; do not assume Asset↔media is one-to-one.
1301
966
  For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
1302
967
  For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. For generated lineage, use linkGenerated so endpoint 0 is output and endpoint 1 is input. relations.of remains endpoint-agnostic for lookup.
968
+ The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
969
+ Voice links to AudioScript through audio-script-render(output,script). Caption owns text/style and is placed through a Clip anchored to the Voice Clip; caption alignment/provenance agree with that Voice/AudioScript. BGM Audio keeps factual source duration and its Marker declares durationPolicy:'timeline'. External Asset identity and storageKey are distinct from the placed Clip identity. Never introduce a speech entity kind.
970
+ Create only the known entity kinds. On an empty document, explicitly create Timeline and Track(role='video_clip') and connect timeline-track before inserting a Clip. If snapshot reports legacy migration is required, recall the listed asset facts and call migrate-legacy first. Missing facts, unsupported layouts, and version conflicts fail closed; never fall back to an old timeline method or raw update endpoint.
1303
971
  Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
1304
972
  `.trim();
1305
973
  /** Render the complete MEngine-owned context injected before one model call. */
@@ -1318,15 +986,51 @@ When updated_since_previous_model_call is true, the document changed after the p
1318
986
 
1319
987
  Sandbox TypeScript interface:
1320
988
  \`\`\`ts
1321
- ${EDIT_SANDBOX_API_DTS}
989
+ ${ENTITY_EDIT_SANDBOX_API_DTS}
1322
990
  \`\`\`
1323
991
  `.trim();
1324
992
  }
1325
993
  //#endregion
1326
994
  //#region src/schema.ts
1327
995
  const MEDEO_TOOL_NAME = "medeo";
996
+ const assetFactProperties = {
997
+ assetId: {
998
+ type: "string",
999
+ minLength: 1
1000
+ },
1001
+ kind: {
1002
+ type: "string",
1003
+ enum: [
1004
+ "image",
1005
+ "video",
1006
+ "audio",
1007
+ "voice"
1008
+ ]
1009
+ },
1010
+ durationMs: {
1011
+ type: "integer",
1012
+ minimum: 1
1013
+ },
1014
+ storageKey: {
1015
+ type: "string",
1016
+ minLength: 1
1017
+ },
1018
+ voice: {
1019
+ type: "object",
1020
+ additionalProperties: false,
1021
+ required: ["system", "key"],
1022
+ properties: {
1023
+ system: { const: "voice-library" },
1024
+ key: {
1025
+ type: "string",
1026
+ minLength: 1
1027
+ },
1028
+ name: { type: "string" }
1029
+ }
1030
+ }
1031
+ };
1328
1032
  /**
1329
- * JSON Schema for the host-facing three-op `medeo` tool surface.
1033
+ * JSON Schema for the host-facing `medeo` tool surface.
1330
1034
  *
1331
1035
  * The schema intentionally does not return or accept the full op journal:
1332
1036
  * journals stay in the tool process and are referenced by `plan_id`. This keeps
@@ -1342,6 +1046,7 @@ const MEDEO_TOOL_PARAMETERS = {
1342
1046
  type: "string",
1343
1047
  enum: [
1344
1048
  "snapshot",
1049
+ "migrate-legacy",
1345
1050
  "run-edit-script",
1346
1051
  "commit-plan"
1347
1052
  ],
@@ -1355,12 +1060,74 @@ const MEDEO_TOOL_PARAMETERS = {
1355
1060
  script: {
1356
1061
  type: "string",
1357
1062
  minLength: 1,
1358
- description: "JavaScript body for run-edit-script. It receives edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. A plan may mutate the timeline or Entity/Relation state, never both."
1063
+ description: "JavaScript body for run-edit-script. Use edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. Asset import, media relations, and timeline entity edits share one entity plan."
1359
1064
  },
1360
1065
  inputs: {
1361
1066
  type: "object",
1362
1067
  description: "Pre-materialized, side-effect-free values passed into the script, including recalled generation lineage and asset facts. Generation and network IO must happen in the host before this call."
1363
1068
  },
1069
+ asset_facts: {
1070
+ type: "array",
1071
+ description: "Factual asset metadata recalled by the host for migrate-legacy only. The package reads the canonical legacy snapshot and version itself; never supply a clip trim window as media duration.",
1072
+ items: { oneOf: [
1073
+ {
1074
+ type: "object",
1075
+ additionalProperties: false,
1076
+ required: ["assetId", "kind"],
1077
+ properties: {
1078
+ assetId: assetFactProperties.assetId,
1079
+ kind: { const: "image" },
1080
+ storageKey: assetFactProperties.storageKey
1081
+ }
1082
+ },
1083
+ {
1084
+ type: "object",
1085
+ additionalProperties: false,
1086
+ required: [
1087
+ "assetId",
1088
+ "kind",
1089
+ "durationMs"
1090
+ ],
1091
+ properties: {
1092
+ assetId: assetFactProperties.assetId,
1093
+ kind: { const: "video" },
1094
+ durationMs: assetFactProperties.durationMs,
1095
+ storageKey: assetFactProperties.storageKey
1096
+ }
1097
+ },
1098
+ {
1099
+ type: "object",
1100
+ additionalProperties: false,
1101
+ required: [
1102
+ "assetId",
1103
+ "kind",
1104
+ "durationMs",
1105
+ "storageKey"
1106
+ ],
1107
+ properties: {
1108
+ assetId: assetFactProperties.assetId,
1109
+ kind: { const: "audio" },
1110
+ durationMs: assetFactProperties.durationMs,
1111
+ storageKey: assetFactProperties.storageKey
1112
+ }
1113
+ },
1114
+ {
1115
+ type: "object",
1116
+ additionalProperties: false,
1117
+ required: [
1118
+ "assetId",
1119
+ "kind",
1120
+ "durationMs",
1121
+ "storageKey",
1122
+ "voice"
1123
+ ],
1124
+ properties: {
1125
+ ...assetFactProperties,
1126
+ kind: { const: "voice" }
1127
+ }
1128
+ }
1129
+ ] }
1130
+ },
1364
1131
  timeout_ms: {
1365
1132
  type: "integer",
1366
1133
  minimum: 1,
@@ -1382,11 +1149,24 @@ const MEDEO_TOOL_PARAMETERS = {
1382
1149
  },
1383
1150
  validation: {
1384
1151
  type: "string",
1385
- enum: ["version", "preflight"],
1386
- description: "Timeline commit mode: version rejects any concurrent change; preflight revalidates each op. Entity plans always use revision CAS and reject preflight."
1152
+ enum: ["version"],
1153
+ description: "Commit with Entity revision CAS; reject concurrent changes."
1387
1154
  }
1388
1155
  },
1389
1156
  oneOf: [
1157
+ {
1158
+ required: [
1159
+ "op",
1160
+ "doc_id",
1161
+ "asset_facts"
1162
+ ],
1163
+ properties: {
1164
+ op: { const: "migrate-legacy" },
1165
+ doc_id: { $ref: "#/properties/doc_id" },
1166
+ asset_facts: { $ref: "#/properties/asset_facts" }
1167
+ },
1168
+ additionalProperties: false
1169
+ },
1390
1170
  {
1391
1171
  required: ["op", "doc_id"],
1392
1172
  properties: {
@@ -1538,11 +1318,31 @@ function requiredContext(value, docId, field) {
1538
1318
  if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
1539
1319
  return resolved;
1540
1320
  }
1321
+ function renderEntitySnapshot(state) {
1322
+ const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
1323
+ const shown = rows.slice(0, 200);
1324
+ return [
1325
+ `Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
1326
+ ...shown,
1327
+ ...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
1328
+ ].join("\n");
1329
+ }
1330
+ function migrationNotice(document, state) {
1331
+ if (state.entities.some((row) => row.entity_kind === "timeline") || Object.keys(document.part_library ?? {}).length === 0) return "";
1332
+ const assetIds = new Set(Object.values(document.part_library ?? {}).flatMap((part) => {
1333
+ const id = part.video_clip?.origin_media_id ?? part.bgm?.origin_media_id;
1334
+ return typeof id === "string" && id !== "" ? [id] : [];
1335
+ }));
1336
+ return `\nLegacy timeline migration required. Recall factual media metadata for ${JSON.stringify([...assetIds])}, then call migrate-legacy with asset_facts. Speech facts are read from the canonical legacy document. Take a fresh snapshot after migration before editing.`;
1337
+ }
1541
1338
  async function commitEntityPlan(client, plan) {
1542
1339
  const rows = plan.entity_rows;
1543
1340
  if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
1544
1341
  try {
1545
- const committed = await client.commit(plan.entity_base_revision, rows);
1342
+ const committed = await client.commit(plan.entity_base_revision, rows, {
1343
+ deleted_entity_ids: plan.deleted_entity_ids ?? [],
1344
+ deleted_relation_ids: plan.deleted_relation_ids ?? []
1345
+ });
1546
1346
  return {
1547
1347
  kind: "committed",
1548
1348
  ops_applied: plan.entity_commands.length,
@@ -1551,7 +1351,7 @@ async function commitEntityPlan(client, plan) {
1551
1351
  };
1552
1352
  } catch (error) {
1553
1353
  if (error instanceof MengineEntityHttpRequestError) {
1554
- if (error.status === 409) {
1354
+ if (error.status === 409 && isRevisionConflictPayload(error.payload)) {
1555
1355
  const actualFromPayload = revisionConflictActual(error.payload);
1556
1356
  try {
1557
1357
  const current = await client.fetchState();
@@ -1602,6 +1402,9 @@ function revisionConflictActual(payload) {
1602
1402
  const actual = payload.actual_revision;
1603
1403
  return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
1604
1404
  }
1405
+ function isRevisionConflictPayload(payload) {
1406
+ return isRecord(payload) && payload.code === "revision_conflict";
1407
+ }
1605
1408
  function entityHttpErrorMessage(payload) {
1606
1409
  if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
1607
1410
  return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
@@ -1631,6 +1434,18 @@ function parseInput(value) {
1631
1434
  op,
1632
1435
  doc_id: docId
1633
1436
  };
1437
+ if (op === "migrate-legacy") {
1438
+ if (Object.keys(value).some((key) => ![
1439
+ "op",
1440
+ "doc_id",
1441
+ "asset_facts"
1442
+ ].includes(key))) throw new Error("migrate-legacy accepts asset_facts only; the package reads the canonical document and version");
1443
+ return {
1444
+ op,
1445
+ doc_id: docId,
1446
+ asset_facts: parseMigrationAssetFacts(value.asset_facts)
1447
+ };
1448
+ }
1634
1449
  if (op === "run-edit-script") {
1635
1450
  if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
1636
1451
  if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
@@ -1791,20 +1606,11 @@ function createMedeoTool(options) {
1791
1606
  if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
1792
1607
  }
1793
1608
  async function fetchEntityStateForSandbox(docId) {
1794
- try {
1795
- return await getEntityClient(docId).fetchState();
1796
- } catch (error) {
1797
- if (error instanceof MengineEntityHttpRequestError && error.status === 404) return {
1798
- revision: 0,
1799
- entities: [],
1800
- relations: []
1801
- };
1802
- throw error;
1803
- }
1609
+ return await getEntityClient(docId).fetchState();
1804
1610
  }
1805
- async function commitCachedPlan(docId, doc, plan, validation) {
1806
- if (plan.plan_kind === "timeline") return await commitPlan(doc, plan, validation === void 0 ? void 0 : { validation });
1807
- if (validation === "preflight") throw new Error("validation=preflight applies only to timeline plans; entity plans use revision CAS");
1611
+ async function commitCachedPlan(docId, _doc, plan, validation) {
1612
+ if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
1613
+ if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
1808
1614
  if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
1809
1615
  return await commitEntityPlan(getEntityClient(docId), plan);
1810
1616
  }
@@ -1829,8 +1635,8 @@ function createMedeoTool(options) {
1829
1635
  if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
1830
1636
  if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
1831
1637
  return await runExclusive(docId, async (doc) => {
1832
- await observePull(doc);
1833
- const documentVersion = encodeDocVersionMark(doc.versionMark());
1638
+ const [, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(docId)]);
1639
+ const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
1834
1640
  const baselineKey = `${contextId}\u0000${docId}`;
1835
1641
  const previousVersion = modelContextVersions.get(baselineKey);
1836
1642
  const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
@@ -1854,18 +1660,60 @@ function createMedeoTool(options) {
1854
1660
  async function snapshot(input) {
1855
1661
  return runExclusive(input.doc_id, async (doc) => {
1856
1662
  assertNoPendingPush(input.doc_id);
1857
- const pull = await observePull(doc);
1663
+ const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
1858
1664
  return {
1859
1665
  ok: true,
1860
1666
  op: "snapshot",
1861
1667
  doc_id: input.doc_id,
1862
- version: encodeDocVersionMark(doc.versionMark()),
1863
- preview: renderCompactProjection(doc.snapshot()),
1668
+ version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
1669
+ preview: renderEntitySnapshot(entityState) + migrationNotice(doc.snapshot(), entityState),
1864
1670
  collaborated: pull.collaborated,
1865
1671
  ...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
1866
1672
  };
1867
1673
  });
1868
1674
  }
1675
+ async function migrate(input) {
1676
+ return runExclusive(input.doc_id, async (doc) => {
1677
+ assertNoPendingPush(input.doc_id);
1678
+ const client = new EntityGraphHttpClient({
1679
+ docId: input.doc_id,
1680
+ httpOrigin: requiredContext(options.httpOrigin, input.doc_id, "httpOrigin"),
1681
+ ...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, input.doc_id) },
1682
+ ...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, input.doc_id) },
1683
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
1684
+ });
1685
+ const base = await client.fetchState();
1686
+ if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
1687
+ ok: true,
1688
+ op: "migrate-legacy",
1689
+ doc_id: input.doc_id,
1690
+ migration_status: "already_entity",
1691
+ entity_revision: base.revision,
1692
+ next_action: "snapshot"
1693
+ };
1694
+ const pull = await doc.pull();
1695
+ if (!pull.ok) throw new Error(`Migration requires a fresh canonical snapshot: ${pull.error.message}`);
1696
+ const migrationBaseVv = encodeDocVersionMark(doc.versionMark());
1697
+ const nextRows = migrateLegacyTimelineToEntities(doc.snapshot(), input.asset_facts, base.rows);
1698
+ let revision;
1699
+ try {
1700
+ revision = (await client.commit(base, nextRows, { migrationBaseVv })).revision;
1701
+ } catch (error) {
1702
+ if (error instanceof MengineHttpRequestError) throw new Error(`Migration rejected (HTTP ${error.status}): ${entityHttpErrorMessage(error.payload)}; take a fresh snapshot before retrying`);
1703
+ throw new Error("Migration submission is unconfirmed; take a fresh snapshot and retry migrate-legacy to inspect whether the Entity timeline already exists");
1704
+ }
1705
+ documents.delete(input.doc_id);
1706
+ for (const [id, cached] of plans) if (cached.docId === input.doc_id) plans.delete(id);
1707
+ return {
1708
+ ok: true,
1709
+ op: "migrate-legacy",
1710
+ doc_id: input.doc_id,
1711
+ migration_status: "committed",
1712
+ entity_revision: revision,
1713
+ next_action: "snapshot"
1714
+ };
1715
+ });
1716
+ }
1869
1717
  async function run(input) {
1870
1718
  return runExclusive(input.doc_id, async (doc) => {
1871
1719
  assertNoPendingPush(input.doc_id);
@@ -1876,6 +1724,7 @@ function createMedeoTool(options) {
1876
1724
  document,
1877
1725
  baseVersion,
1878
1726
  entityState,
1727
+ entityOnly: true,
1879
1728
  script: input.script,
1880
1729
  ...input.inputs !== void 0 ? { inputs: input.inputs } : {},
1881
1730
  timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
@@ -1973,6 +1822,7 @@ function createMedeoTool(options) {
1973
1822
  try {
1974
1823
  const parsed = parseInput(input);
1975
1824
  if (parsed.op === "snapshot") return await snapshot(parsed);
1825
+ if (parsed.op === "migrate-legacy") return await migrate(parsed);
1976
1826
  if (parsed.op === "run-edit-script") return await run(parsed);
1977
1827
  return await commit(parsed);
1978
1828
  } catch (error) {