videobook-engine 2.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +87 -41
  2. package/dist/artifact-names.d.ts +6 -0
  3. package/dist/artifact-names.d.ts.map +1 -0
  4. package/dist/artifact-names.js +56 -0
  5. package/dist/artifact-names.js.map +1 -0
  6. package/dist/artifacts.d.ts.map +1 -1
  7. package/dist/artifacts.js +57 -23
  8. package/dist/artifacts.js.map +1 -1
  9. package/dist/books.js +2 -1
  10. package/dist/books.js.map +1 -1
  11. package/dist/cas.d.ts +27 -1
  12. package/dist/cas.d.ts.map +1 -1
  13. package/dist/cas.js +78 -6
  14. package/dist/cas.js.map +1 -1
  15. package/dist/catalog-metadata.d.ts +56 -0
  16. package/dist/catalog-metadata.d.ts.map +1 -0
  17. package/dist/catalog-metadata.js +42 -0
  18. package/dist/catalog-metadata.js.map +1 -0
  19. package/dist/communications.js +4 -2
  20. package/dist/communications.js.map +1 -1
  21. package/dist/context.d.ts +6 -1
  22. package/dist/context.d.ts.map +1 -1
  23. package/dist/context.js +35 -6
  24. package/dist/context.js.map +1 -1
  25. package/dist/domain.d.ts +4 -0
  26. package/dist/domain.d.ts.map +1 -1
  27. package/dist/domain.js +870 -58
  28. package/dist/domain.js.map +1 -1
  29. package/dist/edit-policy.d.ts +3 -0
  30. package/dist/edit-policy.d.ts.map +1 -0
  31. package/dist/edit-policy.js +6 -0
  32. package/dist/edit-policy.js.map +1 -0
  33. package/dist/edits.d.ts +10 -0
  34. package/dist/edits.d.ts.map +1 -0
  35. package/dist/edits.js +1392 -0
  36. package/dist/edits.js.map +1 -0
  37. package/dist/engine-types.d.ts +110 -60
  38. package/dist/engine-types.d.ts.map +1 -1
  39. package/dist/engine-types.js +33 -0
  40. package/dist/engine-types.js.map +1 -1
  41. package/dist/engine.d.ts +69 -8
  42. package/dist/engine.d.ts.map +1 -1
  43. package/dist/engine.js +24 -18
  44. package/dist/engine.js.map +1 -1
  45. package/dist/files.d.ts.map +1 -1
  46. package/dist/files.js +31 -27
  47. package/dist/files.js.map +1 -1
  48. package/dist/fork.d.ts +147 -0
  49. package/dist/fork.d.ts.map +1 -0
  50. package/dist/fork.js +609 -0
  51. package/dist/fork.js.map +1 -0
  52. package/dist/history.d.ts +0 -4
  53. package/dist/history.d.ts.map +1 -1
  54. package/dist/history.js +166 -595
  55. package/dist/history.js.map +1 -1
  56. package/dist/index.d.ts +16 -4
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +12 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/job-queue.d.ts +2 -0
  61. package/dist/job-queue.d.ts.map +1 -1
  62. package/dist/job-queue.js +29 -2
  63. package/dist/job-queue.js.map +1 -1
  64. package/dist/media.d.ts.map +1 -1
  65. package/dist/media.js +1 -0
  66. package/dist/media.js.map +1 -1
  67. package/dist/merge-policy.d.ts +99 -0
  68. package/dist/merge-policy.d.ts.map +1 -0
  69. package/dist/merge-policy.js +310 -0
  70. package/dist/merge-policy.js.map +1 -0
  71. package/dist/metadata.d.ts.map +1 -1
  72. package/dist/metadata.js +15 -20
  73. package/dist/metadata.js.map +1 -1
  74. package/dist/migration.d.ts +9 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +534 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/mvp-contract-fixtures.d.ts +244 -0
  79. package/dist/mvp-contract-fixtures.d.ts.map +1 -0
  80. package/dist/mvp-contract-fixtures.js +556 -0
  81. package/dist/mvp-contract-fixtures.js.map +1 -0
  82. package/dist/mvp-contracts.d.ts +761 -0
  83. package/dist/mvp-contracts.d.ts.map +1 -0
  84. package/dist/mvp-contracts.js +59 -0
  85. package/dist/mvp-contracts.js.map +1 -0
  86. package/dist/mvp-time.d.ts +54 -0
  87. package/dist/mvp-time.d.ts.map +1 -0
  88. package/dist/mvp-time.js +148 -0
  89. package/dist/mvp-time.js.map +1 -0
  90. package/dist/notebook/types.d.ts +113 -8
  91. package/dist/notebook/types.d.ts.map +1 -1
  92. package/dist/order-keys.d.ts +22 -0
  93. package/dist/order-keys.d.ts.map +1 -0
  94. package/dist/order-keys.js +262 -0
  95. package/dist/order-keys.js.map +1 -0
  96. package/dist/runtime-services.js +4 -2
  97. package/dist/runtime-services.js.map +1 -1
  98. package/dist/schema.d.ts +24 -5
  99. package/dist/schema.d.ts.map +1 -1
  100. package/dist/schema.js +644 -132
  101. package/dist/schema.js.map +1 -1
  102. package/dist/search-benchmark.d.ts +4 -0
  103. package/dist/search-benchmark.d.ts.map +1 -0
  104. package/dist/search-benchmark.js +144 -0
  105. package/dist/search-benchmark.js.map +1 -0
  106. package/dist/sequences.d.ts +20 -0
  107. package/dist/sequences.d.ts.map +1 -0
  108. package/dist/sequences.js +825 -0
  109. package/dist/sequences.js.map +1 -0
  110. package/dist/similarity.js +1 -1
  111. package/dist/storage.d.ts +3 -1
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +388 -7
  114. package/dist/storage.js.map +1 -1
  115. package/dist/store.d.ts +63 -2
  116. package/dist/store.d.ts.map +1 -1
  117. package/dist/store.js +351 -65
  118. package/dist/store.js.map +1 -1
  119. package/dist/streams.d.ts +10 -0
  120. package/dist/streams.d.ts.map +1 -0
  121. package/dist/streams.js +183 -0
  122. package/dist/streams.js.map +1 -0
  123. package/dist/temporal-models.d.ts +48 -0
  124. package/dist/temporal-models.d.ts.map +1 -0
  125. package/dist/temporal-models.js +278 -0
  126. package/dist/temporal-models.js.map +1 -0
  127. package/dist/temporal-search.d.ts +29 -0
  128. package/dist/temporal-search.d.ts.map +1 -0
  129. package/dist/temporal-search.js +1507 -0
  130. package/dist/temporal-search.js.map +1 -0
  131. package/dist/transcripts.d.ts +16 -0
  132. package/dist/transcripts.d.ts.map +1 -0
  133. package/dist/transcripts.js +519 -0
  134. package/dist/transcripts.js.map +1 -0
  135. package/docs/engine-layout.md +346 -106
  136. package/docs/mvp-prd.md +918 -0
  137. package/fixtures/media/vancat.mp4 +0 -0
  138. package/fixtures/media/vancat_profile.jpg +0 -0
  139. package/fixtures/v5/contract-fixtures.json +1532 -0
  140. package/package.json +30 -5
  141. package/dist/history-types.d.ts +0 -74
  142. package/dist/history-types.d.ts.map +0 -1
  143. package/dist/history-types.js +0 -2
  144. package/dist/history-types.js.map +0 -1
  145. package/dist/timeline.d.ts +0 -9
  146. package/dist/timeline.d.ts.map +0 -1
  147. package/dist/timeline.js +0 -236
  148. package/dist/timeline.js.map +0 -1
