@mengine/medeo-client 2.0.1-alpha.22 → 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.
@@ -1446,6 +1446,20 @@ function emptyLaneTracks() {
1446
1446
  }));
1447
1447
  }
1448
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
1449
1463
  //#region ../medeo-dsl/src/entities.ts
1450
1464
  /**
1451
1465
  * What every known entity kind declares.
@@ -1467,12 +1481,6 @@ function emptyLaneTracks() {
1467
1481
  * flowed, is the `from-asset` / `to-asset` Relation's answer alone.
1468
1482
  */
1469
1483
  const ENTITY_KINDS = Object.freeze({
1470
- axvideo: Object.freeze([
1471
- "Container",
1472
- "Sequence",
1473
- "Visual",
1474
- "Audible"
1475
- ]),
1476
1484
  timeline: Object.freeze(["Container"]),
1477
1485
  track: Object.freeze(["Container"]),
1478
1486
  clip: Object.freeze(["Container"]),
@@ -1488,7 +1496,7 @@ const ENTITY_KINDS = Object.freeze({
1488
1496
  "Audible",
1489
1497
  "MediaFile"
1490
1498
  ]),
1491
- voiceover: Object.freeze([
1499
+ "voiceover-recording": Object.freeze([
1492
1500
  "Sequence",
1493
1501
  "Audible",
1494
1502
  "MediaFile"
@@ -1500,7 +1508,6 @@ const ENTITY_KINDS = Object.freeze({
1500
1508
  ]),
1501
1509
  voice: Object.freeze(["Container", "IPBound"]),
1502
1510
  "sequence-marker": Object.freeze([]),
1503
- viewport: Object.freeze([]),
1504
1511
  "audio-script": Object.freeze(["SegmentText", "MediaFile"]),
1505
1512
  "phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
1506
1513
  caption: Object.freeze([
@@ -1533,7 +1540,7 @@ function isSequenceFields(value) {
1533
1540
  return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
1534
1541
  }
1535
1542
  function isMediaAssetVariantKind(kind) {
1536
- return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover";
1543
+ return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover-recording";
1537
1544
  }
1538
1545
  function isKnownEntityKind(kind) {
1539
1546
  return Object.hasOwn(ENTITY_KINDS, kind);
@@ -1805,7 +1812,7 @@ function entityToRow(entity) {
1805
1812
  //#region ../medeo-dsl/src/invariants.ts
1806
1813
  /** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
1807
1814
  /** Validates one complete set of entities and its authoritative Relation rows. */
1808
- function validateEntityRelationSet(entityRefs, index, options) {
1815
+ function validateEntityRelationSet(entityRefs, index) {
1809
1816
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
1810
1817
  const entityIds = new Set(entities.map((entity) => entity.entityId));
1811
1818
  const rows = {
@@ -1829,17 +1836,13 @@ function validateEntityRelationSet(entityRefs, index, options) {
1829
1836
  if (current.entityKind === "sequence-marker") {
1830
1837
  const marker = entity;
1831
1838
  issues.push(...validateMarkerUse(entity, index));
1832
- issues.push(...validateMarkerRanges(marker, {
1833
- source: (left, right) => options.compareMarkerPoints(marker, "source", left, right),
1834
- target: (left, right) => options.compareMarkerPoints(marker, "target", left, right)
1835
- }));
1836
- issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
1839
+ issues.push(...validateMarkerRanges(marker));
1840
+ issues.push(...validateMarkerSourceBounds(marker, index));
1837
1841
  }
1838
1842
  if (current.entityKind === "clip") {
1839
1843
  issues.push(...validateClipAdmission(entity, index));
1840
1844
  issues.push(...validateClipPlacement(entity, index));
1841
1845
  }
1842
- if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
1843
1846
  if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
1844
1847
  if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
1845
1848
  issues.push(...validateSequenceComposition(entity));
@@ -1848,18 +1851,16 @@ function validateEntityRelationSet(entityRefs, index, options) {
1848
1851
  return issues;
1849
1852
  }
1850
1853
  function validateMarkerUse(marker, index) {
1851
- const relations = [...index.relationsOf(marker)];
1852
- const clipEdges = ofKind(relations, "clip-marker");
1853
- const axVideoEdges = ofKind(relations, "axvideo-marker");
1854
- const contentEdges = ofKind(relations, "marker-content");
1855
- const timelineEdges = ofKind(relations, "marker-timeline");
1856
- 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");
1857
1858
  const issues = [];
1858
1859
  if (scriptEdges.length > 0) {
1859
- if (clipEdges.length + axVideoEdges.length + contentEdges.length + timelineEdges.length > 0) issues.push({
1860
+ if (clipEdges.length > 0) issues.push({
1860
1861
  code: "marker_container_xor",
1861
1862
  entityId: marker.entityId,
1862
- 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"
1863
1864
  });
1864
1865
  const ranges = marker.current().segmentRanges;
1865
1866
  if (!ranges?.length) issues.push({
@@ -1886,64 +1887,44 @@ function validateMarkerUse(marker, index) {
1886
1887
  entityId: marker.entityId,
1887
1888
  message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
1888
1889
  });
1889
- if (clipEdges.length + axVideoEdges.length !== 1) issues.push({
1890
+ if (clipEdges.length !== 1) issues.push({
1890
1891
  code: "marker_container_xor",
1891
1892
  entityId: marker.entityId,
1892
- message: "Sequence Marker must have exactly one Clip XOR AXVideo container relation"
1893
- });
1894
- if (contentEdges.length + timelineEdges.length !== 1) issues.push({
1895
- code: "marker_content_xor",
1896
- entityId: marker.entityId,
1897
- message: "Sequence Marker must have exactly one Sequence content XOR Timeline relation"
1898
- });
1899
- if (clipEdges.length === 1 && timelineEdges.length === 1 || axVideoEdges.length === 1 && contentEdges.length === 1) issues.push({
1900
- code: "marker_pair_mismatch",
1901
- entityId: marker.entityId,
1902
- message: "Only Clip+Content or AXVideo+Timeline Marker relation pairs are valid"
1893
+ message: "A display Sequence Marker belongs to exactly one Clip"
1903
1894
  });
1904
1895
  return issues;
1905
1896
  }
1906
1897
  function validateClipAdmission(clip, index) {
1907
- const markerEdges = ofKind([...index.relationsOf(clip)], "clip-marker");
1898
+ const markerEdges = markersOn(clip, index);
1908
1899
  if (markerEdges.length !== 1) return [{
1909
1900
  code: "clip_marker_cardinality",
1910
1901
  entityId: clip.entityId,
1911
1902
  message: "Clip must have exactly one authoritative Clip-Marker Relation"
1912
1903
  }];
1913
- const marker = markerEdges[0]?.other(clip)?.deref();
1914
- if (marker == null) return [{
1904
+ if (markerEdges[0]?.other(clip)?.deref() == null) return [{
1915
1905
  code: "clip_content_cardinality",
1916
1906
  entityId: clip.entityId,
1917
- message: "Clip must resolve one live Sequence Marker and one content Relation"
1907
+ message: "Clip must resolve one live Sequence Marker"
1918
1908
  }];
1919
- const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
1909
+ const contentEdges = holds(clip, index);
1920
1910
  if (contentEdges.length !== 1) return [{
1921
1911
  code: "clip_content_cardinality",
1922
1912
  entityId: clip.entityId,
1923
- message: "Clip Marker must resolve exactly one content Relation"
1913
+ message: "Clip must contain exactly one content entity"
1924
1914
  }];
1925
- const content = contentEdges[0]?.other(marker)?.deref()?.current();
1915
+ const content = contentEdges[0]?.other(clip)?.deref()?.current();
1926
1916
  if (content == null) return [{
1927
1917
  code: "clip_content_cardinality",
1928
1918
  entityId: clip.entityId,
1929
- message: "Clip Marker content Relation must resolve one live entity"
1919
+ message: "Clip content Relation must resolve one live entity"
1930
1920
  }];
1931
1921
  if (!hasSequence(content)) return [{
1932
1922
  code: "clip_content_not_sequence",
1933
1923
  entityId: clip.entityId,
1934
- message: `Clip Marker resolves to non-Sequence entity "${content.entityId}"`
1924
+ message: `Clip contains non-Sequence entity "${content.entityId}"`
1935
1925
  }];
1936
1926
  return [];
1937
1927
  }
1938
- function validateAXVideoAdmission(axVideo, index) {
1939
- const markerEdges = ofKind([...index.relationsOf(axVideo)], "axvideo-marker");
1940
- if (markerEdges.length === 1 && markerEdges[0]?.other(axVideo)?.deref() != null) return [];
1941
- return [{
1942
- code: "axvideo_marker_cardinality",
1943
- entityId: axVideo.entityId,
1944
- message: "AXVideo must have exactly one live AXVideo-Marker Relation"
1945
- }];
1946
- }
1947
1928
  /** Validate a Caption against the complete entity set, including its directly held bases. */
1948
1929
  function validateCaptionComposition(caption, index, entities = []) {
1949
1930
  return validateVariantComposition(caption, index, entities, "caption");
@@ -1970,33 +1951,30 @@ function validateVariantComposition(variant, index, entities, kind) {
1970
1951
  return issues;
1971
1952
  }
1972
1953
  function validateClipPlacement(clip, index) {
1973
- const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
1954
+ const marker = markersOn(clip, index)[0]?.other(clip)?.deref();
1974
1955
  if (marker?.current().entityKind !== "sequence-marker") return [];
1975
1956
  const markerValue = marker.current();
1976
1957
  const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
1977
- const hasOrder = clip.current().order !== void 0;
1978
- const hasTarget = markerValue.targetRange !== void 0;
1979
- const hasAnchorOffset = markerValue.anchorOffset !== void 0;
1958
+ const hasStart = markerValue.startMs !== void 0;
1980
1959
  const issues = [];
1981
1960
  if (anchors.length > 1) issues.push({
1982
1961
  code: "clip_anchor_cardinality",
1983
1962
  entityId: clip.entityId,
1984
1963
  message: "A Clip may follow at most one host Clip"
1985
1964
  });
1986
- const hasAnchor = anchors.length === 1;
1987
- if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
1965
+ if (anchors.length > 0 && !hasStart) issues.push({
1988
1966
  code: "clip_placement_invalid",
1989
1967
  entityId: clip.entityId,
1990
- 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"
1991
1969
  });
1992
- if (markerValue.durationPolicy === "timeline") {
1993
- const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
1994
- 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();
1995
1973
  const trackRole = track?.entityKind === "track" ? track.role : void 0;
1996
- if (content?.entityKind !== "audio" || trackRole !== "bgm" || !hasOrder && !hasTarget) issues.push({
1974
+ if (content?.entityKind !== "audio" || trackRole !== "bgm") issues.push({
1997
1975
  code: "timeline_duration_policy_invalid",
1998
1976
  entityId: marker.entityId,
1999
- 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"
2000
1978
  });
2001
1979
  }
2002
1980
  return issues;
@@ -2028,34 +2006,28 @@ function validateClipAnchorCycles(entities, index) {
2028
2006
  }
2029
2007
  return issues;
2030
2008
  }
2031
- function validateMarkerRanges(marker, compare) {
2032
- const current = marker.current();
2033
- const issues = [];
2034
- 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 [{
2035
2013
  code: "marker_source_range_empty",
2036
2014
  entityId: marker.entityId,
2037
- message: "Sequence Marker sourceRange must be a non-empty half-open interval"
2038
- });
2039
- if (current.targetRange != null && !(compare.target(current.targetRange.start, current.targetRange.end) < 0)) issues.push({
2040
- code: "marker_target_range_empty",
2041
- entityId: marker.entityId,
2042
- message: "Sequence Marker targetRange must be a non-empty half-open interval"
2043
- });
2044
- return issues;
2015
+ message: "Sequence Marker outMs must be a finite millisecond value greater than inMs"
2016
+ }];
2045
2017
  }
2046
- function validateMarkerSourceBounds(marker, index, compare) {
2047
- 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);
2048
2022
  if (contentEdges.length !== 1) return [];
2049
- const content = contentEdges[0]?.other(marker)?.deref()?.current();
2023
+ const content = contentEdges[0]?.other(clip)?.deref()?.current();
2050
2024
  if (content == null || !hasSequence(content)) return [];
2051
- const sourceRange = marker.current().sourceRange;
2052
- const startVsOrigin = compare(sourceRange.start, 0);
2053
- const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
2054
- 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 [];
2055
2027
  return [{
2056
2028
  code: "marker_source_out_of_bounds",
2057
2029
  entityId: marker.entityId,
2058
- 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"
2059
2031
  }];
2060
2032
  }
2061
2033
  function validateSequenceComposition(entity) {
@@ -2102,21 +2074,20 @@ function validateKnownEntityPayload(entity) {
2102
2074
  "extent",
2103
2075
  "sampling",
2104
2076
  "coordinateSpace",
2105
- "sourceRange",
2106
- "targetRange",
2107
- "duration",
2077
+ "inMs",
2078
+ "outMs",
2108
2079
  "startMs",
2109
- "endMs"
2080
+ "rate"
2110
2081
  ]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
2111
2082
  }
2112
2083
  switch (entity.entityKind) {
2113
2084
  case "track":
2114
2085
  validateOptionalField(value, "hidden", "boolean", problems);
2115
2086
  validateOptionalField(value, "role", "string", problems);
2116
- validateOptionalFiniteNumber(value, "order", problems);
2117
2087
  break;
2118
2088
  case "video":
2119
2089
  case "audio":
2090
+ case "voiceover-recording":
2120
2091
  validateDurationPayload(value, "own", problems);
2121
2092
  break;
2122
2093
  case "caption":
@@ -2130,12 +2101,7 @@ function validateKnownEntityPayload(entity) {
2130
2101
  validateDurationPayload(value, "none", problems);
2131
2102
  break;
2132
2103
  case "axvideo":
2133
- for (const retired of [
2134
- "extent",
2135
- "sampling",
2136
- "coordinateSpace",
2137
- "durationMs"
2138
- ]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
2104
+ validateDurationPayload(value, "none", problems);
2139
2105
  break;
2140
2106
  case "sequence-marker":
2141
2107
  validateMarkerPayload(value, problems);
@@ -2149,7 +2115,6 @@ function validateKnownEntityPayload(entity) {
2149
2115
  case "timeline":
2150
2116
  case "viewport": break;
2151
2117
  case "clip":
2152
- validateOptionalFiniteNumber(value, "order", problems);
2153
2118
  validateOptionalFiniteNumber(value, "volume", problems);
2154
2119
  if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
2155
2120
  break;
@@ -2183,17 +2148,16 @@ function validateDurationPayload(value, expected, problems) {
2183
2148
  if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
2184
2149
  }
2185
2150
  function validateMarkerPayload(value, problems) {
2186
- validateRange(value.sourceRange, "sourceRange", true, problems);
2187
- if (Object.hasOwn(value, "targetRange") && value.targetRange !== void 0) validateRange(value.targetRange, "targetRange", true, problems);
2188
- const duration = value.duration;
2189
- if (!isRecord(duration)) problems.push("duration must be an object");
2190
- else if (duration.mode === "fixed") {
2191
- if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
2192
- } else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
2193
- if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
2194
- 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");
2195
2156
  if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
2196
2157
  }
2158
+ function isWholeMilliseconds(value) {
2159
+ return typeof value === "number" && Number.isSafeInteger(value);
2160
+ }
2197
2161
  /** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
2198
2162
  function validateSegmentRanges(value, problems) {
2199
2163
  if (!Array.isArray(value)) {
@@ -2292,19 +2256,10 @@ function validateCaptionPayload(value, problems) {
2292
2256
  "strokeColor"
2293
2257
  ]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
2294
2258
  }
2295
- function validateRange(value, path, required, problems) {
2296
- if (!isRecord(value)) {
2297
- if (required) problems.push(`${path} must be an object`);
2298
- return;
2299
- }
2300
- if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
2301
- if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
2302
- }
2303
2259
  /** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
2304
2260
  function validatePhoneticScriptPayload(value, problems) {
2305
2261
  if (value.segments !== void 0) validateScriptPayload(value, problems);
2306
2262
  if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
2307
- if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
2308
2263
  }
2309
2264
  function validateScriptPayload(value, problems) {
2310
2265
  if (!Array.isArray(value.segments)) {
@@ -2321,9 +2276,8 @@ function validateScriptPayload(value, problems) {
2321
2276
  "endMs",
2322
2277
  "start_ms",
2323
2278
  "end_ms",
2324
- "sourceRange",
2325
- "targetRange",
2326
- "duration"
2279
+ "inMs",
2280
+ "outMs"
2327
2281
  ]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
2328
2282
  if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
2329
2283
  if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
@@ -2378,11 +2332,11 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2378
2332
  if ([
2379
2333
  "video",
2380
2334
  "audio",
2335
+ "voiceover-recording",
2381
2336
  "image",
2382
- "caption",
2383
- "axvideo"
2337
+ "caption"
2384
2338
  ].includes(entityKind) && /^durationMs$/.test(path)) return true;
2385
- 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;
2386
2340
  if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2387
2341
  if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
2388
2342
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
@@ -2450,19 +2404,57 @@ function expectedDurationShape(kind) {
2450
2404
  function isPositiveInteger(value) {
2451
2405
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
2452
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
+ }
2453
2423
  function ofKind(relations, kind) {
2454
2424
  return relations.filter((relation) => relation.kind === kind);
2455
2425
  }
2456
2426
  //#endregion
2457
2427
  //#region ../medeo-dsl/src/relation-specs.ts
2458
- const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2459
- const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
2460
- const clipMarkerRelationSpec = emptySpec("clip-marker", "clip", "sequence-marker");
2461
- const axVideoMarkerRelationSpec = emptySpec("axvideo-marker", "axvideo", "sequence-marker");
2462
- const markerTimelineRelationSpec = emptySpec("marker-timeline", "sequence-marker", "timeline");
2463
- const markerContentRelationSpec = Object.freeze({
2464
- kind: "marker-content",
2465
- validateEndpoints: (endpoints) => hasMarkerAndSequence(endpoints),
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",
2466
2458
  validateMetadata: isEmptyMetadata
2467
2459
  });
2468
2460
  /**
@@ -2476,6 +2468,8 @@ const markerContentRelationSpec = Object.freeze({
2476
2468
  */
2477
2469
  const fromAssetRelationSpec = Object.freeze({
2478
2470
  kind: "from-asset",
2471
+ ordered: true,
2472
+ sequenced: false,
2479
2473
  validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
2480
2474
  validateMetadata: isAssetBinding
2481
2475
  });
@@ -2496,117 +2490,87 @@ const fromAssetRelationSpec = Object.freeze({
2496
2490
  */
2497
2491
  const toAssetRelationSpec = Object.freeze({
2498
2492
  kind: "to-asset",
2493
+ ordered: true,
2494
+ sequenced: false,
2499
2495
  validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
2500
2496
  validateMetadata: isAssetBinding
2501
2497
  });
2502
- /** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
2503
- const generatedRelationSpec = Object.freeze({
2504
- kind: "generated",
2505
- validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
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)),
2506
2521
  validateMetadata: isEmptyMetadata
2507
2522
  });
2508
2523
  const captionAlignmentRelationSpec = Object.freeze({
2509
2524
  kind: "caption-alignment",
2510
- 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"])),
2511
2528
  validateMetadata: isCaptionAlignmentMetadata
2512
2529
  });
2513
2530
  /** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
2514
2531
  const clipAnchorRelationSpec = Object.freeze({
2515
2532
  kind: "clip-anchor",
2533
+ ordered: true,
2534
+ sequenced: false,
2516
2535
  validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
2517
2536
  validateMetadata: isEmptyMetadata
2518
2537
  });
2519
2538
  /**
2520
- * The Voiceover and the PhoneticScript it was spoken from; kinds determine
2521
- * roles regardless of endpoint positions.
2522
- *
2523
- * Every Voiceover must carry one: which words were said, and how, is the only
2524
- * thing that makes speech reproducible, and a Voiceover that cannot answer it
2525
- * is refused when its plan is built.
2526
- */
2527
- const phoneticScriptRenderRelationSpec = Object.freeze({
2528
- kind: "phonetic-script-render",
2529
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["phonetic-script"])),
2530
- validateMetadata: isEmptyMetadata
2531
- });
2532
- /**
2533
2539
  * The timbre identity a Voiceover was synthesized with. Voice stays an
2534
2540
  * identity: it never carries the audio itself, so the link between the two is a
2535
2541
  * Relation rather than one shared entity, and several takes can name one voice.
2536
2542
  */
2537
2543
  const voiceTimbreRelationSpec = Object.freeze({
2538
2544
  kind: "voice-timbre",
2539
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["voice"])),
2540
- validateMetadata: isEmptyMetadata
2541
- });
2542
- /** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
2543
- const audioScriptSourceRelationSpec = Object.freeze({
2544
- kind: "audio-script-source",
2545
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
2546
- "audio",
2547
- "voiceover",
2548
- "video"
2549
- ])),
2550
- validateMetadata: isEmptyMetadata
2551
- });
2552
- /**
2553
- * `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
2554
- * whose `segmentRanges` hold directly assigned per-Segment time values. The
2555
- * script keeps no Sequence and no Clip admission; annotation Markers cannot
2556
- * enter Clip/AXVideo use chains (enforced by the marker-use invariant).
2557
- */
2558
- const audioScriptMarkerRelationSpec = Object.freeze({
2559
- kind: "audio-script-marker",
2560
- 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"])),
2561
2548
  validateMetadata: isEmptyMetadata
2562
2549
  });
2563
2550
  /** Built-in kinds are reserved; callers may add specs only under new names. */
2564
2551
  const builtInRelationSpecs = Object.freeze([
2565
- timelineTrackRelationSpec,
2566
- trackClipRelationSpec,
2567
- clipMarkerRelationSpec,
2568
- markerContentRelationSpec,
2569
- axVideoMarkerRelationSpec,
2570
- markerTimelineRelationSpec,
2552
+ containRelationSpec,
2553
+ markerRelationSpec,
2571
2554
  fromAssetRelationSpec,
2572
2555
  toAssetRelationSpec,
2573
- generatedRelationSpec,
2556
+ derivedFromRelationSpec,
2574
2557
  captionAlignmentRelationSpec,
2575
2558
  clipAnchorRelationSpec,
2576
- phoneticScriptRenderRelationSpec,
2577
- voiceTimbreRelationSpec,
2578
- audioScriptSourceRelationSpec,
2579
- audioScriptMarkerRelationSpec
2559
+ voiceTimbreRelationSpec
2580
2560
  ]);
2581
- function emptySpec(kind, a, b) {
2582
- return metadataSpec(kind, a, b, isEmptyMetadata);
2583
- }
2584
- function metadataSpec(kind, a, b, validateMetadata) {
2585
- return Object.freeze({
2586
- kind,
2587
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set([a]), new Set([b])),
2588
- validateMetadata
2589
- });
2590
- }
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
+ */
2591
2567
  function hasKinds(endpoints, aKinds, bKinds) {
2592
- const first = endpoints[0].current().entityKind;
2593
- const second = endpoints[1].current().entityKind;
2594
- return aKinds.has(first) && bKinds.has(second) || aKinds.has(second) && bKinds.has(first);
2595
- }
2596
- function hasMarkerAndSequence(endpoints) {
2597
- const first = endpoints[0].current();
2598
- const second = endpoints[1].current();
2599
- return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
2568
+ return aKinds.has(endpoints[0].current().entityKind) && bKinds.has(endpoints[1].current().entityKind);
2600
2569
  }
2601
- /** Exactly one end is the Asset, so the other end is unambiguously the bound entity. */
2570
+ /** `(entity, asset)`: the bound entity first, the Asset naming its bytes second. */
2602
2571
  function hasExactlyOneAsset(endpoints) {
2603
- const kinds = endpoints.map((endpoint) => endpoint.current().entityKind);
2604
- const assets = kinds.map((kind) => kind === "asset");
2605
- if (assets[0] === assets[1]) return false;
2606
- return declaresKind(assets[0] ? kinds[1] : kinds[0], "MediaFile");
2607
- }
2608
- function isGeneratedMedia(entity) {
2609
- return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voiceover";
2572
+ const bound = endpoints[0].current().entityKind;
2573
+ return endpoints[1].current().entityKind === "asset" && bound !== "asset" && declaresKind(bound, "MediaFile");
2610
2574
  }
2611
2575
  function isEmptyMetadata(value) {
2612
2576
  return isJsonObject(value) && Object.keys(value).length === 0;
@@ -2619,6 +2583,18 @@ function isAssetBinding(value) {
2619
2583
  function isCaptionAlignmentMetadata(value) {
2620
2584
  return isJsonObject(value) && Object.hasOwn(value, "alignment");
2621
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
+ }
2622
2598
  //#endregion
2623
2599
  //#region ../medeo-dsl/src/relations.ts
2624
2600
  function createEntityRef(entity) {
@@ -2671,16 +2647,15 @@ var BiRelationIndex = class {
2671
2647
  canonicalRefs = /* @__PURE__ */ new Map();
2672
2648
  byRelationId = /* @__PURE__ */ new Map();
2673
2649
  link(input) {
2674
- if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
2675
- 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`);
2676
2651
  return this.linkValidated(input);
2677
2652
  }
2678
- /** Author `generated(output, input)` without exposing positional arguments. */
2679
- linkGenerated(input) {
2653
+ /** Author `derived-from(product, source)` without exposing positional arguments. */
2654
+ linkDerivedFrom(input) {
2680
2655
  return this.linkValidated({
2681
2656
  relationId: input.relationId,
2682
- spec: generatedRelationSpec,
2683
- endpoints: [input.output, input.input],
2657
+ spec: derivedFromRelationSpec,
2658
+ endpoints: [input.product, input.source],
2684
2659
  metadata: {},
2685
2660
  trace: input.trace
2686
2661
  });
@@ -2695,26 +2670,6 @@ var BiRelationIndex = class {
2695
2670
  trace: input.trace
2696
2671
  });
2697
2672
  }
2698
- /** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
2699
- linkPhoneticScriptRender(input) {
2700
- return this.linkValidated({
2701
- relationId: input.relationId,
2702
- spec: phoneticScriptRenderRelationSpec,
2703
- endpoints: [input.output, input.phoneticScript],
2704
- metadata: {},
2705
- trace: input.trace
2706
- });
2707
- }
2708
- /** Author `audio-script-source(script, source)` without exposing positional arguments. */
2709
- linkAudioScriptSource(input) {
2710
- return this.linkValidated({
2711
- relationId: input.relationId,
2712
- spec: audioScriptSourceRelationSpec,
2713
- endpoints: [input.script, input.source],
2714
- metadata: {},
2715
- trace: input.trace
2716
- });
2717
- }
2718
2673
  /**
2719
2674
  * Rehydrate a persisted row after resolving its spec and endpoint refs.
2720
2675
  *
@@ -2730,7 +2685,6 @@ var BiRelationIndex = class {
2730
2685
  this.assertRelationIdAvailable(input.relationId);
2731
2686
  const builtInSpec = builtInSpecByKind.get(input.spec.kind);
2732
2687
  if (builtInSpec != null && builtInSpec !== input.spec) throw new Error(`Relation kind "${input.spec.kind}" must use its built-in specification`);
2733
- if (isForbiddenAuthoritativeRelation(input.spec.kind, input.endpoints)) throw new Error("A direct Clip-Content Relation is derived-only and cannot be authoritative");
2734
2688
  if (!input.spec.validateEndpoints(input.endpoints)) throw new Error(`Relation "${input.spec.kind}" received invalid endpoints`);
2735
2689
  if (!input.spec.validateMetadata(input.metadata)) throw new Error(`Relation "${input.spec.kind}" received invalid metadata`);
2736
2690
  this.assertCanonicalRefAvailable(first);
@@ -2779,12 +2733,6 @@ var BiRelationIndex = class {
2779
2733
  }
2780
2734
  };
2781
2735
  const builtInSpecByKind = new Map(builtInRelationSpecs.map((spec) => [spec.kind, spec]));
2782
- function isForbiddenAuthoritativeRelation(kind, endpoints) {
2783
- if (kind === "clip-content") return true;
2784
- const first = endpoints[0].current();
2785
- const second = endpoints[1].current();
2786
- return first.entityKind === "clip" && hasSequence(second) || second.entityKind === "clip" && hasSequence(first);
2787
- }
2788
2736
  //#endregion
2789
2737
  //#region ../medeo-dsl/src/entity-relation-rows.ts
2790
2738
  var InvalidEntityRelationRowsError = class extends Error {
@@ -2873,7 +2821,7 @@ function decodeEntityRelationRows(rows, options) {
2873
2821
  issues.push(errorMessage(error));
2874
2822
  }
2875
2823
  }
2876
- 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}`);
2877
2825
  if (issues.length > 0) throw new InvalidEntityRelationRowsError(issues);
2878
2826
  return {
2879
2827
  entitiesById,
@@ -2986,7 +2934,7 @@ var EntityProjectionGraph = class {
2986
2934
  timeline;
2987
2935
  constructor(rows) {
2988
2936
  this.rows = rows;
2989
- const decoded = decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2937
+ const decoded = decodeEntityRelationRows(rows, {});
2990
2938
  this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
2991
2939
  const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
2992
2940
  if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
@@ -3000,6 +2948,13 @@ var EntityProjectionGraph = class {
3000
2948
  adjacent(id, kind) {
3001
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));
3002
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
+ }
3003
2958
  one(id, relation, kind) {
3004
2959
  const values = this.adjacent(id, relation);
3005
2960
  if (values.length !== 1 || kind !== void 0 && values[0]?.entityKind !== kind) fail(`entity ${id} requires exactly one ${relation}${kind === void 0 ? "" : ` to ${kind}`}`);
@@ -3023,52 +2978,44 @@ function resolveEntityTimelineLayout(rows) {
3023
2978
  function resolveGraphLayout(graph) {
3024
2979
  const facts = /* @__PURE__ */ new Map();
3025
2980
  const seenRoles = /* @__PURE__ */ new Set();
3026
- const tracks = graph.adjacent(graph.timeline.entityId, "timeline-track").map((track) => {
2981
+ const tracks = graph.holds(graph.timeline.entityId).map((track) => {
3027
2982
  const role = track.payload.role;
3028
2983
  if (track.entityKind !== "track" || typeof role !== "string" || !Object.hasOwn(roleOrder, role)) fail(`unsupported compatibility Track role on ${track.entityId}`);
3029
2984
  if (seenRoles.has(role)) fail(`The compatibility editor supports only one ${role} Track`);
3030
2985
  seenRoles.add(role);
3031
2986
  const typedRole = role;
3032
2987
  if (track.payload.hidden !== void 0 && typeof track.payload.hidden !== "boolean") fail("Track hidden must be boolean");
3033
- const order = track.payload.order === void 0 ? roleOrder[typedRole] : finite(track.payload.order, "Track order");
3034
- const trackFacts = graph.adjacent(track.entityId, "track-clip").map((clip) => {
3035
- if (clip.entityKind !== "clip" || graph.adjacent(clip.entityId, "track-clip").length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
3036
- const marker = graph.one(clip.entityId, "clip-marker", "sequence-marker");
3037
- const content = graph.one(marker.entityId, "marker-content");
3038
- const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover"] : typedRole === "caption" ? ["caption"] : ["audio"];
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"];
3039
2996
  if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
3040
- const source = range(marker.payload.sourceRange, "sourceRange");
3041
- const durationFact = marker.payload.duration;
3042
- if (!object(durationFact)) fail(`Marker ${marker.entityId} requires duration`);
3043
- const duration = positive(durationFact.mode === "from-source" ? source.end - source.start : durationFact.value, "duration");
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");
3044
3001
  if (!Number.isSafeInteger(duration)) fail("Compatibility duration must be whole milliseconds");
3045
- const fillTimeline = marker.payload.durationPolicy === "timeline";
3046
- 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");
3047
3004
  const anchors = graph.rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
3048
3005
  if (anchors.length > 1) fail(`Clip ${clip.entityId} has multiple anchors`);
3049
- 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");
3050
3007
  const anchorId = anchors[0]?.endpoint1EntityId;
3051
3008
  let mode;
3052
3009
  let absoluteStart;
3053
3010
  let anchorOffset;
3054
- let clipOrder;
3055
3011
  if (anchorId !== void 0) {
3056
- 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`);
3057
3013
  mode = "anchored";
3058
- anchorOffset = milliseconds(marker.payload.anchorOffset, "anchorOffset");
3059
- } else {
3060
- if (marker.payload.anchorOffset !== void 0) fail(`Marker ${marker.entityId} has anchorOffset without a clip-anchor Relation`);
3061
- if (target !== void 0) {
3062
- mode = "absolute";
3063
- absoluteStart = milliseconds(target.start, "target start");
3064
- milliseconds(target.end, "target end");
3065
- if (Math.abs(target.end - target.start - duration) > 1e-6) fail(`Marker ${marker.entityId} targetRange must match its effective duration`);
3066
- } else {
3067
- mode = "sequential";
3068
- if (clip.payload.order === void 0) fail(`Clip ${clip.entityId} has no placement: targetRange or explicit order is required`);
3069
- clipOrder = finite(clip.payload.order, "Clip order");
3070
- }
3071
- }
3014
+ anchorOffset = startMs;
3015
+ } else if (startMs !== void 0) {
3016
+ mode = "absolute";
3017
+ absoluteStart = startMs;
3018
+ } else mode = "sequential";
3072
3019
  const fact = {
3073
3020
  clip,
3074
3021
  marker,
@@ -3077,7 +3024,7 @@ function resolveGraphLayout(graph) {
3077
3024
  role: typedRole,
3078
3025
  duration,
3079
3026
  mode,
3080
- order: clipOrder,
3027
+ order: clipIndex,
3081
3028
  absoluteStart,
3082
3029
  anchorId,
3083
3030
  anchorOffset,
@@ -3087,8 +3034,7 @@ function resolveGraphLayout(graph) {
3087
3034
  return fact;
3088
3035
  });
3089
3036
  if (typedRole === "bgm" && trackFacts.length > 1) fail("The compatibility editor supports only one background Audio Clip");
3090
- const sequential = trackFacts.filter((fact) => fact.mode === "sequential").sort((left, right) => left.order - right.order);
3091
- 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");
3092
3038
  sequential.forEach((fact, index) => {
3093
3039
  fact.predecessor = sequential[index - 1]?.clip.entityId;
3094
3040
  });
@@ -3181,7 +3127,7 @@ function projectEntityTimeline(rows) {
3181
3127
  if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
3182
3128
  if (track.role === "video_clip") {
3183
3129
  const asset = mediaAssetFacts(graph, content);
3184
- const source = range(marker.payload.sourceRange, "sourceRange");
3130
+ const source = markerWindow(marker);
3185
3131
  const external = externalLocator(asset, "memota");
3186
3132
  const speed = videoSpeed(content, marker, placement);
3187
3133
  parts[clip.entityId] = { video_clip: {
@@ -3197,9 +3143,9 @@ function projectEntityTimeline(rows) {
3197
3143
  const asset = mediaAssetFacts(graph, content);
3198
3144
  requireUntrimmedAudio(content, marker, placement);
3199
3145
  const external = externalLocator(asset, "memota-speech");
3200
- const renderEdges = graph.adjacent(content.entityId, "phonetic-script-render");
3146
+ const renderEdges = graph.adjacent(content.entityId, "derived-from");
3201
3147
  const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
3202
- const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? 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;
3203
3149
  if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
3204
3150
  const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
3205
3151
  if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
@@ -3210,7 +3156,7 @@ function projectEntityTimeline(rows) {
3210
3156
  media_duration_ms: placement.duration,
3211
3157
  audio_storage_key: storageKey(asset),
3212
3158
  origin_speech_id: external.key,
3213
- 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")) : ""),
3214
3160
  volume: volume.data,
3215
3161
  voice: object(voice) ? {
3216
3162
  id: typeof voice.key === "string" ? voice.key : void 0,
@@ -3222,7 +3168,6 @@ function projectEntityTimeline(rows) {
3222
3168
  const assembled = assembleCaptionContent(rows, content.entityId);
3223
3169
  const text = assembled.text;
3224
3170
  const cues = captionDisplayCues(assembled, marker, placement.duration);
3225
- if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
3226
3171
  const style = captionStyle(content.payload.style);
3227
3172
  const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
3228
3173
  const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
@@ -3239,9 +3184,9 @@ function projectEntityTimeline(rows) {
3239
3184
  } else {
3240
3185
  const asset = mediaAssetFacts(graph, content);
3241
3186
  const external = externalLocator(asset, "memota");
3242
- if (marker.payload.durationPolicy !== "timeline") fail("Background Audio requires an explicit timeline-fill durationPolicy");
3243
- if (marker.payload.timeRemapping !== void 0) fail("BGM timeRemapping is not supported by this reader");
3244
- 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);
3245
3190
  if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
3246
3191
  parts[clip.entityId] = { bgm: {
3247
3192
  id: clip.entityId,
@@ -3329,22 +3274,26 @@ function captionStyle(value) {
3329
3274
  return style;
3330
3275
  }
3331
3276
  function requireUntrimmedAudio(content, marker, placement) {
3332
- const source = range(marker.payload.sourceRange, "sourceRange");
3333
- 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");
3334
3286
  }
3335
3287
  function videoSpeed(content, marker, placement) {
3336
- const remapping = marker.payload.timeRemapping;
3337
- if (content.entityKind === "image" && remapping === void 0) return void 0;
3338
- const source = range(marker.payload.sourceRange, "sourceRange");
3339
- if (remapping !== void 0) {
3340
- if (!object(remapping) || remapping.kind !== "linear") fail("unsupported timeRemapping on " + marker.entityId);
3341
- if (Object.keys(remapping).some((key) => key !== "kind" && key !== "rate" && key !== "mode")) fail("Unsupported linear timeRemapping fields");
3342
- const rate = positive(remapping.rate, "linear remapping rate");
3343
- if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("timeRemapping on " + marker.entityId + " must agree with Marker duration");
3344
- 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");
3345
3294
  return {
3346
3295
  category: "linear",
3347
- mode: typeof remapping.mode === "string" ? remapping.mode : "constant",
3296
+ mode: "constant",
3348
3297
  config: { linear: { speed: rate } }
3349
3298
  };
3350
3299
  }
@@ -3359,7 +3308,7 @@ function videoSpeed(content, marker, placement) {
3359
3308
  function captionDisplayCues(assembled, marker, duration) {
3360
3309
  const ranges = assembled.caption.payload.segmentRanges;
3361
3310
  if (!Array.isArray(ranges)) return void 0;
3362
- const source = range(marker.payload.sourceRange, "Caption sourceRange");
3311
+ const source = markerWindow(marker);
3363
3312
  const scale = duration / (source.end - source.start);
3364
3313
  return assembled.segments.flatMap((segment) => {
3365
3314
  const timing = ranges.find((entry) => object(entry) && entry.segmentId === segment.segmentId);
@@ -3383,23 +3332,11 @@ function captionDisplayCues(assembled, marker, duration) {
3383
3332
  }
3384
3333
  //#endregion
3385
3334
  //#region src/entity-crdt/field-changes.ts
3386
- const atomicFields = new Set([
3387
- "sequence",
3388
- "sourceRange",
3389
- "targetRange",
3390
- "duration",
3391
- "durationMs",
3392
- "timeRemapping",
3393
- "textRange",
3394
- "selection",
3395
- "external"
3396
- ]);
3397
- const textFields = new Set(["text", "phonemeScript"]);
3398
- const orderedFields = new Set([
3399
- "segments",
3400
- "segmentRanges",
3401
- "baseEntityIds"
3402
- ]);
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" };
3403
3340
  const unsafeKeys = new Set([
3404
3341
  "__proto__",
3405
3342
  "prototype",
@@ -3687,27 +3624,24 @@ function phoneticScriptAssetOf(rows, entityId) {
3687
3624
  }
3688
3625
  //#endregion
3689
3626
  //#region src/entity-crdt/ordering.ts
3690
- /** 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
+ */
3691
3634
  function entityOrderGroups(rows) {
3692
3635
  const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
3693
3636
  const groups = /* @__PURE__ */ new Map();
3694
3637
  for (const relation of rows.relations) {
3695
- const childKind = relation.relationKind === "timeline-track" ? "track" : relation.relationKind === "track-clip" ? "clip" : null;
3696
- if (!childKind) continue;
3697
- const endpoints = [relation.endpoint0EntityId, relation.endpoint1EntityId];
3698
- const childId = endpoints.find((id) => entities.get(id)?.entityKind === childKind);
3699
- if (!childId) continue;
3700
- const child = entities.get(childId);
3701
- if (typeof child.payload.order !== "number") continue;
3702
- const parent = endpoints.find((id) => id !== childId);
3703
- const values = groups.get(parent) ?? [];
3704
- values.push({
3705
- id: childId,
3706
- order: child.payload.order
3707
- });
3708
- 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)]);
3709
3643
  }
3710
- 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;
3711
3645
  }
3712
3646
  function ordering(doc) {
3713
3647
  const map = doc.getMap("medeo").get("ordering");
@@ -3776,12 +3710,7 @@ function content(map) {
3776
3710
  return requireMap(map, "content").toJSON();
3777
3711
  }
3778
3712
  function entityContent(doc, key) {
3779
- const payload = content(workspace(doc, key));
3780
- const order = entityOrdinal(doc, key);
3781
- return order === void 0 ? payload : {
3782
- ...payload,
3783
- order
3784
- };
3713
+ return content(workspace(doc, key));
3785
3714
  }
3786
3715
  function bases(map) {
3787
3716
  const links = requireMap(map, "bases");
@@ -3858,6 +3787,7 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
3858
3787
  trace: requireMap(map, "trace").toJSON()
3859
3788
  });
3860
3789
  }
3790
+ sortSequencedRelations(doc, relations);
3861
3791
  const project = {};
3862
3792
  for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
3863
3793
  if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
@@ -4393,6 +4323,30 @@ function materialize(doc, allowSelectionConflicts = false) {
4393
4323
  const project = section(doc, "project");
4394
4324
  for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
4395
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
+ }
4396
4350
  //#endregion
4397
4351
  //#region src/document/document-mutation-guard.ts
4398
4352
  /**
@@ -4728,4 +4682,4 @@ function createPlainMemoryAdapter(document, options) {
4728
4682
  return new PlainMemoryAdapter(document, options);
4729
4683
  }
4730
4684
  //#endregion
4731
- export { partUnionSchema as $, decodeEntityRelationRows as A, isJsonObject as B, applyFieldChanges as C, recordEntries as Ct, EntityProjectionGraph as D, projectEntityTimeline as E, bytesToBase64 as Et, assembleScriptText as F, buildInitialVideoDocument as G, createRelationId as H, findComposedAudioScript as I, fromVideoDocument as J, buildSpeechHostMap as K, selectAudioScriptSegment as L, assembleCaptionContent as M, assembleEntityContent as N, resolveEntityTimelineLayout as O, assemblePhoneticScriptContent as P, validateVideoDocument as Q, updateEntityFields as R, equalJson as S, partUnionToDraft as St, resolveFieldPath as T, base64ToBytes as Tt, hasSequence as U, createEntityId as V, isMediaAssetVariantKind as W, VideoDocumentValidationError as X, toVideoDocument as Y, assertValidVideoDocument as Z, audioScriptAssetOf as _, partDurationMs as _t, createMirrorVideoDocument as a, laneTrackId as at, phoneticScriptAssetOf as b, effectiveVideoClipDurationMs as bt, DocumentMutationGuard as c, arrangeMainTrackSeamlessly as ct, LoroEntityDraft as d, recalculateTimelineDuration as dt, videoDocumentSchema as et, readEntityDocument as f, resolveAllSpeechOverlaps as ft, audioScriptAssetHash as g, isEmptyVideoClip as gt, audioScriptAssetFields as h, TIMELINE_SKELETON_DURATION_MS as ht, MirrorVideoDocumentAdapter as i, hasResolvedPlacement as it, ScriptCompositionError as j, EntityTimelineProjectionError as k, LORO_ENTITY_SCHEMA as l, fillMainTrackTimeGaps as lt, audioScriptAssetContent as m, syncAggregatedClipsTimePosition as mt, createPlainMemoryAdapter as n, ensureLaneTrack as nt, createMirrorVideoDocumentAdapter as o, solveVideoDocument as ot, entityOrderGroups as p, resolveSpeechOverlapByShiftingVideos as pt, derivePositionFromAbs as q, generatePartId as r, findLaneTrack as rt, readVideoDocumentFromDraft as s, cascadeAfterVideoClipChanges as st, PlainMemoryAdapter as t, LANE_KINDS_IN_STACK_ORDER as tt, LoroEntityDocument as u, reassignSpeechesToVideoClipsByTime as ut, phoneticScriptAssetContent as v, safeDurationMs as vt, assertFieldChanges as w, videoDocumentMirrorSchema as wt, scriptSourceAssetOf as x, speedOf as xt, phoneticScriptAssetHash as y, DEFAULT_UNIT_TIME_MS as yt, variantBaseEntityIds 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 };