@mengine/medeo-client 2.0.1-alpha.23 → 2.0.1-alpha.24

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.
@@ -1177,8 +1177,7 @@ function toAuthoritativePartLibrary(partLibrary) {
1177
1177
  if (typeof part !== "object" || part == null) continue;
1178
1178
  if (part.video_clip != null) out[partId] = { video_clip: withTrimWindowFromDuration(part.video_clip) };
1179
1179
  else if (part.speech != null) {
1180
- const { origin_asset_system: _originAssetSystem, ...readSpeech } = part.speech;
1181
- const { duration_ms, rest } = splitDurationMs(readSpeech);
1180
+ const { duration_ms, rest } = splitDurationMs(part.speech);
1182
1181
  out[partId] = { speech: {
1183
1182
  ...rest,
1184
1183
  media_duration_ms: rest.media_duration_ms ?? duration_ms
@@ -1447,6 +1446,20 @@ function emptyLaneTracks() {
1447
1446
  }));
1448
1447
  }
1449
1448
  //#endregion
1449
+ //#region ../medeo-dsl/src/field-merge.ts
1450
+ const FIELD_MERGE = Object.freeze({
1451
+ /** One segment reference, narrowed by an optional code-point range. */
1452
+ selection: "atomic",
1453
+ textRange: "atomic",
1454
+ /** Collaborative text, merged by code point. */
1455
+ text: "text",
1456
+ phonemeScript: "text",
1457
+ /** Element identity is the segment id, so inserts and moves survive each other. */
1458
+ segments: "list",
1459
+ segmentRanges: "list",
1460
+ baseEntityIds: "list"
1461
+ });
1462
+ //#endregion
1450
1463
  //#region ../medeo-dsl/src/entities.ts
1451
1464
  /**
1452
1465
  * What every known entity kind declares.
@@ -1468,12 +1481,6 @@ function emptyLaneTracks() {
1468
1481
  * flowed, is the `from-asset` / `to-asset` Relation's answer alone.
1469
1482
  */
1470
1483
  const ENTITY_KINDS = Object.freeze({
1471
- axvideo: Object.freeze([
1472
- "Container",
1473
- "Sequence",
1474
- "Visual",
1475
- "Audible"
1476
- ]),
1477
1484
  timeline: Object.freeze(["Container"]),
1478
1485
  track: Object.freeze(["Container"]),
1479
1486
  clip: Object.freeze(["Container"]),
@@ -1489,7 +1496,7 @@ const ENTITY_KINDS = Object.freeze({
1489
1496
  "Audible",
1490
1497
  "MediaFile"
1491
1498
  ]),
1492
- voiceover: Object.freeze([
1499
+ "voiceover-recording": Object.freeze([
1493
1500
  "Sequence",
1494
1501
  "Audible",
1495
1502
  "MediaFile"
@@ -1501,7 +1508,6 @@ const ENTITY_KINDS = Object.freeze({
1501
1508
  ]),
1502
1509
  voice: Object.freeze(["Container", "IPBound"]),
1503
1510
  "sequence-marker": Object.freeze([]),
1504
- viewport: Object.freeze([]),
1505
1511
  "audio-script": Object.freeze(["SegmentText", "MediaFile"]),
1506
1512
  "phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
1507
1513
  caption: Object.freeze([
@@ -1534,7 +1540,7 @@ function isSequenceFields(value) {
1534
1540
  return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
1535
1541
  }
1536
1542
  function isMediaAssetVariantKind(kind) {
1537
- return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover";
1543
+ return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover-recording";
1538
1544
  }
1539
1545
  function isKnownEntityKind(kind) {
1540
1546
  return Object.hasOwn(ENTITY_KINDS, kind);
@@ -1806,7 +1812,7 @@ function entityToRow(entity) {
1806
1812
  //#region ../medeo-dsl/src/invariants.ts
1807
1813
  /** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
1808
1814
  /** Validates one complete set of entities and its authoritative Relation rows. */
1809
- function validateEntityRelationSet(entityRefs, index, options) {
1815
+ function validateEntityRelationSet(entityRefs, index) {
1810
1816
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
1811
1817
  const entityIds = new Set(entities.map((entity) => entity.entityId));
1812
1818
  const rows = {
@@ -1830,17 +1836,13 @@ function validateEntityRelationSet(entityRefs, index, options) {
1830
1836
  if (current.entityKind === "sequence-marker") {
1831
1837
  const marker = entity;
1832
1838
  issues.push(...validateMarkerUse(entity, index));
1833
- issues.push(...validateMarkerRanges(marker, {
1834
- source: (left, right) => options.compareMarkerPoints(marker, "source", left, right),
1835
- target: (left, right) => options.compareMarkerPoints(marker, "target", left, right)
1836
- }));
1837
- issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
1839
+ issues.push(...validateMarkerRanges(marker));
1840
+ issues.push(...validateMarkerSourceBounds(marker, index));
1838
1841
  }
1839
1842
  if (current.entityKind === "clip") {
1840
1843
  issues.push(...validateClipAdmission(entity, index));
1841
1844
  issues.push(...validateClipPlacement(entity, index));
1842
1845
  }
1843
- if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
1844
1846
  if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
1845
1847
  if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
1846
1848
  issues.push(...validateSequenceComposition(entity));
@@ -1849,18 +1851,16 @@ function validateEntityRelationSet(entityRefs, index, options) {
1849
1851
  return issues;
1850
1852
  }
1851
1853
  function validateMarkerUse(marker, index) {
1852
- const relations = [...index.relationsOf(marker)];
1853
- const clipEdges = ofKind(relations, "clip-marker");
1854
- const axVideoEdges = ofKind(relations, "axvideo-marker");
1855
- const contentEdges = ofKind(relations, "marker-content");
1856
- const timelineEdges = ofKind(relations, "marker-timeline");
1857
- const scriptEdges = ofKind(relations, "audio-script-marker");
1854
+ const markerEdges = ofKind([...index.relationsOf(marker)], "marker");
1855
+ const markedKind = (relation) => relation.other(marker)?.deref()?.current().entityKind;
1856
+ const clipEdges = markerEdges.filter((relation) => markedKind(relation) === "clip");
1857
+ const scriptEdges = markerEdges.filter((relation) => markedKind(relation) === "audio-script");
1858
1858
  const issues = [];
1859
1859
  if (scriptEdges.length > 0) {
1860
- if (clipEdges.length + axVideoEdges.length + contentEdges.length + timelineEdges.length > 0) issues.push({
1860
+ if (clipEdges.length > 0) issues.push({
1861
1861
  code: "marker_container_xor",
1862
1862
  entityId: marker.entityId,
1863
- message: "An AudioScript annotation Marker must not carry Clip, AXVideo, content, or Timeline relations"
1863
+ message: "An AudioScript annotation Marker must not carry Clip or content relations"
1864
1864
  });
1865
1865
  const ranges = marker.current().segmentRanges;
1866
1866
  if (!ranges?.length) issues.push({
@@ -1887,64 +1887,44 @@ function validateMarkerUse(marker, index) {
1887
1887
  entityId: marker.entityId,
1888
1888
  message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
1889
1889
  });
1890
- if (clipEdges.length + axVideoEdges.length !== 1) issues.push({
1890
+ if (clipEdges.length !== 1) issues.push({
1891
1891
  code: "marker_container_xor",
1892
1892
  entityId: marker.entityId,
1893
- message: "Sequence Marker must have exactly one Clip XOR AXVideo container relation"
1894
- });
1895
- if (contentEdges.length + timelineEdges.length !== 1) issues.push({
1896
- code: "marker_content_xor",
1897
- entityId: marker.entityId,
1898
- message: "Sequence Marker must have exactly one Sequence content XOR Timeline relation"
1899
- });
1900
- if (clipEdges.length === 1 && timelineEdges.length === 1 || axVideoEdges.length === 1 && contentEdges.length === 1) issues.push({
1901
- code: "marker_pair_mismatch",
1902
- entityId: marker.entityId,
1903
- message: "Only Clip+Content or AXVideo+Timeline Marker relation pairs are valid"
1893
+ message: "A display Sequence Marker belongs to exactly one Clip"
1904
1894
  });
1905
1895
  return issues;
1906
1896
  }
1907
1897
  function validateClipAdmission(clip, index) {
1908
- const markerEdges = ofKind([...index.relationsOf(clip)], "clip-marker");
1898
+ const markerEdges = markersOn(clip, index);
1909
1899
  if (markerEdges.length !== 1) return [{
1910
1900
  code: "clip_marker_cardinality",
1911
1901
  entityId: clip.entityId,
1912
1902
  message: "Clip must have exactly one authoritative Clip-Marker Relation"
1913
1903
  }];
1914
- const marker = markerEdges[0]?.other(clip)?.deref();
1915
- if (marker == null) return [{
1904
+ if (markerEdges[0]?.other(clip)?.deref() == null) return [{
1916
1905
  code: "clip_content_cardinality",
1917
1906
  entityId: clip.entityId,
1918
- message: "Clip must resolve one live Sequence Marker and one content Relation"
1907
+ message: "Clip must resolve one live Sequence Marker"
1919
1908
  }];
1920
- const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
1909
+ const contentEdges = holds(clip, index);
1921
1910
  if (contentEdges.length !== 1) return [{
1922
1911
  code: "clip_content_cardinality",
1923
1912
  entityId: clip.entityId,
1924
- message: "Clip Marker must resolve exactly one content Relation"
1913
+ message: "Clip must contain exactly one content entity"
1925
1914
  }];
1926
- const content = contentEdges[0]?.other(marker)?.deref()?.current();
1915
+ const content = contentEdges[0]?.other(clip)?.deref()?.current();
1927
1916
  if (content == null) return [{
1928
1917
  code: "clip_content_cardinality",
1929
1918
  entityId: clip.entityId,
1930
- message: "Clip Marker content Relation must resolve one live entity"
1919
+ message: "Clip content Relation must resolve one live entity"
1931
1920
  }];
1932
1921
  if (!hasSequence(content)) return [{
1933
1922
  code: "clip_content_not_sequence",
1934
1923
  entityId: clip.entityId,
1935
- message: `Clip Marker resolves to non-Sequence entity "${content.entityId}"`
1924
+ message: `Clip contains non-Sequence entity "${content.entityId}"`
1936
1925
  }];
1937
1926
  return [];
1938
1927
  }
1939
- function validateAXVideoAdmission(axVideo, index) {
1940
- const markerEdges = ofKind([...index.relationsOf(axVideo)], "axvideo-marker");
1941
- if (markerEdges.length === 1 && markerEdges[0]?.other(axVideo)?.deref() != null) return [];
1942
- return [{
1943
- code: "axvideo_marker_cardinality",
1944
- entityId: axVideo.entityId,
1945
- message: "AXVideo must have exactly one live AXVideo-Marker Relation"
1946
- }];
1947
- }
1948
1928
  /** Validate a Caption against the complete entity set, including its directly held bases. */
1949
1929
  function validateCaptionComposition(caption, index, entities = []) {
1950
1930
  return validateVariantComposition(caption, index, entities, "caption");
@@ -1971,33 +1951,30 @@ function validateVariantComposition(variant, index, entities, kind) {
1971
1951
  return issues;
1972
1952
  }
1973
1953
  function validateClipPlacement(clip, index) {
1974
- const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
1954
+ const marker = markersOn(clip, index)[0]?.other(clip)?.deref();
1975
1955
  if (marker?.current().entityKind !== "sequence-marker") return [];
1976
1956
  const markerValue = marker.current();
1977
1957
  const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
1978
- const hasOrder = clip.current().order !== void 0;
1979
- const hasTarget = markerValue.targetRange !== void 0;
1980
- const hasAnchorOffset = markerValue.anchorOffset !== void 0;
1958
+ const hasStart = markerValue.startMs !== void 0;
1981
1959
  const issues = [];
1982
1960
  if (anchors.length > 1) issues.push({
1983
1961
  code: "clip_anchor_cardinality",
1984
1962
  entityId: clip.entityId,
1985
1963
  message: "A Clip may follow at most one host Clip"
1986
1964
  });
1987
- const hasAnchor = anchors.length === 1;
1988
- if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
1965
+ if (anchors.length > 0 && !hasStart) issues.push({
1989
1966
  code: "clip_placement_invalid",
1990
1967
  entityId: clip.entityId,
1991
- message: "Clip placement must be exactly one of Clip.order, Marker.targetRange, or clip-anchor with Marker.anchorOffset"
1968
+ message: "A Clip following a host states startMs, the offset inside that host"
1992
1969
  });
1993
- if (markerValue.durationPolicy === "timeline") {
1994
- const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
1995
- const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
1970
+ if (markerValue.fillTimeline === true) {
1971
+ const content = holds(clip, index)[0]?.other(clip)?.deref()?.current();
1972
+ const track = heldBy(clip, index)[0]?.other(clip)?.deref()?.current();
1996
1973
  const trackRole = track?.entityKind === "track" ? track.role : void 0;
1997
- if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder && !hasTarget) issues.push({
1974
+ if (content?.entityKind !== "audio" || trackRole !== "bgm") issues.push({
1998
1975
  code: "timeline_duration_policy_invalid",
1999
1976
  entityId: marker.entityId,
2000
- message: "Marker durationPolicy \"timeline\" is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
1977
+ message: "Marker fillTimeline is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
2001
1978
  });
2002
1979
  }
2003
1980
  return issues;
@@ -2029,34 +2006,28 @@ function validateClipAnchorCycles(entities, index) {
2029
2006
  }
2030
2007
  return issues;
2031
2008
  }
2032
- function validateMarkerRanges(marker, compare) {
2033
- const current = marker.current();
2034
- const issues = [];
2035
- if (!(compare.source(current.sourceRange.start, current.sourceRange.end) < 0)) issues.push({
2009
+ function validateMarkerRanges(marker) {
2010
+ const { inMs, outMs } = marker.current();
2011
+ if (Number.isFinite(inMs) && Number.isFinite(outMs) && outMs > inMs) return [];
2012
+ return [{
2036
2013
  code: "marker_source_range_empty",
2037
2014
  entityId: marker.entityId,
2038
- message: "Sequence Marker sourceRange must be a non-empty half-open interval"
2039
- });
2040
- if (current.targetRange != null && !(compare.target(current.targetRange.start, current.targetRange.end) < 0)) issues.push({
2041
- code: "marker_target_range_empty",
2042
- entityId: marker.entityId,
2043
- message: "Sequence Marker targetRange must be a non-empty half-open interval"
2044
- });
2045
- return issues;
2015
+ message: "Sequence Marker outMs must be a finite millisecond value greater than inMs"
2016
+ }];
2046
2017
  }
2047
- function validateMarkerSourceBounds(marker, index, compare) {
2048
- const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
2018
+ function validateMarkerSourceBounds(marker, index) {
2019
+ const clip = markedBy(marker, index)[0]?.other(marker)?.deref();
2020
+ if (clip == null) return [];
2021
+ const contentEdges = holds(clip, index);
2049
2022
  if (contentEdges.length !== 1) return [];
2050
- const content = contentEdges[0]?.other(marker)?.deref()?.current();
2023
+ const content = contentEdges[0]?.other(clip)?.deref()?.current();
2051
2024
  if (content == null || !hasSequence(content)) return [];
2052
- const sourceRange = marker.current().sourceRange;
2053
- const startVsOrigin = compare(sourceRange.start, 0);
2054
- const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
2055
- if (!Number.isNaN(startVsOrigin) && startVsOrigin >= 0 && (endVsDuration == null || !Number.isNaN(endVsDuration) && endVsDuration <= 0)) return [];
2025
+ const { inMs, outMs } = marker.current();
2026
+ if (inMs >= 0 && (content.durationMs == null || outMs <= content.durationMs)) return [];
2056
2027
  return [{
2057
2028
  code: "marker_source_out_of_bounds",
2058
2029
  entityId: marker.entityId,
2059
- message: `Sequence Marker sourceRange must stay within content "${content.entityId}" duration`
2030
+ message: "Sequence Marker in and out points must stay inside the length its content states"
2060
2031
  }];
2061
2032
  }
2062
2033
  function validateSequenceComposition(entity) {
@@ -2103,21 +2074,20 @@ function validateKnownEntityPayload(entity) {
2103
2074
  "extent",
2104
2075
  "sampling",
2105
2076
  "coordinateSpace",
2106
- "sourceRange",
2107
- "targetRange",
2108
- "duration",
2077
+ "inMs",
2078
+ "outMs",
2109
2079
  "startMs",
2110
- "endMs"
2080
+ "rate"
2111
2081
  ]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
2112
2082
  }
2113
2083
  switch (entity.entityKind) {
2114
2084
  case "track":
2115
2085
  validateOptionalField(value, "hidden", "boolean", problems);
2116
2086
  validateOptionalField(value, "role", "string", problems);
2117
- validateOptionalFiniteNumber(value, "order", problems);
2118
2087
  break;
2119
2088
  case "video":
2120
2089
  case "audio":
2090
+ case "voiceover-recording":
2121
2091
  validateDurationPayload(value, "own", problems);
2122
2092
  break;
2123
2093
  case "caption":
@@ -2131,12 +2101,7 @@ function validateKnownEntityPayload(entity) {
2131
2101
  validateDurationPayload(value, "none", problems);
2132
2102
  break;
2133
2103
  case "axvideo":
2134
- for (const retired of [
2135
- "extent",
2136
- "sampling",
2137
- "coordinateSpace",
2138
- "durationMs"
2139
- ]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
2104
+ validateDurationPayload(value, "none", problems);
2140
2105
  break;
2141
2106
  case "sequence-marker":
2142
2107
  validateMarkerPayload(value, problems);
@@ -2150,7 +2115,6 @@ function validateKnownEntityPayload(entity) {
2150
2115
  case "timeline":
2151
2116
  case "viewport": break;
2152
2117
  case "clip":
2153
- validateOptionalFiniteNumber(value, "order", problems);
2154
2118
  validateOptionalFiniteNumber(value, "volume", problems);
2155
2119
  if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
2156
2120
  break;
@@ -2184,17 +2148,16 @@ function validateDurationPayload(value, expected, problems) {
2184
2148
  if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
2185
2149
  }
2186
2150
  function validateMarkerPayload(value, problems) {
2187
- validateRange(value.sourceRange, "sourceRange", true, problems);
2188
- if (Object.hasOwn(value, "targetRange") && value.targetRange !== void 0) validateRange(value.targetRange, "targetRange", true, problems);
2189
- const duration = value.duration;
2190
- if (!isRecord(duration)) problems.push("duration must be an object");
2191
- else if (duration.mode === "fixed") {
2192
- if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
2193
- } else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
2194
- if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
2195
- if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
2151
+ if (!isWholeMilliseconds(value.inMs) || value.inMs < 0) problems.push("inMs must be whole milliseconds at or after zero");
2152
+ if (!isWholeMilliseconds(value.outMs)) problems.push("outMs must be whole milliseconds");
2153
+ if (Object.hasOwn(value, "startMs") && (!isWholeMilliseconds(value.startMs) || value.startMs < 0)) problems.push("startMs must be whole milliseconds at or after zero when present");
2154
+ if (Object.hasOwn(value, "rate") && (typeof value.rate !== "number" || !(value.rate > 0))) problems.push("rate must be a positive number when present");
2155
+ if (Object.hasOwn(value, "fillTimeline") && value.fillTimeline !== true) problems.push("fillTimeline is stated only as true");
2196
2156
  if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
2197
2157
  }
2158
+ function isWholeMilliseconds(value) {
2159
+ return typeof value === "number" && Number.isSafeInteger(value);
2160
+ }
2198
2161
  /** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
2199
2162
  function validateSegmentRanges(value, problems) {
2200
2163
  if (!Array.isArray(value)) {
@@ -2293,19 +2256,10 @@ function validateCaptionPayload(value, problems) {
2293
2256
  "strokeColor"
2294
2257
  ]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
2295
2258
  }
2296
- function validateRange(value, path, required, problems) {
2297
- if (!isRecord(value)) {
2298
- if (required) problems.push(`${path} must be an object`);
2299
- return;
2300
- }
2301
- if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
2302
- if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
2303
- }
2304
2259
  /** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
2305
2260
  function validatePhoneticScriptPayload(value, problems) {
2306
2261
  if (value.segments !== void 0) validateScriptPayload(value, problems);
2307
2262
  if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
2308
- if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
2309
2263
  }
2310
2264
  function validateScriptPayload(value, problems) {
2311
2265
  if (!Array.isArray(value.segments)) {
@@ -2322,9 +2276,8 @@ function validateScriptPayload(value, problems) {
2322
2276
  "endMs",
2323
2277
  "start_ms",
2324
2278
  "end_ms",
2325
- "sourceRange",
2326
- "targetRange",
2327
- "duration"
2279
+ "inMs",
2280
+ "outMs"
2328
2281
  ]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
2329
2282
  if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
2330
2283
  if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
@@ -2379,11 +2332,11 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2379
2332
  if ([
2380
2333
  "video",
2381
2334
  "audio",
2335
+ "voiceover-recording",
2382
2336
  "image",
2383
- "caption",
2384
- "axvideo"
2337
+ "caption"
2385
2338
  ].includes(entityKind) && /^durationMs$/.test(path)) return true;
2386
- if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
2339
+ if (entityKind === "sequence-marker" && /^(?:fillTimeline|rate)$/.test(path)) return true;
2387
2340
  if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2388
2341
  if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
2389
2342
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
@@ -2451,19 +2404,57 @@ function expectedDurationShape(kind) {
2451
2404
  function isPositiveInteger(value) {
2452
2405
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
2453
2406
  }
2407
+ /** Marker edges where this entity is what the Marker is marked on. */
2408
+ function markersOn(target, index) {
2409
+ return ofKind([...index.relationsOf(target)], "marker").filter((relation) => relation.endpoints[0].deref()?.entityId === target.entityId);
2410
+ }
2411
+ /** Marker edges seen from the Marker itself. */
2412
+ function markedBy(marker, index) {
2413
+ return ofKind([...index.relationsOf(marker)], "marker").filter((relation) => relation.endpoints[1].deref()?.entityId === marker.entityId);
2414
+ }
2415
+ /** Contain edges where this entity is the Container. */
2416
+ function holds(container, index) {
2417
+ return ofKind([...index.relationsOf(container)], "contain").filter((relation) => relation.endpoints[0].deref()?.entityId === container.entityId);
2418
+ }
2419
+ /** Contain edges where this entity is what a Container holds. */
2420
+ function heldBy(contained, index) {
2421
+ return ofKind([...index.relationsOf(contained)], "contain").filter((relation) => relation.endpoints[1].deref()?.entityId === contained.entityId);
2422
+ }
2454
2423
  function ofKind(relations, kind) {
2455
2424
  return relations.filter((relation) => relation.kind === kind);
2456
2425
  }
2457
2426
  //#endregion
2458
2427
  //#region ../medeo-dsl/src/relation-specs.ts
2459
- const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2460
- const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
2461
- const clipMarkerRelationSpec = emptySpec("clip-marker", "clip", "sequence-marker");
2462
- const axVideoMarkerRelationSpec = emptySpec("axvideo-marker", "axvideo", "sequence-marker");
2463
- const markerTimelineRelationSpec = emptySpec("marker-timeline", "sequence-marker", "timeline");
2464
- const markerContentRelationSpec = Object.freeze({
2465
- kind: "marker-content",
2466
- validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "sequence-marker" && hasSequence(endpoints[1].current()),
2428
+ /**
2429
+ * What a Container holds. Which pairs are legal is the Container's own rule:
2430
+ * a Timeline holds Tracks, a Track holds Clips, and a Clip — the placement
2431
+ * itself — holds content that declares Sequence.
2432
+ */
2433
+ const containRelationSpec = Object.freeze({
2434
+ kind: "contain",
2435
+ ordered: true,
2436
+ sequenced: true,
2437
+ validateEndpoints: (endpoints) => containerAdmits(endpoints[0].current(), endpoints[1].current()),
2438
+ validateMetadata: isEmptyMetadata
2439
+ });
2440
+ function containerAdmits(container, contained) {
2441
+ if (!declaresKind(container.entityKind, "Container")) return false;
2442
+ if (container.entityKind === "timeline") return contained.entityKind === "track";
2443
+ if (container.entityKind === "track") return contained.entityKind === "clip";
2444
+ if (container.entityKind === "clip") return hasSequence(contained);
2445
+ return false;
2446
+ }
2447
+ /**
2448
+ * A point marked on endpoint 0's timeline. A Clip takes a display Marker — the
2449
+ * window and placement it plays its content through; an AudioScript takes an
2450
+ * annotation Marker — assigned per-Segment times. What the Marker means is the
2451
+ * marked entity's own business.
2452
+ */
2453
+ const markerRelationSpec = Object.freeze({
2454
+ kind: "marker",
2455
+ ordered: true,
2456
+ sequenced: false,
2457
+ validateEndpoints: (endpoints) => (endpoints[0].current().entityKind === "clip" || endpoints[0].current().entityKind === "audio-script") && endpoints[1].current().entityKind === "sequence-marker",
2467
2458
  validateMetadata: isEmptyMetadata
2468
2459
  });
2469
2460
  /**
@@ -2477,7 +2468,9 @@ const markerContentRelationSpec = Object.freeze({
2477
2468
  */
2478
2469
  const fromAssetRelationSpec = Object.freeze({
2479
2470
  kind: "from-asset",
2480
- validateEndpoints: (endpoints) => declaresKind(endpoints[0].current().entityKind, "MediaFile") && endpoints[1].current().entityKind === "asset",
2471
+ ordered: true,
2472
+ sequenced: false,
2473
+ validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
2481
2474
  validateMetadata: isAssetBinding
2482
2475
  });
2483
2476
  /**
@@ -2497,105 +2490,87 @@ const fromAssetRelationSpec = Object.freeze({
2497
2490
  */
2498
2491
  const toAssetRelationSpec = Object.freeze({
2499
2492
  kind: "to-asset",
2500
- validateEndpoints: (endpoints) => declaresKind(endpoints[0].current().entityKind, "MediaFile") && endpoints[1].current().entityKind === "asset",
2493
+ ordered: true,
2494
+ sequenced: false,
2495
+ validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
2501
2496
  validateMetadata: isAssetBinding
2502
2497
  });
2503
- /** `generated(output, input)` records media generation or an independent AudioScript revision. */
2504
- const generatedRelationSpec = Object.freeze({
2505
- kind: "generated",
2506
- validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())) || endpoints.every((endpoint) => endpoint.current().entityKind === "audio-script"),
2498
+ /** Media a generated medium can be made from, and a transcript can be read off. */
2499
+ const MEDIA_VARIANT_KINDS = [
2500
+ "video",
2501
+ "image",
2502
+ "audio",
2503
+ "voiceover-recording"
2504
+ ];
2505
+ const MEDIA_AUDIBLE_KINDS = [
2506
+ "video",
2507
+ "audio",
2508
+ "voiceover-recording"
2509
+ ];
2510
+ /**
2511
+ * What a thing was made from: a Voiceover Recording from the Phonetic Script it
2512
+ * was spoken from, a transcribed Audio Script from the media it was read off, a
2513
+ * generated medium from the media it was made with. Endpoint 0 is the product,
2514
+ * endpoint 1 what it came from.
2515
+ */
2516
+ const derivedFromRelationSpec = Object.freeze({
2517
+ kind: "derived-from",
2518
+ ordered: true,
2519
+ sequenced: false,
2520
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(MEDIA_VARIANT_KINDS), new Set(MEDIA_VARIANT_KINDS)) || hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["phonetic-script"])) || hasKinds(endpoints, new Set(["audio-script"]), new Set(MEDIA_AUDIBLE_KINDS)),
2507
2521
  validateMetadata: isEmptyMetadata
2508
2522
  });
2509
2523
  const captionAlignmentRelationSpec = Object.freeze({
2510
2524
  kind: "caption-alignment",
2511
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover"])),
2525
+ ordered: true,
2526
+ sequenced: false,
2527
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover-recording"])),
2512
2528
  validateMetadata: isCaptionAlignmentMetadata
2513
2529
  });
2514
2530
  /** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
2515
2531
  const clipAnchorRelationSpec = Object.freeze({
2516
2532
  kind: "clip-anchor",
2533
+ ordered: true,
2534
+ sequenced: false,
2517
2535
  validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
2518
2536
  validateMetadata: isEmptyMetadata
2519
2537
  });
2520
2538
  /**
2521
- * The Voiceover and the PhoneticScript it was spoken from. Endpoint 0 is the
2522
- * output and endpoint 1 is the input script.
2523
- *
2524
- * Every Voiceover must carry one: which words were said, and how, is the only
2525
- * thing that makes speech reproducible, and a Voiceover that cannot answer it
2526
- * is refused when its plan is built.
2527
- */
2528
- const phoneticScriptRenderRelationSpec = Object.freeze({
2529
- kind: "phonetic-script-render",
2530
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["phonetic-script"])),
2531
- validateMetadata: isEmptyMetadata
2532
- });
2533
- /**
2534
2539
  * The timbre identity a Voiceover was synthesized with. Voice stays an
2535
2540
  * identity: it never carries the audio itself, so the link between the two is a
2536
2541
  * Relation rather than one shared entity, and several takes can name one voice.
2537
2542
  */
2538
2543
  const voiceTimbreRelationSpec = Object.freeze({
2539
2544
  kind: "voice-timbre",
2540
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["voice"])),
2541
- validateMetadata: isEmptyMetadata
2542
- });
2543
- /** AudioScript was transcribed from Audio or Video: `[script, source]`. */
2544
- const audioScriptSourceRelationSpec = Object.freeze({
2545
- kind: "audio-script-source",
2546
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
2547
- "audio",
2548
- "voiceover",
2549
- "video"
2550
- ])),
2551
- validateMetadata: isEmptyMetadata
2552
- });
2553
- /**
2554
- * `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
2555
- * whose `segmentRanges` hold directly assigned per-Segment time values. The
2556
- * script keeps no Sequence and no Clip admission; annotation Markers cannot
2557
- * enter Clip/AXVideo use chains (enforced by the marker-use invariant).
2558
- */
2559
- const audioScriptMarkerRelationSpec = Object.freeze({
2560
- kind: "audio-script-marker",
2561
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["sequence-marker"])),
2545
+ ordered: true,
2546
+ sequenced: false,
2547
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["voice"])),
2562
2548
  validateMetadata: isEmptyMetadata
2563
2549
  });
2564
2550
  /** Built-in kinds are reserved; callers may add specs only under new names. */
2565
2551
  const builtInRelationSpecs = Object.freeze([
2566
- timelineTrackRelationSpec,
2567
- trackClipRelationSpec,
2568
- clipMarkerRelationSpec,
2569
- markerContentRelationSpec,
2570
- axVideoMarkerRelationSpec,
2571
- markerTimelineRelationSpec,
2552
+ containRelationSpec,
2553
+ markerRelationSpec,
2572
2554
  fromAssetRelationSpec,
2573
2555
  toAssetRelationSpec,
2574
- generatedRelationSpec,
2556
+ derivedFromRelationSpec,
2575
2557
  captionAlignmentRelationSpec,
2576
2558
  clipAnchorRelationSpec,
2577
- phoneticScriptRenderRelationSpec,
2578
- voiceTimbreRelationSpec,
2579
- audioScriptSourceRelationSpec,
2580
- audioScriptMarkerRelationSpec
2559
+ voiceTimbreRelationSpec
2581
2560
  ]);
2582
- function emptySpec(kind, a, b) {
2583
- return metadataSpec(kind, a, b, isEmptyMetadata);
2584
- }
2585
- function metadataSpec(kind, a, b, validateMetadata) {
2586
- return Object.freeze({
2587
- kind,
2588
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set([a]), new Set([b])),
2589
- validateMetadata
2590
- });
2591
- }
2561
+ /**
2562
+ * Endpoint 0 is one of `aKinds` and endpoint 1 one of `bKinds`.
2563
+ *
2564
+ * Positional, because every built-in kind is ordered: a reversed row states a
2565
+ * different fact, not the same one written backwards.
2566
+ */
2592
2567
  function hasKinds(endpoints, aKinds, bKinds) {
2593
- const first = endpoints[0].current().entityKind;
2594
- const second = endpoints[1].current().entityKind;
2595
- return aKinds.has(first) && bKinds.has(second);
2568
+ return aKinds.has(endpoints[0].current().entityKind) && bKinds.has(endpoints[1].current().entityKind);
2596
2569
  }
2597
- function isGeneratedMedia(entity) {
2598
- return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voiceover";
2570
+ /** `(entity, asset)`: the bound entity first, the Asset naming its bytes second. */
2571
+ function hasExactlyOneAsset(endpoints) {
2572
+ const bound = endpoints[0].current().entityKind;
2573
+ return endpoints[1].current().entityKind === "asset" && bound !== "asset" && declaresKind(bound, "MediaFile");
2599
2574
  }
2600
2575
  function isEmptyMetadata(value) {
2601
2576
  return isJsonObject(value) && Object.keys(value).length === 0;
@@ -2608,6 +2583,18 @@ function isAssetBinding(value) {
2608
2583
  function isCaptionAlignmentMetadata(value) {
2609
2584
  return isJsonObject(value) && Object.hasOwn(value, "alignment");
2610
2585
  }
2586
+ /** Whether this kind's Relations are ordered among themselves inside one container. */
2587
+ function isSequencedRelationKind(kind) {
2588
+ return builtInRelationSpecs.find((spec) => spec.kind === kind)?.sequenced ?? false;
2589
+ }
2590
+ /** Relations of `kind` where `entityId` is endpoint 0 — the container, the product, the child. */
2591
+ function relationsFrom(relations, kind, entityId) {
2592
+ return relations.filter((row) => row.relationKind === kind && row.endpoint0EntityId === entityId);
2593
+ }
2594
+ /** Relations of `kind` where `entityId` is endpoint 1 — what is contained, the source, the host. */
2595
+ function relationsTo(relations, kind, entityId) {
2596
+ return relations.filter((row) => row.relationKind === kind && row.endpoint1EntityId === entityId);
2597
+ }
2611
2598
  //#endregion
2612
2599
  //#region ../medeo-dsl/src/relations.ts
2613
2600
  function createEntityRef(entity) {
@@ -2660,16 +2647,15 @@ var BiRelationIndex = class {
2660
2647
  canonicalRefs = /* @__PURE__ */ new Map();
2661
2648
  byRelationId = /* @__PURE__ */ new Map();
2662
2649
  link(input) {
2663
- if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
2664
- if (input.spec.kind === "clip-anchor" || input.spec.kind === "phonetic-script-render" || input.spec.kind === "audio-script-source") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
2650
+ if (input.spec.kind === "derived-from" || input.spec.kind === "clip-anchor") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
2665
2651
  return this.linkValidated(input);
2666
2652
  }
2667
- /** Author `generated(output, input)` without exposing positional arguments. */
2668
- linkGenerated(input) {
2653
+ /** Author `derived-from(product, source)` without exposing positional arguments. */
2654
+ linkDerivedFrom(input) {
2669
2655
  return this.linkValidated({
2670
2656
  relationId: input.relationId,
2671
- spec: generatedRelationSpec,
2672
- endpoints: [input.output, input.input],
2657
+ spec: derivedFromRelationSpec,
2658
+ endpoints: [input.product, input.source],
2673
2659
  metadata: {},
2674
2660
  trace: input.trace
2675
2661
  });
@@ -2684,26 +2670,6 @@ var BiRelationIndex = class {
2684
2670
  trace: input.trace
2685
2671
  });
2686
2672
  }
2687
- /** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
2688
- linkPhoneticScriptRender(input) {
2689
- return this.linkValidated({
2690
- relationId: input.relationId,
2691
- spec: phoneticScriptRenderRelationSpec,
2692
- endpoints: [input.output, input.phoneticScript],
2693
- metadata: {},
2694
- trace: input.trace
2695
- });
2696
- }
2697
- /** Author `audio-script-source(script, source)` without exposing positional arguments. */
2698
- linkAudioScriptSource(input) {
2699
- return this.linkValidated({
2700
- relationId: input.relationId,
2701
- spec: audioScriptSourceRelationSpec,
2702
- endpoints: [input.script, input.source],
2703
- metadata: {},
2704
- trace: input.trace
2705
- });
2706
- }
2707
2673
  /**
2708
2674
  * Rehydrate a persisted row after resolving its spec and endpoint refs.
2709
2675
  *
@@ -2719,7 +2685,6 @@ var BiRelationIndex = class {
2719
2685
  this.assertRelationIdAvailable(input.relationId);
2720
2686
  const builtInSpec = builtInSpecByKind.get(input.spec.kind);
2721
2687
  if (builtInSpec != null && builtInSpec !== input.spec) throw new Error(`Relation kind "${input.spec.kind}" must use its built-in specification`);
2722
- if (isForbiddenAuthoritativeRelation(input.spec.kind, input.endpoints)) throw new Error("A direct Clip-Content Relation is derived-only and cannot be authoritative");
2723
2688
  if (!input.spec.validateEndpoints(input.endpoints)) throw new Error(`Relation "${input.spec.kind}" received invalid endpoints`);
2724
2689
  if (!input.spec.validateMetadata(input.metadata)) throw new Error(`Relation "${input.spec.kind}" received invalid metadata`);
2725
2690
  this.assertCanonicalRefAvailable(first);
@@ -2768,12 +2733,6 @@ var BiRelationIndex = class {
2768
2733
  }
2769
2734
  };
2770
2735
  const builtInSpecByKind = new Map(builtInRelationSpecs.map((spec) => [spec.kind, spec]));
2771
- function isForbiddenAuthoritativeRelation(kind, endpoints) {
2772
- if (kind === "clip-content") return true;
2773
- const first = endpoints[0].current();
2774
- const second = endpoints[1].current();
2775
- return first.entityKind === "clip" && hasSequence(second) || second.entityKind === "clip" && hasSequence(first);
2776
- }
2777
2736
  //#endregion
2778
2737
  //#region ../medeo-dsl/src/entity-relation-rows.ts
2779
2738
  var InvalidEntityRelationRowsError = class extends Error {
@@ -2862,7 +2821,7 @@ function decodeEntityRelationRows(rows, options) {
2862
2821
  issues.push(errorMessage(error));
2863
2822
  }
2864
2823
  }
2865
- if (issues.length === 0) for (const issue of validateEntityRelationSet([...entitiesById.values()], relationIndex, options)) issues.push(`${issue.code}: ${issue.message}`);
2824
+ if (issues.length === 0) for (const issue of validateEntityRelationSet([...entitiesById.values()], relationIndex)) issues.push(`${issue.code}: ${issue.message}`);
2866
2825
  if (issues.length > 0) throw new InvalidEntityRelationRowsError(issues);
2867
2826
  return {
2868
2827
  entitiesById,
@@ -2928,42 +2887,6 @@ function errorMessage(error) {
2928
2887
  return error instanceof Error ? error.message : String(error);
2929
2888
  }
2930
2889
  //#endregion
2931
- //#region ../medeo-dsl/src/relation-transition.ts
2932
- var InvalidEntityRelationTransitionError = class extends Error {
2933
- issues;
2934
- constructor(issues) {
2935
- super(`Invalid Medeo relation transition:\n- ${issues.join("\n- ")}`);
2936
- this.issues = issues;
2937
- this.name = "InvalidEntityRelationTransitionError";
2938
- }
2939
- };
2940
- /** Enforce relation fact identity while admitting a complete candidate view. */
2941
- function validateEntityRelationTransition(before, candidate) {
2942
- const issues = [];
2943
- const previous = new Map(before.relations.map((row) => [row.relationId, row]));
2944
- for (const relation of candidate.relations) {
2945
- const original = previous.get(relation.relationId);
2946
- if (original == null) {
2947
- if (relation.relationKind === "caption-alignment") issues.push(`new caption-alignment relation "${relation.relationId}" is forbidden`);
2948
- continue;
2949
- }
2950
- if (original.relationKind !== relation.relationKind || original.endpoint0EntityId !== relation.endpoint0EntityId || original.endpoint1EntityId !== relation.endpoint1EntityId) {
2951
- issues.push(`relation "${relation.relationId}" changed its kind or ordered endpoints`);
2952
- continue;
2953
- }
2954
- if (!sameJson(original.metadata, relation.metadata)) issues.push(`relation "${relation.relationId}" changed semantic metadata without a new identity`);
2955
- if (relation.relationKind === "caption-alignment" && !sameJson(original.trace, relation.trace)) issues.push(`legacy caption-alignment relation "${relation.relationId}" may only be carried unchanged or deleted`);
2956
- }
2957
- if (issues.length > 0) throw new InvalidEntityRelationTransitionError(issues);
2958
- }
2959
- function sameJson(left, right) {
2960
- if (left === right) return true;
2961
- if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
2962
- if (!isJsonObject(left) || !isJsonObject(right)) return false;
2963
- const leftKeys = Object.keys(left);
2964
- return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
2965
- }
2966
- //#endregion
2967
2890
  //#region src/document/entity-projection-values.ts
2968
2891
  /** A graph cannot be represented faithfully by the compatibility reader. */
2969
2892
  var EntityTimelineProjectionError = class extends Error {
@@ -3011,7 +2934,7 @@ var EntityProjectionGraph = class {
3011
2934
  timeline;
3012
2935
  constructor(rows) {
3013
2936
  this.rows = rows;
3014
- const decoded = decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2937
+ const decoded = decodeEntityRelationRows(rows, {});
3015
2938
  this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
3016
2939
  const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
3017
2940
  if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
@@ -3025,6 +2948,13 @@ var EntityProjectionGraph = class {
3025
2948
  adjacent(id, kind) {
3026
2949
  return [...new Set(this.rows.relations.filter((row) => row.relationKind === kind && (row.endpoint0EntityId === id || row.endpoint1EntityId === id)).map((row) => row.endpoint0EntityId === id ? row.endpoint1EntityId : row.endpoint0EntityId))].map((entityId) => this.entity(entityId));
3027
2950
  }
2951
+ /** What this entity contains, as opposed to what contains it. */
2952
+ holds(id) {
2953
+ return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint0EntityId === id).map((row) => this.entity(row.endpoint1EntityId));
2954
+ }
2955
+ heldBy(id) {
2956
+ return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint1EntityId === id).map((row) => this.entity(row.endpoint0EntityId));
2957
+ }
3028
2958
  one(id, relation, kind) {
3029
2959
  const values = this.adjacent(id, relation);
3030
2960
  if (values.length !== 1 || kind !== void 0 && values[0]?.entityKind !== kind) fail(`entity ${id} requires exactly one ${relation}${kind === void 0 ? "" : ` to ${kind}`}`);
@@ -3048,51 +2978,44 @@ function resolveEntityTimelineLayout(rows) {
3048
2978
  function resolveGraphLayout(graph) {
3049
2979
  const facts = /* @__PURE__ */ new Map();
3050
2980
  const seenRoles = /* @__PURE__ */ new Set();
3051
- const tracks = graph.adjacent(graph.timeline.entityId, "timeline-track").map((track) => {
2981
+ const tracks = graph.holds(graph.timeline.entityId).map((track) => {
3052
2982
  const role = track.payload.role;
3053
2983
  if (track.entityKind !== "track" || typeof role !== "string" || !Object.hasOwn(roleOrder, role)) fail(`unsupported compatibility Track role on ${track.entityId}`);
3054
2984
  if (seenRoles.has(role)) fail(`The compatibility editor supports only one ${role} Track`);
3055
2985
  seenRoles.add(role);
3056
2986
  const typedRole = role;
3057
2987
  if (track.payload.hidden !== void 0 && typeof track.payload.hidden !== "boolean") fail("Track hidden must be boolean");
3058
- const order = track.payload.order === void 0 ? roleOrder[typedRole] : finite(track.payload.order, "Track order");
3059
- const trackFacts = graph.adjacent(track.entityId, "track-clip").map((clip) => {
3060
- if (clip.entityKind !== "clip" || graph.adjacent(clip.entityId, "track-clip").length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
3061
- const marker = graph.one(clip.entityId, "clip-marker", "sequence-marker");
3062
- const content = graph.one(marker.entityId, "marker-content");
3063
- assertTrackContent(graph, typedRole, content);
3064
- const source = range(marker.payload.sourceRange, "sourceRange");
3065
- const durationFact = marker.payload.duration;
3066
- if (!object(durationFact)) fail(`Marker ${marker.entityId} requires duration`);
3067
- const duration = positive(durationFact.mode === "from-source" ? source.end - source.start : durationFact.value, "duration");
2988
+ const order = roleOrder[typedRole];
2989
+ const trackFacts = graph.holds(track.entityId).map((clip, clipIndex) => {
2990
+ if (clip.entityKind !== "clip" || graph.heldBy(clip.entityId).length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
2991
+ const marker = graph.one(clip.entityId, "marker", "sequence-marker");
2992
+ const held = graph.holds(clip.entityId);
2993
+ if (held.length !== 1) fail(`Clip ${clip.entityId} must contain exactly one content entity`);
2994
+ const content = held[0];
2995
+ const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover-recording"] : typedRole === "caption" ? ["caption"] : ["audio"];
2996
+ if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
2997
+ const inMs = milliseconds(marker.payload.inMs, "inMs");
2998
+ const outMs = milliseconds(marker.payload.outMs, "outMs");
2999
+ const rate = marker.payload.rate === void 0 ? 1 : positive(marker.payload.rate, "rate");
3000
+ const duration = positive(Math.round((outMs - inMs) / rate), "duration");
3068
3001
  if (!Number.isSafeInteger(duration)) fail("Compatibility duration must be whole milliseconds");
3069
- const fillTimeline = marker.payload.durationPolicy === "timeline";
3070
- if (marker.payload.durationPolicy !== void 0 && (!fillTimeline || typedRole !== "bgm")) fail("durationPolicy is only supported for timeline-filling background Audio");
3002
+ const fillTimeline = marker.payload.fillTimeline === true;
3003
+ if (marker.payload.fillTimeline !== void 0 && (!fillTimeline || typedRole !== "bgm")) fail("fillTimeline is only supported for timeline-filling background Audio");
3071
3004
  const anchors = graph.rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
3072
3005
  if (anchors.length > 1) fail(`Clip ${clip.entityId} has multiple anchors`);
3073
- const target = marker.payload.targetRange === void 0 ? void 0 : range(marker.payload.targetRange, "targetRange");
3006
+ const startMs = marker.payload.startMs === void 0 ? void 0 : milliseconds(marker.payload.startMs, "startMs");
3074
3007
  const anchorId = anchors[0]?.endpoint1EntityId;
3075
3008
  let mode;
3076
3009
  let absoluteStart;
3077
3010
  let anchorOffset;
3078
- let clipOrder;
3079
3011
  if (anchorId !== void 0) {
3080
- if (target !== void 0) fail(`Anchored Marker ${marker.entityId} cannot also have targetRange`);
3012
+ if (startMs === void 0) fail(`Anchored Marker ${marker.entityId} requires startMs inside its host`);
3081
3013
  mode = "anchored";
3082
- anchorOffset = milliseconds(marker.payload.anchorOffset, "anchorOffset");
3083
- } else {
3084
- if (marker.payload.anchorOffset !== void 0) fail(`Marker ${marker.entityId} has anchorOffset without a clip-anchor Relation`);
3085
- if (target !== void 0) {
3086
- mode = "absolute";
3087
- absoluteStart = milliseconds(target.start, "target start");
3088
- milliseconds(target.end, "target end");
3089
- if (Math.abs(target.end - target.start - duration) > 1e-6) fail(`Marker ${marker.entityId} targetRange must match its effective duration`);
3090
- } else {
3091
- mode = "sequential";
3092
- if (clip.payload.order === void 0) fail(`Clip ${clip.entityId} has no placement: targetRange or explicit order is required`);
3093
- clipOrder = finite(clip.payload.order, "Clip order");
3094
- }
3095
- }
3014
+ anchorOffset = startMs;
3015
+ } else if (startMs !== void 0) {
3016
+ mode = "absolute";
3017
+ absoluteStart = startMs;
3018
+ } else mode = "sequential";
3096
3019
  const fact = {
3097
3020
  clip,
3098
3021
  marker,
@@ -3101,7 +3024,7 @@ function resolveGraphLayout(graph) {
3101
3024
  role: typedRole,
3102
3025
  duration,
3103
3026
  mode,
3104
- order: clipOrder,
3027
+ order: clipIndex,
3105
3028
  absoluteStart,
3106
3029
  anchorId,
3107
3030
  anchorOffset,
@@ -3111,8 +3034,7 @@ function resolveGraphLayout(graph) {
3111
3034
  return fact;
3112
3035
  });
3113
3036
  if (typedRole === "bgm" && trackFacts.length > 1) fail("The compatibility editor supports only one background Audio Clip");
3114
- const sequential = trackFacts.filter((fact) => fact.mode === "sequential").sort((left, right) => left.order - right.order);
3115
- if (new Set(sequential.map((fact) => fact.order)).size !== sequential.length) fail("sequential Clips must have distinct order values");
3037
+ const sequential = trackFacts.filter((fact) => fact.mode === "sequential");
3116
3038
  sequential.forEach((fact, index) => {
3117
3039
  fact.predecessor = sequential[index - 1]?.clip.entityId;
3118
3040
  });
@@ -3179,33 +3101,6 @@ function resolveGraphLayout(graph) {
3179
3101
  }))
3180
3102
  };
3181
3103
  }
3182
- function assertTrackContent(graph, role, content) {
3183
- if (role === "speech") {
3184
- assertSpeechContent(graph, content);
3185
- return;
3186
- }
3187
- const expected = role === "video_clip" ? ["image", "video"] : role === "caption" ? ["caption"] : ["audio"];
3188
- if (!expected.includes(content.entityKind)) fail(`${role} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
3189
- }
3190
- function assertSpeechContent(graph, content) {
3191
- const assetLinks = graph.rows.relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
3192
- if (assetLinks.length !== 1 || assetLinks[0].endpoint0EntityId !== content.entityId) fail("Speech content requires one ordered from-asset binding");
3193
- const asset = graph.entity(assetLinks[0].endpoint1EntityId, "asset");
3194
- if (content.entityKind === "voiceover") {
3195
- const renders = graph.rows.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
3196
- if (renders.length !== 1 || renders[0].endpoint0EntityId !== content.entityId || graph.entity(renders[0].endpoint1EntityId).entityKind !== "phonetic-script") fail("Synthesized Speech Voiceover requires one ordered PhoneticScript render");
3197
- if (asset.payload.system !== "memota-speech") fail("Synthesized Speech Voiceover requires an explicit memota-speech key");
3198
- return;
3199
- }
3200
- if (content.entityKind === "audio") {
3201
- const sources = graph.rows.relations.filter((relation) => relation.relationKind === "audio-script-source" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
3202
- if (sources.length > 1) fail("Recorded speech has an ambiguous AudioScript source receipt");
3203
- if (sources.length === 1 && (sources[0].endpoint1EntityId !== content.entityId || graph.entity(sources[0].endpoint0EntityId).entityKind !== "audio-script")) fail("Recorded Speech AudioScript source must use ordered endpoints");
3204
- if (asset.payload.system !== "memota") fail("Recorded Speech Audio requires an explicit memota key");
3205
- return;
3206
- }
3207
- fail(`speech Track accepts voiceover or audio, got ${content.entityKind}`);
3208
- }
3209
3104
  //#endregion
3210
3105
  //#region src/document/entity-projection.ts
3211
3106
  /**
@@ -3232,7 +3127,7 @@ function projectEntityTimeline(rows) {
3232
3127
  if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
3233
3128
  if (track.role === "video_clip") {
3234
3129
  const asset = mediaAssetFacts(graph, content);
3235
- const source = range(marker.payload.sourceRange, "sourceRange");
3130
+ const source = markerWindow(marker);
3236
3131
  const external = externalLocator(asset, "memota");
3237
3132
  const speed = videoSpeed(content, marker, placement);
3238
3133
  parts[clip.entityId] = { video_clip: {
@@ -3247,35 +3142,32 @@ function projectEntityTimeline(rows) {
3247
3142
  } else if (track.role === "speech") {
3248
3143
  const asset = mediaAssetFacts(graph, content);
3249
3144
  requireUntrimmedAudio(content, marker, placement);
3145
+ const external = externalLocator(asset, "memota-speech");
3146
+ const renderEdges = graph.adjacent(content.entityId, "derived-from");
3250
3147
  const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
3251
- const isSynthesized = content.entityKind === "voiceover";
3252
- const external = externalLocator(asset, isSynthesized ? "memota-speech" : "memota");
3253
- const assembled = isSynthesized ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
3148
+ const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "derived-from", "phonetic-script").entityId) : void 0;
3254
3149
  if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
3255
3150
  const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
3256
3151
  if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
3257
3152
  const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
3258
- const speech = {
3153
+ parts[clip.entityId] = { speech: {
3259
3154
  id: clip.entityId,
3260
3155
  kind: "speech",
3261
3156
  media_duration_ms: placement.duration,
3262
3157
  audio_storage_key: storageKey(asset),
3263
3158
  origin_speech_id: external.key,
3264
- origin_asset_system: external.system,
3265
- audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "audio-script-source").length > 0 ? assembleScriptText(graph.one(content.entityId, "audio-script-source", "audio-script")) : ""),
3159
+ audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "derived-from").length > 0 ? assembleScriptText(graph.one(content.entityId, "derived-from", "audio-script")) : ""),
3266
3160
  volume: volume.data,
3267
3161
  voice: object(voice) ? {
3268
3162
  id: typeof voice.key === "string" ? voice.key : void 0,
3269
3163
  name: typeof voice.name === "string" ? voice.name : void 0
3270
3164
  } : void 0,
3271
3165
  caption_ids: captionIds
3272
- };
3273
- parts[clip.entityId] = { speech };
3166
+ } };
3274
3167
  } else if (track.role === "caption") {
3275
3168
  const assembled = assembleCaptionContent(rows, content.entityId);
3276
3169
  const text = assembled.text;
3277
3170
  const cues = captionDisplayCues(assembled, marker, placement.duration);
3278
- if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
3279
3171
  const style = captionStyle(content.payload.style);
3280
3172
  const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
3281
3173
  const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
@@ -3292,9 +3184,9 @@ function projectEntityTimeline(rows) {
3292
3184
  } else {
3293
3185
  const asset = mediaAssetFacts(graph, content);
3294
3186
  const external = externalLocator(asset, "memota");
3295
- if (marker.payload.durationPolicy !== "timeline") fail("Background Audio requires an explicit timeline-fill durationPolicy");
3296
- if (marker.payload.timeRemapping !== void 0) fail("BGM timeRemapping is not supported by this reader");
3297
- const source = range(marker.payload.sourceRange, "sourceRange");
3187
+ if (marker.payload.fillTimeline !== true) fail("Background Audio requires an explicit fillTimeline");
3188
+ if (marker.payload.rate !== void 0) fail("BGM rate is not supported by this reader");
3189
+ const source = markerWindow(marker);
3298
3190
  if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
3299
3191
  parts[clip.entityId] = { bgm: {
3300
3192
  id: clip.entityId,
@@ -3350,10 +3242,7 @@ function storageKey(asset) {
3350
3242
  function externalLocator(asset, system) {
3351
3243
  const { system: found, key } = asset.payload;
3352
3244
  if (found !== system || typeof key !== "string" || key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " key");
3353
- return {
3354
- key,
3355
- system
3356
- };
3245
+ return { key };
3357
3246
  }
3358
3247
  function captionStyle(value) {
3359
3248
  if (value === void 0) return void 0;
@@ -3385,22 +3274,26 @@ function captionStyle(value) {
3385
3274
  return style;
3386
3275
  }
3387
3276
  function requireUntrimmedAudio(content, marker, placement) {
3388
- const source = range(marker.payload.sourceRange, "sourceRange");
3389
- if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.timeRemapping !== void 0) fail("Voiceover compatibility requires the complete, unremapped take");
3277
+ const source = markerWindow(marker);
3278
+ if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.rate !== void 0) fail("Voiceover compatibility requires the complete, unremapped take");
3279
+ }
3280
+ /** The Marker's window, in the {start,end} shape this compatibility reader speaks. */
3281
+ function markerWindow(marker) {
3282
+ return range({
3283
+ start: marker.payload.inMs,
3284
+ end: marker.payload.outMs
3285
+ }, "Sequence Marker window");
3390
3286
  }
3391
3287
  function videoSpeed(content, marker, placement) {
3392
- const remapping = marker.payload.timeRemapping;
3393
- if (content.entityKind === "image" && remapping === void 0) return void 0;
3394
- const source = range(marker.payload.sourceRange, "sourceRange");
3395
- if (remapping !== void 0) {
3396
- if (!object(remapping) || remapping.kind !== "linear") fail("unsupported timeRemapping on " + marker.entityId);
3397
- if (Object.keys(remapping).some((key) => key !== "kind" && key !== "rate" && key !== "mode")) fail("Unsupported linear timeRemapping fields");
3398
- const rate = positive(remapping.rate, "linear remapping rate");
3399
- if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("timeRemapping on " + marker.entityId + " must agree with Marker duration");
3400
- if (remapping.mode !== void 0 && typeof remapping.mode !== "string") fail("Linear remapping mode must be a string");
3288
+ const stated = marker.payload.rate;
3289
+ if (content.entityKind === "image" && stated === void 0) return void 0;
3290
+ const source = markerWindow(marker);
3291
+ if (stated !== void 0) {
3292
+ const rate = positive(stated, "linear speed rate");
3293
+ if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("rate on " + marker.entityId + " must agree with Marker duration");
3401
3294
  return {
3402
3295
  category: "linear",
3403
- mode: typeof remapping.mode === "string" ? remapping.mode : "constant",
3296
+ mode: "constant",
3404
3297
  config: { linear: { speed: rate } }
3405
3298
  };
3406
3299
  }
@@ -3415,7 +3308,7 @@ function videoSpeed(content, marker, placement) {
3415
3308
  function captionDisplayCues(assembled, marker, duration) {
3416
3309
  const ranges = assembled.caption.payload.segmentRanges;
3417
3310
  if (!Array.isArray(ranges)) return void 0;
3418
- const source = range(marker.payload.sourceRange, "Caption sourceRange");
3311
+ const source = markerWindow(marker);
3419
3312
  const scale = duration / (source.end - source.start);
3420
3313
  return assembled.segments.flatMap((segment) => {
3421
3314
  const timing = ranges.find((entry) => object(entry) && entry.segmentId === segment.segmentId);
@@ -3439,23 +3332,11 @@ function captionDisplayCues(assembled, marker, duration) {
3439
3332
  }
3440
3333
  //#endregion
3441
3334
  //#region src/entity-crdt/field-changes.ts
3442
- const atomicFields = new Set([
3443
- "sequence",
3444
- "sourceRange",
3445
- "targetRange",
3446
- "duration",
3447
- "durationMs",
3448
- "timeRemapping",
3449
- "textRange",
3450
- "selection",
3451
- "external"
3452
- ]);
3453
- const textFields = new Set(["text", "phonemeScript"]);
3454
- const orderedFields = new Set([
3455
- "segments",
3456
- "segmentRanges",
3457
- "baseEntityIds"
3458
- ]);
3335
+ /** Merge behaviour is declared with the field shapes in @mengine/medeo-dsl. */
3336
+ const merge = (key) => FIELD_MERGE[key];
3337
+ const atomicFields = { has: (key) => merge(key) === "atomic" };
3338
+ const textFields = { has: (key) => merge(key) === "text" };
3339
+ const orderedFields = { has: (key) => merge(key) === "list" };
3459
3340
  const unsafeKeys = new Set([
3460
3341
  "__proto__",
3461
3342
  "prototype",
@@ -3743,27 +3624,24 @@ function phoneticScriptAssetOf(rows, entityId) {
3743
3624
  }
3744
3625
  //#endregion
3745
3626
  //#region src/entity-crdt/ordering.ts
3746
- /** Domain order is scoped by membership; relation storage has no global order. */
3627
+ /**
3628
+ * Domain order is scoped by membership: the members of one container, in the
3629
+ * order their `contain` Relations sit in the rows.
3630
+ *
3631
+ * Nothing carries a copy of its own position, so this reads the order rather
3632
+ * than deriving it — the rows are already in it.
3633
+ */
3747
3634
  function entityOrderGroups(rows) {
3748
3635
  const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
3749
3636
  const groups = /* @__PURE__ */ new Map();
3750
3637
  for (const relation of rows.relations) {
3751
- const childKind = relation.relationKind === "timeline-track" ? "track" : relation.relationKind === "track-clip" ? "clip" : null;
3752
- if (!childKind) continue;
3753
- const endpoints = [relation.endpoint0EntityId, relation.endpoint1EntityId];
3754
- const childId = endpoints.find((id) => entities.get(id)?.entityKind === childKind);
3755
- if (!childId) continue;
3756
- const child = entities.get(childId);
3757
- if (typeof child.payload.order !== "number") continue;
3758
- const parent = endpoints.find((id) => id !== childId);
3759
- const values = groups.get(parent) ?? [];
3760
- values.push({
3761
- id: childId,
3762
- order: child.payload.order
3763
- });
3764
- groups.set(parent, values);
3638
+ if (!isSequencedRelationKind(relation.relationKind)) continue;
3639
+ const childKind = entities.get(String(relation.endpoint1EntityId))?.entityKind;
3640
+ if (childKind !== "track" && childKind !== "clip") continue;
3641
+ const parent = String(relation.endpoint0EntityId);
3642
+ groups.set(parent, [...groups.get(parent) ?? [], String(relation.endpoint1EntityId)]);
3765
3643
  }
3766
- return new Map([...groups].map(([parent, values]) => [parent, values.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((item) => item.id)]));
3644
+ return groups;
3767
3645
  }
3768
3646
  function ordering(doc) {
3769
3647
  const map = doc.getMap("medeo").get("ordering");
@@ -3806,19 +3684,6 @@ function writeEntityOrder(doc, group, roots, token) {
3806
3684
  //#region src/entity-crdt/entity-document.ts
3807
3685
  const LORO_ENTITY_SCHEMA = "medeo.entities.loro.v1";
3808
3686
  const MATERIALIZE_ORIGIN = "medeo.materialize";
3809
- /** Read persisted relation roots before current entity versions are projected. */
3810
- function readEntityRelationRecords(doc) {
3811
- if (root(doc).get("schema") !== "medeo.entities.loro.v1") throw new Error("Document has no Loro entity state");
3812
- return relationRecords(doc).map(({ rootId, identity, map, deleted }) => ({
3813
- relationId: createRelationId(rootId),
3814
- relationKind: identity.kind,
3815
- endpoint0EntityId: createEntityId(identity.endpoints[0]),
3816
- endpoint1EntityId: createEntityId(identity.endpoints[1]),
3817
- metadata: requireMap(map, "metadata").toJSON(),
3818
- trace: requireMap(map, "trace").toJSON(),
3819
- deleted
3820
- }));
3821
- }
3822
3687
  function hash(value) {
3823
3688
  return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
3824
3689
  }
@@ -3845,12 +3710,7 @@ function content(map) {
3845
3710
  return requireMap(map, "content").toJSON();
3846
3711
  }
3847
3712
  function entityContent(doc, key) {
3848
- const payload = content(workspace(doc, key));
3849
- const order = entityOrdinal(doc, key);
3850
- return order === void 0 ? payload : {
3851
- ...payload,
3852
- order
3853
- };
3713
+ return content(workspace(doc, key));
3854
3714
  }
3855
3715
  function bases(map) {
3856
3716
  const links = requireMap(map, "bases");
@@ -3910,17 +3770,24 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
3910
3770
  }
3911
3771
  };
3912
3772
  });
3913
- const relations = activeRelationRecords(doc, ids).map(({ rootId, identity, map }) => {
3914
- const endpoints = identity.endpoints.map((id) => currentId(doc, id, ids));
3915
- return {
3916
- relationId: createRelationId(equalJson(endpoints, identity.endpoints) ? rootId : `relation_${hash([rootId, endpoints])}`),
3917
- relationKind: identity.kind,
3773
+ const relations = [];
3774
+ for (const key of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3775
+ const map = section(doc, "relations").get(key);
3776
+ if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
3777
+ const original = map.get("identity");
3778
+ if (original.endpoints.some((id) => !ids.has(family(doc, id)))) continue;
3779
+ const endpoints = original.endpoints.map((id) => currentId(doc, id, ids));
3780
+ const relationId = equalJson(endpoints, original.endpoints) ? key : `relation_${hash([key, endpoints])}`;
3781
+ relations.push({
3782
+ relationId: createRelationId(relationId),
3783
+ relationKind: original.kind,
3918
3784
  endpoint0EntityId: createEntityId(endpoints[0]),
3919
3785
  endpoint1EntityId: createEntityId(endpoints[1]),
3920
3786
  metadata: requireMap(map, "metadata").toJSON(),
3921
3787
  trace: requireMap(map, "trace").toJSON()
3922
- };
3923
- });
3788
+ });
3789
+ }
3790
+ sortSequencedRelations(doc, relations);
3924
3791
  const project = {};
3925
3792
  for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
3926
3793
  if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
@@ -3939,32 +3806,6 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
3939
3806
  }
3940
3807
  return state;
3941
3808
  }
3942
- function relationRecords(doc) {
3943
- const records = [];
3944
- for (const rootId of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3945
- const map = section(doc, "relations").get(rootId);
3946
- if (!(map instanceof LoroMap)) throw new Error(`Invalid relation record ${rootId}`);
3947
- const value = map.get("identity");
3948
- if (!isJsonObject(value) || typeof value.kind !== "string" || !Array.isArray(value.endpoints) || value.endpoints.length !== 2 || !value.endpoints.every((endpoint) => typeof endpoint === "string")) throw new Error(`Invalid relation identity ${rootId}`);
3949
- const identity = {
3950
- kind: value.kind,
3951
- endpoints: [value.endpoints[0], value.endpoints[1]]
3952
- };
3953
- records.push({
3954
- rootId,
3955
- identity,
3956
- map,
3957
- deleted: map.get("deleted") === true
3958
- });
3959
- }
3960
- return records;
3961
- }
3962
- function activeRelationRecords(doc, ids) {
3963
- return relationRecords(doc).filter(({ identity, deleted }) => {
3964
- if (deleted) return false;
3965
- return identity.endpoints.every((id) => ids.has(family(doc, id)));
3966
- });
3967
- }
3968
3809
  /** Native editing containers survive domain version changes and merge independently. */
3969
3810
  var LoroEntityDocument = class LoroEntityDocument {
3970
3811
  doc;
@@ -4195,47 +4036,16 @@ var LoroEntityDraft = class {
4195
4036
  });
4196
4037
  }
4197
4038
  link(row) {
4198
- if (row.relationKind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
4199
- if (row.relationKind === "generated" || row.relationKind === "clip-anchor") throw new Error(`Author ordered ${row.relationKind} Relations with the dedicated role-named method`);
4200
4039
  this.record({
4201
4040
  type: "relation",
4202
4041
  row: clone(row)
4203
4042
  });
4204
4043
  }
4205
- linkGenerated(input) {
4206
- this.record({
4207
- type: "relation",
4208
- row: {
4209
- relationId: input.relationId,
4210
- relationKind: "generated",
4211
- endpoint0EntityId: input.outputEntityId,
4212
- endpoint1EntityId: input.inputEntityId,
4213
- metadata: {},
4214
- trace: clone(input.trace ?? {})
4215
- }
4216
- });
4217
- }
4218
- linkClipAnchor(input) {
4219
- this.record({
4220
- type: "relation",
4221
- row: {
4222
- relationId: input.relationId,
4223
- relationKind: "clip-anchor",
4224
- endpoint0EntityId: input.childClipEntityId,
4225
- endpoint1EntityId: input.hostClipEntityId,
4226
- metadata: {},
4227
- trace: clone(input.trace ?? {})
4228
- }
4229
- });
4230
- }
4231
- patchRelationTrace(relationId, trace) {
4232
- const relation = this.snapshot().rows.relations.find((row) => row.relationId === relationId);
4233
- if (relation == null) throw new Error(`Unknown relation ${relationId}`);
4234
- if (relation.relationKind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
4044
+ patchRelation(relationId, metadata, trace) {
4235
4045
  this.record({
4236
4046
  type: "relation-fields",
4237
4047
  root: this.relationRoot(relationId),
4238
- metadata: clone(relation.metadata),
4048
+ metadata: clone(metadata),
4239
4049
  trace: clone(trace)
4240
4050
  });
4241
4051
  }
@@ -4331,9 +4141,8 @@ var LoroEntityDraft = class {
4331
4141
  const key = this.relationRoot(relationId);
4332
4142
  const map = section(this.doc, "relations").get(key);
4333
4143
  if (!(map instanceof LoroMap) || map.get("deleted") === true) throw new Error("Unknown relation");
4334
- if (map.get("identity").kind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
4335
4144
  for (const change of changes) {
4336
- if (change.path[0] !== "trace" || change.path.length < 2) throw new Error("Relation changes may only edit trace fields");
4145
+ if (!["metadata", "trace"].includes(change.path[0]) || change.path.length < 2) throw new Error("Relation changes may only edit metadata or trace fields");
4337
4146
  const payload = {
4338
4147
  metadata: requireMap(map, "metadata").toJSON(),
4339
4148
  trace: requireMap(map, "trace").toJSON()
@@ -4514,6 +4323,30 @@ function materialize(doc, allowSelectionConflicts = false) {
4514
4323
  const project = section(doc, "project");
4515
4324
  for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
4516
4325
  }
4326
+ /**
4327
+ * Put each container's sequenced Relations in the order its native list states.
4328
+ *
4329
+ * Per container, never across: a Timeline's Tracks and a Track's Clips are two
4330
+ * separate orders, and sorting them together would interleave them.
4331
+ */
4332
+ function sortSequencedRelations(doc, relations) {
4333
+ const containers = /* @__PURE__ */ new Map();
4334
+ for (const [slot, relation] of relations.entries()) {
4335
+ if (!isSequencedRelationKind(relation.relationKind)) continue;
4336
+ const container = String(relation.endpoint0EntityId);
4337
+ const ordinal = entityOrdinal(doc, family(doc, String(relation.endpoint1EntityId))) ?? slot;
4338
+ containers.set(container, [...containers.get(container) ?? [], {
4339
+ slot,
4340
+ relation,
4341
+ ordinal
4342
+ }]);
4343
+ }
4344
+ for (const members of containers.values()) {
4345
+ const slots = members.map((member) => member.slot);
4346
+ const ordered = [...members].sort((left, right) => left.ordinal - right.ordinal);
4347
+ slots.forEach((slot, at) => relations[slot] = ordered[at].relation);
4348
+ }
4349
+ }
4517
4350
  //#endregion
4518
4351
  //#region src/document/document-mutation-guard.ts
4519
4352
  /**
@@ -4849,4 +4682,4 @@ function createPlainMemoryAdapter(document, options) {
4849
4682
  return new PlainMemoryAdapter(document, options);
4850
4683
  }
4851
4684
  //#endregion
4852
- export { VideoDocumentValidationError as $, resolveEntityTimelineLayout as A, selectAudioScriptSegment as B, canonicalJson as C, effectiveVideoClipDurationMs as Ct, resolveFieldPath as D, videoDocumentMirrorSchema as Dt, assertFieldChanges as E, recordEntries as Et, assembleCaptionContent as F, createRelationId as G, variantBaseEntityIds as H, assembleEntityContent as I, buildInitialVideoDocument as J, hasSequence as K, assemblePhoneticScriptContent as L, validateEntityRelationTransition as M, decodeEntityRelationRows as N, projectEntityTimeline as O, base64ToBytes as Ot, ScriptCompositionError as P, toVideoDocument as Q, assembleScriptText as R, scriptSourceAssetOf as S, DEFAULT_UNIT_TIME_MS as St, applyFieldChanges as T, partUnionToDraft as Tt, isJsonObject as U, updateEntityFields as V, createEntityId as W, derivePositionFromAbs as X, buildSpeechHostMap as Y, fromVideoDocument as Z, audioScriptAssetHash as _, syncAggregatedClipsTimePosition as _t, createMirrorVideoDocument as a, ensureLaneTrack as at, phoneticScriptAssetHash as b, partDurationMs as bt, DocumentMutationGuard as c, laneTrackId as ct, LoroEntityDraft as d, arrangeMainTrackSeamlessly as dt, assertValidVideoDocument as et, readEntityDocument as f, fillMainTrackTimeGaps as ft, audioScriptAssetFields as g, resolveSpeechOverlapByShiftingVideos as gt, audioScriptAssetContent as h, resolveAllSpeechOverlaps as ht, MirrorVideoDocumentAdapter as i, LANE_KINDS_IN_STACK_ORDER as it, EntityTimelineProjectionError as j, EntityProjectionGraph as k, bytesToBase64 as kt, LORO_ENTITY_SCHEMA as l, solveVideoDocument as lt, entityOrderGroups as m, recalculateTimelineDuration as mt, createPlainMemoryAdapter as n, partUnionSchema as nt, createMirrorVideoDocumentAdapter as o, findLaneTrack as ot, readEntityRelationRecords as p, reassignSpeechesToVideoClipsByTime as pt, isMediaAssetVariantKind as q, generatePartId as r, videoDocumentSchema as rt, readVideoDocumentFromDraft as s, hasResolvedPlacement as st, PlainMemoryAdapter as t, validateVideoDocument as tt, LoroEntityDocument as u, cascadeAfterVideoClipChanges as ut, audioScriptAssetOf as v, TIMELINE_SKELETON_DURATION_MS as vt, equalJson as w, speedOf as wt, phoneticScriptAssetOf as x, safeDurationMs as xt, phoneticScriptAssetContent as y, isEmptyVideoClip as yt, findComposedAudioScript as z };
4685
+ export { assertValidVideoDocument as $, decodeEntityRelationRows as A, updateEntityFields as B, applyFieldChanges as C, speedOf as Ct, EntityProjectionGraph as D, base64ToBytes as Dt, projectEntityTimeline as E, videoDocumentMirrorSchema as Et, assembleEntityContent as F, hasSequence as G, isJsonObject as H, assemblePhoneticScriptContent as I, buildSpeechHostMap as J, isMediaAssetVariantKind as K, assembleScriptText as L, relationsTo as M, ScriptCompositionError as N, resolveEntityTimelineLayout as O, bytesToBase64 as Ot, assembleCaptionContent as P, VideoDocumentValidationError as Q, findComposedAudioScript as R, equalJson as S, effectiveVideoClipDurationMs as St, resolveFieldPath as T, recordEntries as Tt, createEntityId as U, variantBaseEntityIds as V, createRelationId as W, fromVideoDocument as X, derivePositionFromAbs as Y, toVideoDocument as Z, audioScriptAssetOf as _, TIMELINE_SKELETON_DURATION_MS as _t, createMirrorVideoDocument as a, findLaneTrack as at, phoneticScriptAssetOf as b, safeDurationMs as bt, DocumentMutationGuard as c, solveVideoDocument as ct, LoroEntityDraft as d, fillMainTrackTimeGaps as dt, validateVideoDocument as et, readEntityDocument as f, reassignSpeechesToVideoClipsByTime as ft, audioScriptAssetHash as g, syncAggregatedClipsTimePosition as gt, audioScriptAssetFields as h, resolveSpeechOverlapByShiftingVideos as ht, MirrorVideoDocumentAdapter as i, ensureLaneTrack as it, relationsFrom as j, EntityTimelineProjectionError as k, LORO_ENTITY_SCHEMA as l, cascadeAfterVideoClipChanges as lt, audioScriptAssetContent as m, resolveAllSpeechOverlaps as mt, createPlainMemoryAdapter as n, videoDocumentSchema as nt, createMirrorVideoDocumentAdapter as o, hasResolvedPlacement as ot, entityOrderGroups as p, recalculateTimelineDuration as pt, buildInitialVideoDocument as q, generatePartId as r, LANE_KINDS_IN_STACK_ORDER as rt, readVideoDocumentFromDraft as s, laneTrackId as st, PlainMemoryAdapter as t, partUnionSchema as tt, LoroEntityDocument as u, arrangeMainTrackSeamlessly as ut, phoneticScriptAssetContent as v, isEmptyVideoClip as vt, assertFieldChanges as w, partUnionToDraft as wt, scriptSourceAssetOf as x, DEFAULT_UNIT_TIME_MS as xt, phoneticScriptAssetHash as y, partDurationMs as yt, selectAudioScriptSegment as z };