@@ -0,0 +1,825 @@
1
+ import { ok } from "./engine-types.js";
2
+ import { resultOf, syncResultOf } from "./context.js";
3
+ import { assertUuidV7, newUuidV7 } from "./ids.js";
4
+ import { initialOrderKeys, orderKeyAfter, orderKeyBetween, reconcileOrderKeys, } from "./order-keys.js";
5
+ import { normalizeRational, normalizeSourceRange } from "./mvp-time.js";
6
+ import { canonicalJson, parseJson } from "./store.js";
7
+ import { EngineFault } from "./store.js";
8
+ export function createSequencesApi(context) {
9
+ return {
10
+ create: (input) => createSequence(context, input),
11
+ list: () => listSequences(context),
12
+ get: (sequenceId) => syncResultOf(() => requiredSequence(context, sequenceId)),
13
+ getPrimary: () => primarySequence(context),
14
+ getAtRevision: (sequenceId, revision) => syncResultOf(() => requiredSequence(context, sequenceId, revision)),
15
+ rename: (sequenceId, name) => renameSequence(context, sequenceId, name),
16
+ updateCanvas: (sequenceId, input) => updateSequenceCanvas(context, sequenceId, input),
17
+ updateTrack: (trackId, input) => updateTrack(context, trackId, input),
18
+ addTrack: (sequenceId, input) => addTrack(context, sequenceId, input),
19
+ moveTrack: (trackId, toOrdinal) => moveTrack(context, trackId, toOrdinal),
20
+ removeTrack: (trackId) => removeTrack(context, trackId),
21
+ delete: (sequenceId) => deleteSequence(context, sequenceId),
22
+ };
23
+ }
24
+ async function addTrack(context, sequenceId, input) {
25
+ return resultOf(async () => {
26
+ assertUuidV7(sequenceId, "Sequence ID");
27
+ requiredSequenceRow(context, sequenceId);
28
+ const siblings = context.store.db
29
+ .prepare(`SELECT order_key FROM sequence_tracks
30
+ WHERE sequence_id=? AND kind=?
31
+ ORDER BY order_key, track_id`)
32
+ .all(sequenceId, input.kind);
33
+ const trackId = newUuidV7();
34
+ const ordinal = siblings.length;
35
+ const orderKey = orderKeyAfter(siblings.length > 0 ? siblings[siblings.length - 1].order_key : null);
36
+ const name = input.name === undefined
37
+ ? input.kind === "caption"
38
+ ? `Captions ${ordinal + 1}`
39
+ : `${input.kind === "video" ? "Video" : "Audio"} ${ordinal + 1}`
40
+ : requiredText(input.name, "Track name");
41
+ const common = {
42
+ trackId,
43
+ sequenceId,
44
+ ordinal,
45
+ name,
46
+ enabled: true,
47
+ locked: false,
48
+ };
49
+ const track = input.kind === "video"
50
+ ? { ...common, kind: "video", blendMode: "normal" }
51
+ : input.kind === "audio"
52
+ ? { ...common, kind: "audio", muted: false, solo: false }
53
+ : { ...common, kind: "caption" };
54
+ const mutation = await context.store.semantic({
55
+ operation: "add_sequence_track",
56
+ tables: ["sequence_tracks"],
57
+ details: { sequenceId, trackId, kind: input.kind, ordinal },
58
+ writeSet: [`sequence:${sequenceId}`, `track:${trackId}`],
59
+ }, () => insertTracks(context, [{ track, orderKey }]));
60
+ return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
61
+ });
62
+ }
63
+ async function moveTrack(context, trackId, toOrdinal) {
64
+ return resultOf(async () => {
65
+ assertUuidV7(trackId, "Track ID");
66
+ if (!Number.isInteger(toOrdinal)) {
67
+ throw new Error("Track ordinal must be an integer");
68
+ }
69
+ const row = requiredTrackRow(context, trackId);
70
+ const siblings = context.store.db
71
+ .prepare(`SELECT track_id, order_key FROM sequence_tracks
72
+ WHERE sequence_id=? AND kind=?
73
+ ORDER BY order_key, track_id`)
74
+ .all(row.sequence_id, row.kind);
75
+ const currentOrdinal = siblings.findIndex((candidate) => candidate.track_id === trackId);
76
+ // A merge can leave siblings with identical order keys (forks mint keys
77
+ // independently; readers tie-break by track_id). The writer path
78
+ // self-repairs via reconcileOrderKeys before computing a between-key,
79
+ // otherwise the position between two duplicates would be unreachable.
80
+ const keyByTrack = new Map(siblings.map((sibling) => [sibling.track_id, sibling.order_key]));
81
+ const repairs = new Map();
82
+ if (new Set(keyByTrack.values()).size !== siblings.length) {
83
+ const reconciled = reconcileOrderKeys(siblings.map((sibling) => sibling.track_id), keyByTrack);
84
+ for (const [id, key] of reconciled) {
85
+ if (keyByTrack.get(id) !== key) {
86
+ repairs.set(id, key);
87
+ keyByTrack.set(id, key);
88
+ }
89
+ }
90
+ }
91
+ const others = siblings.filter((candidate) => candidate.track_id !== trackId);
92
+ const target = Math.max(0, Math.min(toOrdinal, others.length));
93
+ if (target === currentOrdinal && repairs.size === 0) {
94
+ return requiredSequence(context, row.sequence_id);
95
+ }
96
+ const keyOf = (index) => keyByTrack.get(others[index].track_id);
97
+ const orderKey = target === others.length
98
+ ? orderKeyAfter(others.length > 0 ? keyOf(others.length - 1) : null)
99
+ : orderKeyBetween(target > 0 ? keyOf(target - 1) : null, keyOf(target));
100
+ const mutation = await context.store.semantic({
101
+ operation: "move_sequence_track",
102
+ tables: ["sequence_tracks"],
103
+ details: {
104
+ sequenceId: row.sequence_id,
105
+ trackId,
106
+ kind: row.kind,
107
+ fromOrdinal: currentOrdinal,
108
+ toOrdinal: target,
109
+ ...(repairs.size > 0 ? { repairedOrderKeys: repairs.size } : {}),
110
+ },
111
+ writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
112
+ }, () => {
113
+ const update = context.store.db.prepare("UPDATE sequence_tracks SET order_key=? WHERE track_id=?");
114
+ for (const [id, key] of repairs) {
115
+ if (id !== trackId)
116
+ update.run(key, id);
117
+ }
118
+ update.run(orderKey, trackId);
119
+ });
120
+ return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
121
+ });
122
+ }
123
+ async function removeTrack(context, trackId) {
124
+ return resultOf(async () => {
125
+ const row = requiredTrackRow(context, trackId);
126
+ const clipCount = context.store.db
127
+ .prepare("SELECT COUNT(*) AS count FROM sequence_clips WHERE track_id=?")
128
+ .get(trackId);
129
+ const captionCount = context.store.db
130
+ .prepare("SELECT COUNT(*) AS count FROM caption_cues WHERE track_id=?")
131
+ .get(trackId);
132
+ if (clipCount.count > 0 || captionCount.count > 0) {
133
+ throw new EngineFault({
134
+ code: "IN_USE",
135
+ message: "A non-empty sequence track cannot be removed",
136
+ ownerId: trackId,
137
+ });
138
+ }
139
+ const kindCount = context.store.db
140
+ .prepare("SELECT COUNT(*) AS count FROM sequence_tracks WHERE sequence_id=? AND kind=?")
141
+ .get(row.sequence_id, row.kind);
142
+ if (kindCount.count <= 1) {
143
+ throw new EngineFault({
144
+ code: "IN_USE",
145
+ message: `The final ${row.kind} track cannot be removed`,
146
+ ownerId: trackId,
147
+ });
148
+ }
149
+ const ordinal = trackOrdinal(context, row);
150
+ const mutation = await context.store.semantic({
151
+ operation: "remove_sequence_track",
152
+ tables: [
153
+ "sequence_tracks",
154
+ "sequence_clips",
155
+ "clip_links",
156
+ "clip_transforms",
157
+ "transitions",
158
+ "caption_cues",
159
+ ],
160
+ details: {
161
+ sequenceId: row.sequence_id,
162
+ trackId,
163
+ kind: row.kind,
164
+ ordinal,
165
+ },
166
+ writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
167
+ }, () => {
168
+ context.store.db
169
+ .prepare("DELETE FROM sequence_tracks WHERE track_id=?")
170
+ .run(trackId);
171
+ });
172
+ return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
173
+ });
174
+ }
175
+ async function updateTrack(context, trackId, input) {
176
+ return resultOf(async () => {
177
+ const row = requiredTrackRow(context, trackId);
178
+ if (row.kind !== "audio" &&
179
+ (input.muted !== undefined || input.solo !== undefined)) {
180
+ throw new EngineFault({
181
+ code: "INVALID_INPUT",
182
+ message: "Muted and solo are only valid for audio tracks",
183
+ });
184
+ }
185
+ const name = input.name === undefined
186
+ ? row.name
187
+ : requiredText(input.name, "Track name");
188
+ const mutation = await context.store.semantic({
189
+ operation: "update_sequence_track",
190
+ tables: ["sequence_tracks"],
191
+ details: { trackId, sequenceId: row.sequence_id },
192
+ writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
193
+ }, () => {
194
+ context.store.db
195
+ .prepare(`UPDATE sequence_tracks
196
+ SET name=?, enabled=?, locked=?, muted=?, solo=?
197
+ WHERE track_id=?`)
198
+ .run(name, (input.enabled ?? row.enabled === 1) ? 1 : 0, (input.locked ?? row.locked === 1) ? 1 : 0, row.kind === "audio"
199
+ ? (input.muted ?? row.muted === 1)
200
+ ? 1
201
+ : 0
202
+ : null, row.kind === "audio"
203
+ ? (input.solo ?? row.solo === 1)
204
+ ? 1
205
+ : 0
206
+ : null, trackId);
207
+ });
208
+ return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
209
+ });
210
+ }
211
+ async function createSequence(context, input) {
212
+ return resultOf(async () => {
213
+ const sequenceId = input.sequenceId ?? newUuidV7();
214
+ assertUuidV7(sequenceId, "Sequence ID");
215
+ const normalized = normalizeCreateInput(input);
216
+ const bookId = context.bookRow().book_id;
217
+ const tracks = defaultTracks(sequenceId, normalized.videoTrackCount, normalized.audioTrackCount, normalized.captionTrackCount);
218
+ const mutation = await context.store.semantic({
219
+ operation: "create_sequence",
220
+ tables: ["sequences", "sequence_tracks"],
221
+ details: {
222
+ sequenceId,
223
+ name: normalized.name,
224
+ width: normalized.width,
225
+ height: normalized.height,
226
+ frameRate: normalized.frameRate,
227
+ trackCount: tracks.length,
228
+ },
229
+ writeSet: [
230
+ `sequence:${sequenceId}`,
231
+ ...tracks.map(({ track }) => `track:${track.trackId}`),
232
+ ],
233
+ }, (_operationId, now) => {
234
+ insertSequence(context, sequenceId, bookId, normalized, now);
235
+ insertTracks(context, tracks);
236
+ });
237
+ return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
238
+ });
239
+ }
240
+ async function renameSequence(context, sequenceId, requestedName) {
241
+ return resultOf(async () => {
242
+ const current = requiredSequence(context, sequenceId);
243
+ const name = requiredText(requestedName, "Sequence name");
244
+ if (name === current.name)
245
+ return current;
246
+ const mutation = await context.store.semantic({
247
+ operation: "rename_sequence",
248
+ tables: ["sequences"],
249
+ details: { sequenceId, oldName: current.name, newName: name },
250
+ writeSet: [`sequence:${sequenceId}`],
251
+ }, () => {
252
+ context.store.db
253
+ .prepare("UPDATE sequences SET name=? WHERE sequence_id=?")
254
+ .run(name, sequenceId);
255
+ });
256
+ return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
257
+ });
258
+ }
259
+ async function updateSequenceCanvas(context, sequenceId, input) {
260
+ return resultOf(async () => {
261
+ const current = requiredSequence(context, sequenceId);
262
+ safeIntegerAtLeast(input.width, 1, "Sequence width");
263
+ safeIntegerAtLeast(input.height, 1, "Sequence height");
264
+ if (input.width === current.width && input.height === current.height) {
265
+ return current;
266
+ }
267
+ const mutation = await context.store.semantic({
268
+ operation: "update_sequence_canvas",
269
+ tables: ["sequences"],
270
+ details: {
271
+ sequenceId,
272
+ oldWidth: current.width,
273
+ oldHeight: current.height,
274
+ width: input.width,
275
+ height: input.height,
276
+ },
277
+ writeSet: [`sequence:${sequenceId}`],
278
+ }, () => {
279
+ context.store.db
280
+ .prepare("UPDATE sequences SET width=?, height=? WHERE sequence_id=?")
281
+ .run(input.width, input.height, sequenceId);
282
+ });
283
+ return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
284
+ });
285
+ }
286
+ async function deleteSequence(context, sequenceId) {
287
+ return resultOf(async () => {
288
+ assertUuidV7(sequenceId, "Sequence ID");
289
+ const row = requiredSequenceRow(context, sequenceId);
290
+ if (row.is_primary === 1) {
291
+ throw new EngineFault({
292
+ code: "IN_USE",
293
+ message: "The primary sequence cannot be deleted",
294
+ ownerId: sequenceId,
295
+ });
296
+ }
297
+ const references = context.store.db
298
+ .prepare(`SELECT notebook_id, cell_id, reference_id
299
+ FROM cell_references
300
+ WHERE kind='sequence' AND target_id=?
301
+ ORDER BY notebook_id, cell_id, reference_id`)
302
+ .all(sequenceId);
303
+ if (references.length > 0) {
304
+ throw new EngineFault({
305
+ code: "IN_USE",
306
+ message: `Sequence is still referenced: ${sequenceId}`,
307
+ ownerId: sequenceId,
308
+ details: { references },
309
+ });
310
+ }
311
+ const mutation = await context.store.semantic({
312
+ operation: "delete_sequence",
313
+ tables: [
314
+ "sequences",
315
+ "sequence_tracks",
316
+ "sequence_clips",
317
+ "clip_links",
318
+ "clip_transforms",
319
+ "transitions",
320
+ "caption_cues",
321
+ ],
322
+ details: { sequenceId },
323
+ writeSet: [`sequence:${sequenceId}`],
324
+ }, () => {
325
+ context.store.db
326
+ .prepare("DELETE FROM sequences WHERE sequence_id=?")
327
+ .run(sequenceId);
328
+ });
329
+ return ok({ sequenceId }, mutation.revision);
330
+ });
331
+ }
332
+ function listSequences(context) {
333
+ const rows = context.store.db
334
+ .prepare(`${SEQUENCE_SELECT} FROM sequences ORDER BY is_primary DESC, created_at, sequence_id`)
335
+ .all();
336
+ return rows.map((row) => sequenceFromRow(context, row));
337
+ }
338
+ function primarySequence(context) {
339
+ // is_primary is a derived singleton: a merge can transiently yield more
340
+ // than one primary row per book (see docs/engine-layout.md "Merge policy
341
+ // per constraint class"), so the read resolves a deterministic winner —
342
+ // earliest created_at, then lowest sequence_id — matching the post-merge
343
+ // reconcile in src/merge-policy.ts instead of returning an arbitrary row.
344
+ const row = context.store.db
345
+ .prepare(`${SEQUENCE_SELECT} FROM sequences
346
+ WHERE is_primary=1
347
+ ORDER BY created_at, sequence_id
348
+ LIMIT 1`)
349
+ .get();
350
+ if (!row) {
351
+ throw new EngineFault({
352
+ code: "SCHEMA_INCOMPATIBLE",
353
+ message: "Book is missing its primary sequence",
354
+ });
355
+ }
356
+ return sequenceFromRow(context, row);
357
+ }
358
+ function requiredSequence(context, sequenceId, revision) {
359
+ const row = requiredSequenceRow(context, sequenceId, revision);
360
+ return sequenceFromRow(context, row, revision);
361
+ }
362
+ function requiredSequenceRow(context, sequenceId, revision) {
363
+ assertUuidV7(sequenceId, "Sequence ID");
364
+ const source = semanticSource("sequences", revision);
365
+ const row = context.store.db
366
+ .prepare(`${SEQUENCE_SELECT} FROM ${source} WHERE sequence_id=?`)
367
+ .get(...revisionParams(revision, sequenceId));
368
+ if (!row) {
369
+ throw new EngineFault({
370
+ code: "NOT_FOUND",
371
+ message: revision
372
+ ? `Sequence not found at revision: ${sequenceId}`
373
+ : `Sequence not found: ${sequenceId}`,
374
+ });
375
+ }
376
+ return row;
377
+ }
378
+ function sequenceFromRow(context, row, revision) {
379
+ const trackRows = queryRows(context, `${TRACK_SELECT} FROM ${semanticSource("sequence_tracks", revision)}
380
+ WHERE sequence_id=? ORDER BY kind, order_key, track_id`, revision, row.sequence_id);
381
+ const ordinalsByKind = new Map();
382
+ const tracks = trackRows.map((trackRow) => {
383
+ const ordinal = ordinalsByKind.get(trackRow.kind) ?? 0;
384
+ ordinalsByKind.set(trackRow.kind, ordinal + 1);
385
+ return trackFromRow(trackRow, ordinal);
386
+ });
387
+ const trackIds = tracks.map((track) => track.trackId);
388
+ const clips = trackIds.length === 0 ? [] : readClips(context, trackIds, revision);
389
+ const transitions = trackIds.length === 0 ? [] : readTransitions(context, trackIds, revision);
390
+ const captions = trackIds.length === 0 ? [] : readCaptions(context, trackIds, revision);
391
+ const background = parseJson(row.background_rgba_json, [0, 0, 0, 1]);
392
+ if (!isRgba(background)) {
393
+ throw new EngineFault({
394
+ code: "SCHEMA_INCOMPATIBLE",
395
+ message: `Sequence has invalid background RGBA: ${row.sequence_id}`,
396
+ });
397
+ }
398
+ return {
399
+ sequenceId: row.sequence_id,
400
+ name: row.name,
401
+ width: row.width,
402
+ height: row.height,
403
+ pixelAspect: {
404
+ numerator: row.pixel_aspect_numerator,
405
+ denominator: row.pixel_aspect_denominator,
406
+ },
407
+ frameRate: {
408
+ numerator: row.frame_rate_numerator,
409
+ denominator: row.frame_rate_denominator,
410
+ },
411
+ audioSampleRateHz: row.audio_sample_rate_hz,
412
+ audioChannelLayout: row.audio_channel_layout,
413
+ backgroundRgba: background,
414
+ revision: revision ?? context.store.head,
415
+ tracks,
416
+ clips,
417
+ transitions,
418
+ captions,
419
+ createdAt: row.created_at,
420
+ };
421
+ }
422
+ function readClips(context, trackIds, revision) {
423
+ const placeholders = trackIds.map(() => "?").join(",");
424
+ const clips = queryRows(context, `${CLIP_SELECT} FROM ${semanticSource("sequence_clips", revision)}
425
+ WHERE track_id IN (${placeholders})
426
+ ORDER BY track_id, timeline_start_frame, clip_id`, revision, ...trackIds);
427
+ const clipIds = clips.map((clip) => clip.clip_id);
428
+ if (clipIds.length === 0)
429
+ return [];
430
+ const clipPlaceholders = clipIds.map(() => "?").join(",");
431
+ const links = queryRows(context, `SELECT link_group_id, clip_id
432
+ FROM ${semanticSource("clip_links", revision)}
433
+ WHERE clip_id IN (${clipPlaceholders})`, revision, ...clipIds);
434
+ const transforms = queryRows(context, `${TRANSFORM_SELECT}
435
+ FROM ${semanticSource("clip_transforms", revision)}
436
+ WHERE clip_id IN (${clipPlaceholders})`, revision, ...clipIds);
437
+ const linkByClip = new Map(links.map((link) => [link.clip_id, link.link_group_id]));
438
+ const transformByClip = new Map(transforms.map((transformRow) => [
439
+ transformRow.clip_id,
440
+ transformFromRow(transformRow),
441
+ ]));
442
+ return clips.map((clip) => clipFromRow(clip, linkByClip.get(clip.clip_id), transformByClip.get(clip.clip_id)));
443
+ }
444
+ function readTransitions(context, trackIds, revision) {
445
+ const placeholders = trackIds.map(() => "?").join(",");
446
+ return queryRows(context, `${TRANSITION_SELECT}
447
+ FROM ${semanticSource("transitions", revision)}
448
+ WHERE track_id IN (${placeholders})
449
+ ORDER BY track_id, outgoing_clip_id, incoming_clip_id`, revision, ...trackIds).map((row) => ({
450
+ transitionId: row.transition_id,
451
+ trackId: row.track_id,
452
+ outgoingClipId: row.outgoing_clip_id,
453
+ incomingClipId: row.incoming_clip_id,
454
+ kind: row.kind,
455
+ durationFrames: row.duration_frames,
456
+ alignment: row.alignment,
457
+ }));
458
+ }
459
+ function readCaptions(context, trackIds, revision) {
460
+ const placeholders = trackIds.map(() => "?").join(",");
461
+ return queryRows(context, `${CAPTION_SELECT}
462
+ FROM ${semanticSource("caption_cues", revision)}
463
+ WHERE track_id IN (${placeholders})
464
+ ORDER BY track_id, timeline_start_frame, cue_id`, revision, ...trackIds).map(captionFromRow);
465
+ }
466
+ function clipFromRow(row, linkGroupId, transform) {
467
+ const common = {
468
+ clipId: row.clip_id,
469
+ trackId: row.track_id,
470
+ timelineStartFrame: row.timeline_start_frame,
471
+ durationFrames: row.duration_frames,
472
+ enabled: row.enabled === 1,
473
+ ...(linkGroupId ? { linkGroupId } : {}),
474
+ ...(transform ? { transform } : {}),
475
+ ...clipAudio(row),
476
+ };
477
+ if (row.source_kind === "still") {
478
+ if (!row.source_path) {
479
+ throw invalidClip(row.clip_id, "still source path");
480
+ }
481
+ return {
482
+ ...common,
483
+ source: {
484
+ kind: "still",
485
+ artifactId: row.artifact_id,
486
+ sourcePath: row.source_path,
487
+ objectHash: row.object_hash,
488
+ },
489
+ };
490
+ }
491
+ if (!row.stream_id ||
492
+ row.source_start_tick === null ||
493
+ row.source_duration_ticks === null ||
494
+ row.time_base_numerator === null ||
495
+ row.time_base_denominator === null ||
496
+ row.speed_numerator === null ||
497
+ row.speed_denominator === null ||
498
+ row.reverse === null ||
499
+ row.audio_policy === null) {
500
+ throw invalidClip(row.clip_id, "timed source mapping");
501
+ }
502
+ return {
503
+ ...common,
504
+ source: {
505
+ kind: "timed",
506
+ artifactId: row.artifact_id,
507
+ range: {
508
+ streamId: row.stream_id,
509
+ objectHash: row.object_hash,
510
+ startTick: row.source_start_tick,
511
+ durationTicks: row.source_duration_ticks,
512
+ timeBase: {
513
+ numerator: row.time_base_numerator,
514
+ denominator: row.time_base_denominator,
515
+ },
516
+ },
517
+ },
518
+ speed: {
519
+ numerator: row.speed_numerator,
520
+ denominator: row.speed_denominator,
521
+ },
522
+ reverse: row.reverse === 1,
523
+ audioPolicy: row.audio_policy,
524
+ };
525
+ }
526
+ function clipAudio(row) {
527
+ const values = [
528
+ row.gain_db,
529
+ row.audio_muted,
530
+ row.fade_in_frames,
531
+ row.fade_out_frames,
532
+ ];
533
+ if (values.every((value) => value === null))
534
+ return {};
535
+ if (values.some((value) => value === null)) {
536
+ throw invalidClip(row.clip_id, "audio projection");
537
+ }
538
+ return {
539
+ audio: {
540
+ gainDb: row.gain_db,
541
+ muted: row.audio_muted === 1,
542
+ fadeInFrames: row.fade_in_frames,
543
+ fadeOutFrames: row.fade_out_frames,
544
+ },
545
+ };
546
+ }
547
+ function transformFromRow(row) {
548
+ return {
549
+ fit: row.fit,
550
+ positionX: row.position_x,
551
+ positionY: row.position_y,
552
+ scaleX: row.scale_x,
553
+ scaleY: row.scale_y,
554
+ anchorX: row.anchor_x,
555
+ anchorY: row.anchor_y,
556
+ rotationDegrees: row.rotation_degrees,
557
+ cropTop: row.crop_top,
558
+ cropRight: row.crop_right,
559
+ cropBottom: row.crop_bottom,
560
+ cropLeft: row.crop_left,
561
+ opacity: row.opacity,
562
+ blendMode: row.blend_mode,
563
+ };
564
+ }
565
+ function captionFromRow(row) {
566
+ const transcriptSelection = captionTranscriptSelection(row);
567
+ return {
568
+ cueId: row.cue_id,
569
+ trackId: row.track_id,
570
+ timelineStartFrame: row.timeline_start_frame,
571
+ durationFrames: row.duration_frames,
572
+ text: row.text,
573
+ ...(row.speaker ? { speaker: row.speaker } : {}),
574
+ styleId: row.style_id,
575
+ ...(transcriptSelection ? { transcriptSelection } : {}),
576
+ };
577
+ }
578
+ function captionTranscriptSelection(row) {
579
+ const values = [
580
+ row.transcript_id,
581
+ row.transcript_revision,
582
+ row.start_word_id,
583
+ row.end_word_id,
584
+ row.source_range_json,
585
+ ];
586
+ if (values.every((value) => value === null))
587
+ return null;
588
+ if (values.some((value) => value === null)) {
589
+ throw new EngineFault({
590
+ code: "SCHEMA_INCOMPATIBLE",
591
+ message: `Caption has an incomplete transcript selection: ${row.cue_id}`,
592
+ });
593
+ }
594
+ return {
595
+ transcriptId: row.transcript_id,
596
+ transcriptRevision: row.transcript_revision,
597
+ startWordId: row.start_word_id,
598
+ endWordId: row.end_word_id,
599
+ range: normalizeSourceRange(parseJson(row.source_range_json, {
600
+ streamId: "",
601
+ objectHash: "",
602
+ startTick: 0,
603
+ durationTicks: 0,
604
+ timeBase: { numerator: 1, denominator: 1 },
605
+ })),
606
+ };
607
+ }
608
+ function trackFromRow(row, ordinal) {
609
+ const common = {
610
+ trackId: row.track_id,
611
+ sequenceId: row.sequence_id,
612
+ ordinal,
613
+ name: row.name,
614
+ enabled: row.enabled === 1,
615
+ locked: row.locked === 1,
616
+ };
617
+ if (row.kind === "video") {
618
+ return {
619
+ ...common,
620
+ kind: "video",
621
+ blendMode: "normal",
622
+ };
623
+ }
624
+ if (row.kind === "audio") {
625
+ return {
626
+ ...common,
627
+ kind: "audio",
628
+ muted: row.muted === 1,
629
+ solo: row.solo === 1,
630
+ };
631
+ }
632
+ return {
633
+ ...common,
634
+ kind: "caption",
635
+ };
636
+ }
637
+ function normalizeCreateInput(input) {
638
+ const pixelAspect = normalizeRational(input.pixelAspect ?? { numerator: 1, denominator: 1 });
639
+ const frameRate = normalizeRational(input.frameRate);
640
+ const backgroundRgba = input.backgroundRgba ?? [0, 0, 0, 1];
641
+ if (!isRgba(backgroundRgba)) {
642
+ throw new Error("Sequence background RGBA must contain four finite values");
643
+ }
644
+ const videoTrackCount = trackCount(input.videoTrackCount ?? 2, "Video track count");
645
+ const audioTrackCount = trackCount(input.audioTrackCount ?? 4, "Audio track count");
646
+ const captionTrackCount = trackCount(input.captionTrackCount ?? 1, "Caption track count");
647
+ safeIntegerAtLeast(input.width, 1, "Sequence width");
648
+ safeIntegerAtLeast(input.height, 1, "Sequence height");
649
+ const audioSampleRateHz = input.audioSampleRateHz ?? 48_000;
650
+ safeIntegerAtLeast(audioSampleRateHz, 1, "Sequence audio sample rate");
651
+ return {
652
+ name: requiredText(input.name, "Sequence name"),
653
+ width: input.width,
654
+ height: input.height,
655
+ pixelAspect,
656
+ frameRate,
657
+ audioSampleRateHz,
658
+ audioChannelLayout: requiredText(input.audioChannelLayout ?? "stereo", "Sequence audio channel layout"),
659
+ backgroundRgba,
660
+ videoTrackCount,
661
+ audioTrackCount,
662
+ captionTrackCount,
663
+ };
664
+ }
665
+ function insertSequence(context, sequenceId, bookId, input, now) {
666
+ context.store.db
667
+ .prepare(`INSERT INTO sequences(
668
+ sequence_id, book_id, name, is_primary, width, height,
669
+ pixel_aspect_numerator, pixel_aspect_denominator,
670
+ frame_rate_numerator, frame_rate_denominator,
671
+ audio_sample_rate_hz, audio_channel_layout,
672
+ background_rgba_json, created_at
673
+ ) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
674
+ .run(sequenceId, bookId, input.name, input.width, input.height, input.pixelAspect.numerator, input.pixelAspect.denominator, input.frameRate.numerator, input.frameRate.denominator, input.audioSampleRateHz, input.audioChannelLayout, canonicalJson(input.backgroundRgba), now);
675
+ }
676
+ function defaultTracks(sequenceId, videoCount, audioCount, captionCount) {
677
+ const tracks = [];
678
+ const videoKeys = initialOrderKeys(videoCount);
679
+ for (let ordinal = 0; ordinal < videoCount; ordinal += 1) {
680
+ tracks.push({
681
+ orderKey: videoKeys[ordinal],
682
+ track: {
683
+ trackId: newUuidV7(),
684
+ sequenceId,
685
+ kind: "video",
686
+ ordinal,
687
+ name: `Video ${ordinal + 1}`,
688
+ enabled: true,
689
+ locked: false,
690
+ blendMode: "normal",
691
+ },
692
+ });
693
+ }
694
+ const audioKeys = initialOrderKeys(audioCount);
695
+ for (let ordinal = 0; ordinal < audioCount; ordinal += 1) {
696
+ tracks.push({
697
+ orderKey: audioKeys[ordinal],
698
+ track: {
699
+ trackId: newUuidV7(),
700
+ sequenceId,
701
+ kind: "audio",
702
+ ordinal,
703
+ name: `Audio ${ordinal + 1}`,
704
+ enabled: true,
705
+ locked: false,
706
+ muted: false,
707
+ solo: false,
708
+ },
709
+ });
710
+ }
711
+ const captionKeys = initialOrderKeys(captionCount);
712
+ for (let ordinal = 0; ordinal < captionCount; ordinal += 1) {
713
+ tracks.push({
714
+ orderKey: captionKeys[ordinal],
715
+ track: {
716
+ trackId: newUuidV7(),
717
+ sequenceId,
718
+ kind: "caption",
719
+ ordinal,
720
+ name: captionCount === 1 ? "Captions" : `Captions ${ordinal + 1}`,
721
+ enabled: true,
722
+ locked: false,
723
+ },
724
+ });
725
+ }
726
+ return tracks;
727
+ }
728
+ function insertTracks(context, tracks) {
729
+ const insert = context.store.db.prepare(`INSERT INTO sequence_tracks(
730
+ track_id, sequence_id, kind, order_key, name,
731
+ enabled, locked, muted, solo, blend_mode
732
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
733
+ for (const { track, orderKey } of tracks) {
734
+ insert.run(track.trackId, track.sequenceId, track.kind, orderKey, track.name, track.enabled ? 1 : 0, track.locked ? 1 : 0, track.kind === "audio" ? (track.muted ? 1 : 0) : null, track.kind === "audio" ? (track.solo ? 1 : 0) : null, track.kind === "video" ? track.blendMode : null);
735
+ }
736
+ }
737
+ function queryRows(context, sql, revision, ...params) {
738
+ return context.store.db
739
+ .prepare(sql)
740
+ .all(...(revision ? [revision, ...params] : params));
741
+ }
742
+ function requiredTrackRow(context, trackId) {
743
+ assertUuidV7(trackId, "Track ID");
744
+ const row = context.store.db
745
+ .prepare(`${TRACK_SELECT} FROM sequence_tracks WHERE track_id=?`)
746
+ .get(trackId);
747
+ if (!row) {
748
+ throw new EngineFault({
749
+ code: "NOT_FOUND",
750
+ message: `Sequence track not found: ${trackId}`,
751
+ });
752
+ }
753
+ return row;
754
+ }
755
+ /** Current rank of a track within its sequence and kind. */
756
+ function trackOrdinal(context, row) {
757
+ const siblings = context.store.db
758
+ .prepare(`SELECT track_id FROM sequence_tracks
759
+ WHERE sequence_id=? AND kind=?
760
+ ORDER BY order_key, track_id`)
761
+ .all(row.sequence_id, row.kind);
762
+ return siblings.findIndex((candidate) => candidate.track_id === row.track_id);
763
+ }
764
+ function semanticSource(table, revision) {
765
+ return revision ? `dolt_at_${table}(?)` : table;
766
+ }
767
+ function revisionParams(revision, ...params) {
768
+ return revision ? [revision, ...params] : params;
769
+ }
770
+ function isRgba(value) {
771
+ return (Array.isArray(value) &&
772
+ value.length === 4 &&
773
+ value.every((channel) => Number.isFinite(channel)));
774
+ }
775
+ function invalidClip(clipId, field) {
776
+ return new EngineFault({
777
+ code: "SCHEMA_INCOMPATIBLE",
778
+ message: `Clip has an invalid ${field}: ${clipId}`,
779
+ });
780
+ }
781
+ function requiredText(value, label) {
782
+ const normalized = value.trim();
783
+ if (!normalized)
784
+ throw new Error(`${label} is required`);
785
+ return normalized;
786
+ }
787
+ function safeIntegerAtLeast(value, minimum, label) {
788
+ if (!Number.isSafeInteger(value) || value < minimum) {
789
+ throw new Error(`${label} must be a safe integer of at least ${minimum}`);
790
+ }
791
+ }
792
+ function trackCount(value, label) {
793
+ safeIntegerAtLeast(value, 0, label);
794
+ if (value > 32)
795
+ throw new Error(`${label} must not exceed 32`);
796
+ return value;
797
+ }
798
+ const SEQUENCE_SELECT = `
799
+ SELECT sequence_id, name, is_primary, width, height,
800
+ pixel_aspect_numerator, pixel_aspect_denominator,
801
+ frame_rate_numerator, frame_rate_denominator,
802
+ audio_sample_rate_hz, audio_channel_layout,
803
+ background_rgba_json, created_at`;
804
+ const TRACK_SELECT = `
805
+ SELECT track_id, sequence_id, kind, order_key, name,
806
+ enabled, locked, muted, solo, blend_mode`;
807
+ const CLIP_SELECT = `
808
+ SELECT clip_id, track_id, source_kind, artifact_id, source_path,
809
+ stream_id, object_hash, source_start_tick, source_duration_ticks,
810
+ time_base_numerator, time_base_denominator,
811
+ timeline_start_frame, duration_frames, speed_numerator,
812
+ speed_denominator, reverse, audio_policy, gain_db, audio_muted,
813
+ fade_in_frames, fade_out_frames, enabled`;
814
+ const TRANSFORM_SELECT = `
815
+ SELECT clip_id, fit, position_x, position_y, scale_x, scale_y,
816
+ anchor_x, anchor_y, rotation_degrees, crop_top, crop_right,
817
+ crop_bottom, crop_left, opacity, blend_mode`;
818
+ const TRANSITION_SELECT = `
819
+ SELECT transition_id, track_id, outgoing_clip_id, incoming_clip_id,
820
+ kind, duration_frames, alignment`;
821
+ const CAPTION_SELECT = `
822
+ SELECT cue_id, track_id, timeline_start_frame, duration_frames,
823
+ text, speaker, style_id, transcript_id, transcript_revision,
824
+ start_word_id, end_word_id, source_range_json`;
825
+ //# sourceMappingURL=sequences.js.map