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
package/dist/history.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { ok } from "./engine-types.js";
3
- import { resultOf, syncResultOf, } from "./context.js";
3
+ import { resultOf } from "./context.js";
4
4
  import { artifactSlug } from "./artifacts.js";
5
5
  import { materializeArtifact } from "./files.js";
6
6
  import { assertUuidV7, isUuidV7, newUuidV7 } from "./ids.js";
7
- import { canonicalJson, EngineFault, parseJson, } from "./store.js";
7
+ import { SCHEMA_VERSION, SEMANTIC_TABLES } from "./schema.js";
8
+ import { canonicalJson, commitDateIso, EngineFault, parseCommitMessage, } from "./store.js";
8
9
  export function createHistoryApi(context) {
9
10
  return {
10
11
  revisions: (limit = 20) => revisionHistory(context, limit),
@@ -15,9 +16,6 @@ export function createHistoryApi(context) {
15
16
  restore: (revision) => restoreBook(context, revision),
16
17
  logAction: (action, payload) => logAction(context, action, payload),
17
18
  actionLog: (options) => actionLog(context, options),
18
- actions: (options) => actions(context, options),
19
- action: (actionId) => action(context, actionId),
20
- recordAction: (input) => recordAction(context, input),
21
19
  };
22
20
  }
23
21
  function revisionForHash(context, hash) {
@@ -30,18 +28,21 @@ function revisionForHash(context, hash) {
30
28
  message: `Revision not found: ${hash}`,
31
29
  });
32
30
  }
31
+ const parsed = parseCommitMessage(commit.message);
32
+ if (parsed)
33
+ return revisionFromCommit(context, commit, parsed);
33
34
  return {
34
35
  hash,
35
36
  message: commit.message,
36
- date: commit.date,
37
+ date: commitDateIso(commit.date),
37
38
  author: commit.committer,
38
39
  };
39
40
  }
40
41
  function revisionHistory(context, limit) {
41
- return allRevisions(context).slice(0, Math.max(0, limit));
42
+ return commitRevisions(context).slice(0, Math.max(0, limit));
42
43
  }
43
44
  function artifactHistory(context, artifactReference, limit) {
44
- const revisions = allRevisions(context);
45
+ const revisions = commitRevisions(context);
45
46
  let artifactId;
46
47
  if (isUuidV7(artifactReference)) {
47
48
  artifactId = artifactReference;
@@ -63,61 +64,52 @@ function artifactHistory(context, artifactReference, limit) {
63
64
  .filter((revision) => revision.artifactId === artifactId)
64
65
  .slice(0, Math.max(0, limit));
65
66
  }
66
- function allRevisions(context) {
67
- const commits = context.store.db.doltLog();
67
+ // History listings are direct commit-log reads: every engine commit carries
68
+ // its operation, parameters, and write set in its structured message, so no
69
+ // per-commit table diffs are needed to describe a revision.
70
+ function commitRevisions(context) {
68
71
  const revisions = [];
69
- for (let index = 0; index < commits.length - 1; index += 1) {
70
- const commit = commits[index];
71
- const parent = commits[index + 1];
72
- const operations = context.store
73
- .diff(parent.commit_hash, commit.commit_hash, "operations")
74
- .filter((row) => row.diff_type === "added")
75
- .map(operationFromDiff)
76
- .filter((operation) => operation !== null);
77
- for (const operation of operations) {
78
- const details = parseJson(operation.details_json, {});
79
- const writeSet = parseJson(operation.write_set_json, []);
80
- const artifactSlugAtRevision = operation.artifact_id
81
- ? artifactSlugAt(context, operation.artifact_id, commit.commit_hash)
82
- : undefined;
83
- const fileChanges = revisionFileChanges(context.store.diff(parent.commit_hash, commit.commit_hash, "artifact_files"), operation.artifact_id);
84
- revisions.push({
85
- hash: commit.commit_hash,
86
- message: commit.message,
87
- date: new Date(operation.created_at).toISOString(),
88
- author: operation.author,
89
- operationId: operation.operation_id,
90
- operation: operation.operation,
91
- ...(operation.artifact_id
92
- ? { artifactId: operation.artifact_id }
93
- : {}),
94
- ...(artifactSlugAtRevision
95
- ? { artifactSlug: artifactSlugAtRevision }
96
- : {}),
97
- details: {
98
- ...details,
99
- ...(writeSet.length > 0 ? { writeSet } : {}),
100
- ...(operation.base_revision
101
- ? { baseRevision: operation.base_revision }
102
- : {}),
103
- },
104
- files: fileChanges.map((change) => change.file),
105
- fileChanges,
106
- });
107
- }
72
+ for (const commit of context.store.db.doltLog()) {
73
+ const parsed = parseCommitMessage(commit.message);
74
+ if (!parsed)
75
+ continue;
76
+ revisions.push(revisionFromCommit(context, commit, parsed));
108
77
  }
109
78
  return revisions;
110
79
  }
80
+ function revisionFromCommit(context, commit, parsed) {
81
+ const artifactSlugAtRevision = parsed.artifactId
82
+ ? artifactSlugAt(context, parsed.artifactId, commit.commit_hash)
83
+ : undefined;
84
+ return {
85
+ hash: commit.commit_hash,
86
+ message: commit.message,
87
+ date: commitDateIso(commit.date),
88
+ author: parsed.actor ?? commit.committer,
89
+ operationId: parsed.operationId,
90
+ operation: parsed.operation,
91
+ ...(parsed.artifactId ? { artifactId: parsed.artifactId } : {}),
92
+ ...(artifactSlugAtRevision ? { artifactSlug: artifactSlugAtRevision } : {}),
93
+ details: {
94
+ ...parsed.details,
95
+ ...(parsed.writeSet.length > 0 ? { writeSet: parsed.writeSet } : {}),
96
+ ...(parsed.baseRevision ? { baseRevision: parsed.baseRevision } : {}),
97
+ },
98
+ };
99
+ }
111
100
  function resolveRevision(context, reference) {
112
- const historical = allRevisions(context).find((revision) => revision.hash === reference || revision.hash.startsWith(reference));
101
+ const historical = commitRevisions(context).find((revision) => revision.hash === reference || revision.hash.startsWith(reference));
113
102
  if (historical)
114
103
  return historical;
115
- const commit = context.store.db.doltLog().find((item) => item.commit_hash === reference || item.commit_hash.startsWith(reference));
104
+ const commit = context.store.db
105
+ .doltLog()
106
+ .find((item) => item.commit_hash === reference ||
107
+ item.commit_hash.startsWith(reference));
116
108
  return commit
117
109
  ? {
118
110
  hash: commit.commit_hash,
119
111
  message: commit.message,
120
- date: commit.date,
112
+ date: commitDateIso(commit.date),
121
113
  author: commit.committer,
122
114
  }
123
115
  : null;
@@ -139,15 +131,17 @@ async function recordOperation(context, operation, artifactReference, details) {
139
131
  : null;
140
132
  const mutation = await context.store.semantic({
141
133
  operation,
134
+ tables: [],
142
135
  ...(artifact ? { artifactId: artifact.artifact_id } : {}),
143
136
  details: {
144
137
  ...(details ?? {}),
145
138
  ...(artifact ? { artifactSlug: artifact.slug } : {}),
146
139
  },
147
- writeSet: artifact
148
- ? [`artifact:${artifact.artifact_id}`]
149
- : ["book"],
150
- }, [], () => undefined);
140
+ writeSet: artifact ? [`artifact:${artifact.artifact_id}`] : ["book"],
141
+ // Provenance-only operations change no semantic rows; the commit
142
+ // itself is their record, so it must be minted even when empty.
143
+ allowEmpty: true,
144
+ }, () => undefined);
151
145
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
152
146
  });
153
147
  }
@@ -191,18 +185,16 @@ async function restoreArtifact(context, artifactId, revisionReference, replaceme
191
185
  .get(revision.hash, artifactId);
192
186
  const mutation = await context.store.semantic({
193
187
  operation: "restore_artifact",
188
+ tables: [
189
+ "artifacts",
190
+ "artifact_files",
191
+ "artifact_metadata",
192
+ "audio_waveforms",
193
+ ],
194
194
  artifactId,
195
195
  details: { fromRevision: revision.hash, slug: desiredSlug },
196
- writeSet: [
197
- `artifact:${artifactId}`,
198
- `artifact-slug:${desiredSlug}`,
199
- ],
200
- }, [
201
- "artifacts",
202
- "artifact_files",
203
- "artifact_metadata",
204
- "audio_waveforms",
205
- ], (_operationId, now) => {
196
+ writeSet: [`artifact:${artifactId}`, `artifact-slug:${desiredSlug}`],
197
+ }, (_operationId, now) => {
206
198
  context.store.db
207
199
  .prepare(`INSERT INTO artifacts(
208
200
  artifact_id, slug, kind, created_at
@@ -235,8 +227,11 @@ async function restoreArtifact(context, artifactId, revisionReference, replaceme
235
227
  }
236
228
  resetArtifactRuntime(context, artifactId, now);
237
229
  });
238
- await rm(context.artifactPath(artifactId), { recursive: true, force: true });
239
- await materializeArtifact(context, artifactId);
230
+ await rm(context.artifactPath(artifactId), {
231
+ recursive: true,
232
+ force: true,
233
+ });
234
+ await materializeIgnoringForgotten(context, artifactId);
240
235
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
241
236
  });
242
237
  }
@@ -259,160 +254,33 @@ async function restoreBook(context, revisionReference) {
259
254
  message: "A restore cannot replace the catalog's stable book ID",
260
255
  });
261
256
  }
257
+ const targetSchema = context.store.db
258
+ .prepare("SELECT version FROM dolt_at_engine_schema(?) WHERE singleton=1")
259
+ .get(revision.hash);
260
+ if (!targetSchema || targetSchema.version !== SCHEMA_VERSION) {
261
+ throw new EngineFault({
262
+ code: "SCHEMA_INCOMPATIBLE",
263
+ message: `Cannot restore ${revision.hash}: it records schema version ` +
264
+ `${targetSchema?.version ?? "unknown"}, not the current ${SCHEMA_VERSION}`,
265
+ });
266
+ }
262
267
  const targetArtifacts = context.store.db
263
268
  .prepare(`SELECT artifact_id, slug, kind, created_at
264
269
  FROM dolt_at_artifacts(?)
265
270
  ORDER BY artifact_id`)
266
271
  .all(revision.hash);
267
272
  const targetIds = targetArtifacts.map((row) => row.artifact_id);
268
- const targetFiles = rowsForArtifactIds(context, "artifact_files", "artifact_id, path, object_hash, mtime_ms, created_at", revision.hash, targetIds);
269
- const bookMetadata = context.store.db
270
- .prepare("SELECT key, value_json FROM dolt_at_book_metadata(?)")
271
- .all(revision.hash);
272
- const artifactMetadata = rowsForArtifactIds(context, "artifact_metadata", "artifact_id, key, value_json", revision.hash, targetIds);
273
- const waveforms = rowsForArtifactIds(context, "audio_waveforms", "artifact_id, peaks_json", revision.hash, targetIds);
274
- const entities = context.store.db
275
- .prepare(`SELECT entity_id, type, name, description, prompt,
276
- data_json, created_at
277
- FROM dolt_at_entities(?)`)
278
- .all(revision.hash);
279
- const notebooks = context.store.db
280
- .prepare(`SELECT notebook_id, name, properties_json, created_at
281
- FROM dolt_at_notebooks(?)`)
282
- .all(revision.hash);
283
- const notebookIds = notebooks.map((row) => row.notebook_id);
284
- const notebookCells = rowsForNotebookIds(context, "cells", `notebook_id, cell_id, type, title, position_x, position_y,
285
- entity_id, prompt, model, inputs_json, output_artifact_id`, revision.hash, notebookIds);
286
- const notebookEdges = rowsForNotebookIds(context, "edges", "notebook_id, edge_id, source_cell_id, target_cell_id, target_input", revision.hash, notebookIds);
287
- const notebookRuns = rowsForNotebookIds(context, "runs", `run_id, notebook_id, status, started_at, completed_at,
288
- cell_order_json, outputs_json, error`, revision.hash, notebookIds);
289
- const timeline = context.store.db
290
- .prepare("SELECT book_id, render FROM dolt_at_timeline(?)")
291
- .get(revision.hash);
292
- const timelineSlots = context.store.db
293
- .prepare(`SELECT slot_id, artifact_id, ordinal, volume,
294
- audio_fade_in, audio_fade_out
295
- FROM dolt_at_timeline_slots(?)`)
296
- .all(revision.hash);
297
- const timelineAudio = context.store.db
298
- .prepare(`SELECT audio_id, artifact_id, ordinal, start_frame, duration_frames,
299
- volume, fade_in, fade_out
300
- FROM dolt_at_timeline_audio(?)`)
301
- .all(revision.hash);
302
- const prompts = context.store.db
303
- .prepare(`SELECT prompt_id, surface, prompt, context_json, created_at
304
- FROM dolt_at_prompt_entries(?)`)
305
- .all(revision.hash);
306
- const messages = context.store.db
307
- .prepare(`SELECT message_id, role, body_json, created_at
308
- FROM dolt_at_messages(?)`)
309
- .all(revision.hash);
310
273
  const currentArtifactIds = context.store.db
311
274
  .prepare("SELECT artifact_id FROM artifacts")
312
275
  .all()
313
276
  .map((row) => row.artifact_id);
314
277
  const mutation = await context.store.semantic({
315
278
  operation: "restore",
279
+ tables: SEMANTIC_TABLES,
316
280
  details: { fromRevision: revision.hash },
317
281
  writeSet: ["book"],
318
- }, [
319
- "book",
320
- "artifacts",
321
- "artifact_files",
322
- "book_metadata",
323
- "artifact_metadata",
324
- "audio_waveforms",
325
- "entities",
326
- "notebooks",
327
- "cells",
328
- "edges",
329
- "runs",
330
- "timeline",
331
- "timeline_slots",
332
- "timeline_audio",
333
- "prompt_entries",
334
- "messages",
335
- "job_runs",
336
- ], (_operationId, now) => {
337
- context.store.db.prepare("DELETE FROM timeline_slots").run();
338
- context.store.db.prepare("DELETE FROM timeline_audio").run();
339
- context.store.db.prepare("DELETE FROM edges").run();
340
- context.store.db.prepare("DELETE FROM runs").run();
341
- context.store.db.prepare("DELETE FROM cells").run();
342
- context.store.db.prepare("DELETE FROM notebooks").run();
343
- context.store.db.prepare("DELETE FROM artifact_metadata").run();
344
- context.store.db.prepare("DELETE FROM audio_waveforms").run();
345
- context.store.db.prepare("DELETE FROM artifact_files").run();
346
- context.store.db.prepare("DELETE FROM entities").run();
347
- context.store.db.prepare("DELETE FROM artifacts").run();
348
- context.store.db
349
- .prepare("UPDATE book SET slug=?, created_at=? WHERE book_id=?")
350
- .run(targetBook.slug, targetBook.created_at, targetBook.book_id);
351
- const insertArtifact = context.store.db.prepare(`INSERT INTO artifacts(artifact_id, slug, kind, created_at)
352
- VALUES (?, ?, ?, ?)`);
353
- for (const row of targetArtifacts) {
354
- insertArtifact.run(row.artifact_id, row.slug, row.kind, row.created_at);
355
- }
356
- insertFiles(context, targetFiles);
357
- const insertArtifactMetadata = context.store.db.prepare(`INSERT INTO artifact_metadata(artifact_id, key, value_json)
358
- VALUES (?, ?, ?)`);
359
- for (const row of artifactMetadata) {
360
- insertArtifactMetadata.run(row.artifact_id, row.key, row.value_json);
361
- }
362
- const insertWaveform = context.store.db.prepare(`INSERT INTO audio_waveforms(artifact_id, peaks_json)
363
- VALUES (?, ?)`);
364
- for (const row of waveforms) {
365
- if (!row.artifact_id)
366
- continue;
367
- insertWaveform.run(row.artifact_id, row.peaks_json);
368
- }
369
- context.store.db.prepare("DELETE FROM book_metadata").run();
370
- const insertBookMetadata = context.store.db.prepare("INSERT INTO book_metadata(key, value_json) VALUES (?, ?)");
371
- for (const row of bookMetadata) {
372
- insertBookMetadata.run(row.key, row.value_json);
373
- }
374
- const insertEntity = context.store.db.prepare(`INSERT INTO entities(
375
- entity_id, type, name, description, prompt, data_json, created_at
376
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`);
377
- for (const row of entities) {
378
- insertEntity.run(row.entity_id, row.type, row.name, row.description, row.prompt, row.data_json, row.created_at);
379
- }
380
- const insertNotebook = context.store.db.prepare(`INSERT INTO notebooks(
381
- notebook_id, name, properties_json, created_at
382
- ) VALUES (?, ?, ?, ?)`);
383
- for (const row of notebooks) {
384
- insertNotebook.run(row.notebook_id, row.name, row.properties_json, row.created_at);
385
- }
386
- insertNotebookChildren(context, notebookCells, notebookEdges, notebookRuns);
387
- context.store.db
388
- .prepare("UPDATE timeline SET render=? WHERE book_id=?")
389
- .run(timeline?.render ?? "landscape", targetBook.book_id);
390
- const insertSlot = context.store.db.prepare(`INSERT INTO timeline_slots(
391
- slot_id, artifact_id, ordinal, volume, audio_fade_in, audio_fade_out
392
- ) VALUES (?, ?, ?, ?, ?, ?)`);
393
- for (const row of timelineSlots) {
394
- insertSlot.run(row.slot_id, row.artifact_id, row.ordinal, row.volume, row.audio_fade_in, row.audio_fade_out);
395
- }
396
- const insertAudio = context.store.db.prepare(`INSERT INTO timeline_audio(
397
- audio_id, artifact_id, ordinal, start_frame, duration_frames,
398
- volume, fade_in, fade_out
399
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
400
- for (const row of timelineAudio) {
401
- insertAudio.run(row.audio_id, row.artifact_id, row.ordinal, row.start_frame, row.duration_frames, row.volume, row.fade_in, row.fade_out);
402
- }
403
- context.store.db.prepare("DELETE FROM prompt_entries").run();
404
- const insertPrompt = context.store.db.prepare(`INSERT INTO prompt_entries(
405
- prompt_id, surface, prompt, context_json, created_at
406
- ) VALUES (?, ?, ?, ?, ?)`);
407
- for (const row of prompts) {
408
- insertPrompt.run(row.prompt_id, row.surface, row.prompt, row.context_json, row.created_at);
409
- }
410
- context.store.db.prepare("DELETE FROM messages").run();
411
- const insertMessage = context.store.db.prepare(`INSERT INTO messages(message_id, role, body_json, created_at)
412
- VALUES (?, ?, ?, ?)`);
413
- for (const row of messages) {
414
- insertMessage.run(row.message_id, row.role, row.body_json, row.created_at);
415
- }
282
+ }, (_operationId, now) => {
283
+ reloadSemanticTables(context, revision.hash);
416
284
  abortActiveJobs(context, now, "Book restored");
417
285
  context.store.db
418
286
  .prepare(`UPDATE runtime_resource_leases
@@ -424,7 +292,9 @@ async function restoreBook(context, revisionReference) {
424
292
  context.store.db.prepare("DELETE FROM runtime_artifact_views").run();
425
293
  context.store.db.prepare("DELETE FROM runtime_pending_tasks").run();
426
294
  context.store.db.prepare("DELETE FROM runtime_generation_errors").run();
427
- context.store.db.prepare("DELETE FROM runtime_similarity_embeddings").run();
295
+ context.store.db
296
+ .prepare("DELETE FROM runtime_similarity_embeddings")
297
+ .run();
428
298
  context.store.db
429
299
  .prepare("DELETE FROM runtime_text_similarity_documents")
430
300
  .run();
@@ -433,14 +303,94 @@ async function restoreBook(context, revisionReference) {
433
303
  }
434
304
  });
435
305
  for (const artifactId of new Set([...currentArtifactIds, ...targetIds])) {
436
- await rm(context.artifactPath(artifactId), { recursive: true, force: true });
306
+ await rm(context.artifactPath(artifactId), {
307
+ recursive: true,
308
+ force: true,
309
+ });
437
310
  }
438
311
  for (const artifact of targetArtifacts) {
439
- await materializeArtifact(context, artifact.artifact_id);
312
+ await materializeIgnoringForgotten(context, artifact.artifact_id);
440
313
  }
441
314
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
442
315
  });
443
316
  }
317
+ /**
318
+ * Workspace materialization after a restore tolerates forgotten objects:
319
+ * the restore commit is already durable, so a file whose bytes were
320
+ * deliberately deleted must not fail the whole restore — reads of that file
321
+ * surface OBJECT_UNAVAILABLE instead.
322
+ */
323
+ async function materializeIgnoringForgotten(context, artifactId) {
324
+ try {
325
+ await materializeArtifact(context, artifactId);
326
+ }
327
+ catch (error) {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ if (!/object unavailable/i.test(message))
330
+ throw error;
331
+ }
332
+ }
333
+ /**
334
+ * Restore is mechanical: every semantic table is reloaded from its
335
+ * `dolt_at_<table>` projection at the target revision, so the resulting
336
+ * state is exactly the state that revision recorded and no hand-maintained
337
+ * table list can drift. `SEMANTIC_TABLES` is ordered parent-before-child,
338
+ * so deleting in reverse order and reinserting in forward order satisfies
339
+ * foreign keys; columns come from `PRAGMA table_info`, not per-table code.
340
+ * Reinserted rows identical to HEAD produce no diff, so unchanged tables
341
+ * are never staged by the surrounding commit.
342
+ *
343
+ * The single deliberate exception is `objects`: its rows are permanent
344
+ * tombstone records, so restore merges instead of wiping — rows are never
345
+ * deleted and `forgotten_at` never rewinds (see `restoreObjectRows`).
346
+ */
347
+ function reloadSemanticTables(context, revision) {
348
+ const db = context.store.db;
349
+ const snapshots = SEMANTIC_TABLES.map((table) => {
350
+ const columns = db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name);
351
+ const rows = db
352
+ .prepare(`SELECT * FROM dolt_at_${table}(?)`)
353
+ .all(revision);
354
+ return { table, columns, rows };
355
+ });
356
+ for (const { table } of [...snapshots].reverse()) {
357
+ // `objects` is the one exception to the mechanical wipe: rows are never
358
+ // deleted (a row is the permanent tombstone for every historical
359
+ // reference) and `forgotten_at` never rewinds — see the upsert below.
360
+ if (table === "objects")
361
+ continue;
362
+ db.prepare(`DELETE FROM ${table}`).run();
363
+ }
364
+ for (const { table, columns, rows } of snapshots) {
365
+ if (rows.length === 0)
366
+ continue;
367
+ if (table === "objects") {
368
+ restoreObjectRows(db, columns, rows);
369
+ continue;
370
+ }
371
+ const insert = db.prepare(`INSERT INTO ${table}(${columns.join(", ")})
372
+ VALUES (${columns.map(() => "?").join(", ")})`);
373
+ for (const row of rows) {
374
+ insert.run(...columns.map((column) => row[column] ?? null));
375
+ }
376
+ }
377
+ }
378
+ /**
379
+ * Restores `objects` with merge semantics instead of wipe-and-reload:
380
+ * rows created after the target revision survive (object rows are never
381
+ * deleted), and an existing forget tombstone always wins over the target
382
+ * revision's live state (`forgotten_at` never rewinds — deleted bytes do
383
+ * not come back because history was restored).
384
+ */
385
+ function restoreObjectRows(db, columns, rows) {
386
+ const upsert = db.prepare(`INSERT INTO objects(${columns.join(", ")})
387
+ VALUES (${columns.map(() => "?").join(", ")})
388
+ ON CONFLICT(object_hash) DO UPDATE SET
389
+ forgotten_at=COALESCE(objects.forgotten_at, excluded.forgotten_at)`);
390
+ for (const row of rows) {
391
+ upsert.run(...columns.map((column) => row[column] ?? null));
392
+ }
393
+ }
444
394
  async function logAction(context, actionName, payload) {
445
395
  return resultOf(async () => {
446
396
  const result = await recordOperation(context, `action:${actionName}`, undefined, {
@@ -464,248 +414,10 @@ function actionLog(context, options = {}) {
464
414
  .map((revision) => ({
465
415
  hash: revision.hash,
466
416
  action: revision.operation.slice("action:".length),
467
- payload: revision.details?.payload ??
468
- {},
417
+ payload: revision.details?.payload ?? {},
469
418
  date: revision.date,
470
419
  }));
471
420
  }
472
- function actions(context, options = {}) {
473
- return syncResultOf(() => {
474
- const limit = Math.max(1, options.limit ?? 200);
475
- const params = [];
476
- let cursorClause = "";
477
- if (options.cursor) {
478
- const cursor = context.store.db
479
- .prepare("SELECT created_at, action_id FROM actions WHERE action_id=?")
480
- .get(options.cursor);
481
- if (cursor) {
482
- cursorClause = "WHERE (created_at < ? OR (created_at = ? AND action_id < ?))";
483
- params.push(cursor.created_at, cursor.created_at, cursor.action_id);
484
- }
485
- }
486
- params.push(limit + 1);
487
- const rows = context.store.db
488
- .prepare(`${ACTION_SELECT}
489
- ${cursorClause}
490
- ORDER BY created_at DESC, action_id DESC
491
- LIMIT ?`)
492
- .all(...params);
493
- const page = rows.slice(0, limit);
494
- return {
495
- headRevision: context.store.head,
496
- actions: page
497
- .map((row) => actionFromRow(context, row))
498
- .sort((left, right) => left.date.localeCompare(right.date)),
499
- ...(rows.length > limit ? { nextCursor: page.at(-1)?.action_id } : {}),
500
- };
501
- });
502
- }
503
- function action(context, actionId) {
504
- return syncResultOf(() => requiredAction(context, actionId));
505
- }
506
- async function recordAction(context, input) {
507
- return resultOf(async () => {
508
- assertWriteSet(context, input.baseRevision, input.writeSet ?? []);
509
- const actionId = input.actionId ?? newUuidV7();
510
- assertUuidV7(actionId, "Action ID");
511
- const phase = input.phase ?? "completed";
512
- const scope = input.scope ?? "book";
513
- assertActionPhase(phase);
514
- assertActionScope(scope);
515
- for (const parentId of input.parentActionIds ?? []) {
516
- assertUuidV7(parentId, "Parent action ID");
517
- requiredAction(context, parentId);
518
- }
519
- if (input.targetActionId) {
520
- assertUuidV7(input.targetActionId, "Target action ID");
521
- }
522
- const actor = input.actor ?? "videobook";
523
- const inputArtifacts = resolveArtifactReferences(context, input.inputArtifactIds ?? []);
524
- const outputArtifacts = resolveArtifactReferences(context, input.outputArtifactIds ?? []);
525
- const targetArtifactId = input.targetArtifactId
526
- ? context.artifactRow(input.targetArtifactId).artifact_id
527
- : null;
528
- const mutation = await context.store.semantic({
529
- operation: `action:${input.operation}`,
530
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
531
- details: {
532
- ...(input.details ?? {}),
533
- actionId,
534
- phase,
535
- scope,
536
- lane: input.lane ?? actor,
537
- },
538
- ...(input.baseRevision ? { baseRevision: input.baseRevision } : {}),
539
- writeSet: input.writeSet ?? [],
540
- }, [
541
- "actions",
542
- "action_events",
543
- "action_parents",
544
- "action_artifacts",
545
- "action_write_set",
546
- ], (operationId, now) => {
547
- context.store.db
548
- .prepare(`INSERT INTO actions(
549
- action_id, operation, scope, actor, lane, phase,
550
- base_revision, target_artifact_id, target_action_id,
551
- layout_json, details_json, created_at
552
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
553
- ON CONFLICT(action_id) DO UPDATE SET
554
- operation=excluded.operation,
555
- scope=excluded.scope,
556
- actor=excluded.actor,
557
- lane=excluded.lane,
558
- phase=excluded.phase,
559
- base_revision=excluded.base_revision,
560
- target_artifact_id=excluded.target_artifact_id,
561
- target_action_id=excluded.target_action_id,
562
- layout_json=excluded.layout_json,
563
- details_json=excluded.details_json`)
564
- .run(actionId, input.operation, scope, actor, input.lane ?? actor, phase, input.baseRevision ?? null, targetArtifactId, input.targetActionId ?? null, input.layout ? canonicalJson(input.layout) : null, canonicalJson(input.details ?? {}), now);
565
- context.store.db
566
- .prepare(`INSERT INTO action_events(
567
- event_id, action_id, operation_id, phase,
568
- details_json, created_at
569
- ) VALUES (?, ?, ?, ?, ?, ?)`)
570
- .run(newUuidV7(), actionId, operationId, phase, canonicalJson(input.details ?? {}), now);
571
- replaceActionLinks(context, actionId, input.parentActionIds ?? [], inputArtifacts, outputArtifacts, input.writeSet ?? []);
572
- });
573
- return ok({
574
- action: requiredAction(context, actionId),
575
- revision: revisionForHash(context, mutation.revision),
576
- }, mutation.revision);
577
- });
578
- }
579
- function requiredAction(context, actionId) {
580
- const row = context.store.db
581
- .prepare(`${ACTION_SELECT} WHERE action_id=?`)
582
- .get(actionId);
583
- if (!row) {
584
- throw new EngineFault({
585
- code: "NOT_FOUND",
586
- message: `History action not found: ${actionId}`,
587
- });
588
- }
589
- return actionFromRow(context, row);
590
- }
591
- function actionFromRow(context, row) {
592
- const parents = context.store.db
593
- .prepare("SELECT parent_action_id FROM action_parents WHERE action_id=?")
594
- .all(row.action_id).map((item) => item.parent_action_id);
595
- const links = context.store.db
596
- .prepare(`SELECT artifact_id, direction
597
- FROM action_artifacts WHERE action_id=?`)
598
- .all(row.action_id);
599
- const events = context.store.db
600
- .prepare(`SELECT event_id, action_id, operation_id, phase,
601
- details_json, created_at
602
- FROM action_events WHERE action_id=?
603
- ORDER BY created_at, event_id`)
604
- .all(row.action_id);
605
- const revisionsByOperation = new Map(allRevisions(context).map((revision) => [revision.operationId, revision]));
606
- return {
607
- id: row.action_id,
608
- operation: row.operation,
609
- title: titleForOperation(row.operation),
610
- scope: row.scope,
611
- actor: row.actor,
612
- lane: row.lane,
613
- date: new Date(row.created_at).toISOString(),
614
- phase: row.phase,
615
- ...(row.base_revision ? { baseRevision: row.base_revision } : {}),
616
- parentActionIds: parents,
617
- inputArtifacts: links
618
- .filter((link) => link.direction === "input")
619
- .map((link) => artifactRef(context, link.artifact_id)),
620
- outputArtifacts: links
621
- .filter((link) => link.direction === "output")
622
- .map((link) => artifactRef(context, link.artifact_id)),
623
- ...(row.target_artifact_id
624
- ? { targetArtifactId: row.target_artifact_id }
625
- : {}),
626
- ...(row.target_action_id ? { targetActionId: row.target_action_id } : {}),
627
- ...(row.layout_json
628
- ? {
629
- layout: parseJson(row.layout_json, { stage: 0, column: 0 }),
630
- }
631
- : {}),
632
- details: parseJson(row.details_json, {}),
633
- events: events.map((event) => {
634
- const revision = revisionsByOperation.get(event.operation_id);
635
- return {
636
- id: event.event_id,
637
- revision: revision?.hash ?? context.store.head,
638
- phase: event.phase,
639
- date: new Date(event.created_at).toISOString(),
640
- details: parseJson(event.details_json, {}),
641
- files: revision?.files ?? [],
642
- fileChanges: revision?.fileChanges ?? [],
643
- };
644
- }),
645
- };
646
- }
647
- function replaceActionLinks(context, actionId, parents, inputs, outputs, writeSet) {
648
- context.store.db
649
- .prepare("DELETE FROM action_parents WHERE action_id=?")
650
- .run(actionId);
651
- context.store.db
652
- .prepare("DELETE FROM action_artifacts WHERE action_id=?")
653
- .run(actionId);
654
- context.store.db
655
- .prepare("DELETE FROM action_write_set WHERE action_id=?")
656
- .run(actionId);
657
- const insertParent = context.store.db.prepare("INSERT INTO action_parents(action_id, parent_action_id) VALUES (?, ?)");
658
- for (const parent of new Set(parents))
659
- insertParent.run(actionId, parent);
660
- const insertArtifact = context.store.db.prepare(`INSERT INTO action_artifacts(action_id, artifact_id, direction)
661
- VALUES (?, ?, ?)`);
662
- for (const artifactId of new Set(inputs)) {
663
- insertArtifact.run(actionId, artifactId, "input");
664
- }
665
- for (const artifactId of new Set(outputs)) {
666
- insertArtifact.run(actionId, artifactId, "output");
667
- }
668
- const insertResource = context.store.db.prepare("INSERT INTO action_write_set(action_id, resource) VALUES (?, ?)");
669
- for (const resource of new Set(writeSet))
670
- insertResource.run(actionId, resource);
671
- }
672
- function assertWriteSet(context, baseRevision, writeSet) {
673
- if (!baseRevision || baseRevision === context.store.head)
674
- return;
675
- const revisions = allRevisions(context);
676
- let baseIndex = revisions.findIndex((revision) => revision.hash === baseRevision || revision.hash.startsWith(baseRevision));
677
- if (baseIndex < 0) {
678
- const commit = context.store.db.doltLog().find((item) => item.commit_hash === baseRevision || item.commit_hash.startsWith(baseRevision));
679
- if (!commit) {
680
- throw new EngineFault({
681
- code: "STALE_REVISION",
682
- message: `Base revision not found: ${baseRevision}`,
683
- });
684
- }
685
- // Initialization has no operation record, so it is older than every
686
- // operation-bearing revision we can inspect for a write-set conflict.
687
- baseIndex = revisions.length;
688
- }
689
- const requested = new Set(writeSet);
690
- const conflicts = new Set();
691
- for (const revision of revisions.slice(0, baseIndex)) {
692
- const changed = revision.details?.writeSet;
693
- if (!Array.isArray(changed))
694
- continue;
695
- for (const resource of changed) {
696
- if (typeof resource === "string" && requested.has(resource)) {
697
- conflicts.add(resource);
698
- }
699
- }
700
- }
701
- if (conflicts.size > 0) {
702
- throw new EngineFault({
703
- code: "ACTION_CONFLICT",
704
- message: `Action conflicts with newer changes: ${[...conflicts].join(", ")}`,
705
- details: { resources: [...conflicts] },
706
- });
707
- }
708
- }
709
421
  function resetArtifactRuntime(context, artifactId, now) {
710
422
  context.store.db
711
423
  .prepare(`INSERT INTO runtime_artifact_views(
@@ -759,111 +471,17 @@ function abortActiveJobs(context, now, message) {
759
471
  }
760
472
  function filesAt(context, revision, artifactId) {
761
473
  return context.store.db
762
- .prepare(`SELECT artifact_id, path, object_hash, mtime_ms, created_at
474
+ .prepare(`SELECT artifact_id, path, object_hash, created_at
763
475
  FROM dolt_at_artifact_files(?) WHERE artifact_id=? ORDER BY path`)
764
476
  .all(revision, artifactId);
765
477
  }
766
- function rowsForArtifactIds(context, table, columns, revision, artifactIds) {
767
- if (artifactIds.length === 0)
768
- return [];
769
- const placeholders = artifactIds.map(() => "?").join(", ");
770
- return context.store.db
771
- .prepare(`SELECT ${columns} FROM dolt_at_${table}(?)
772
- WHERE artifact_id IN (${placeholders})`)
773
- .all(revision, ...artifactIds);
774
- }
775
- function rowsForNotebookIds(context, table, columns, revision, notebookIds) {
776
- if (notebookIds.length === 0)
777
- return [];
778
- const placeholders = notebookIds.map(() => "?").join(", ");
779
- return context.store.db
780
- .prepare(`SELECT ${columns} FROM dolt_at_${table}(?)
781
- WHERE notebook_id IN (${placeholders})`)
782
- .all(revision, ...notebookIds);
783
- }
784
478
  function insertFiles(context, files) {
785
479
  const insert = context.store.db.prepare(`INSERT INTO artifact_files(
786
- artifact_id, path, object_hash, mtime_ms, created_at
787
- ) VALUES (?, ?, ?, ?, ?)`);
480
+ artifact_id, path, object_hash, created_at
481
+ ) VALUES (?, ?, ?, ?)`);
788
482
  for (const row of files) {
789
- insert.run(row.artifact_id, row.path, row.object_hash, row.mtime_ms, row.created_at);
790
- }
791
- }
792
- function insertNotebookChildren(context, cells, edges, runs) {
793
- const insertCell = context.store.db.prepare(`INSERT INTO cells(
794
- notebook_id, cell_id, type, title, position_x, position_y,
795
- entity_id, prompt, model, inputs_json, output_artifact_id
796
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
797
- for (const row of cells) {
798
- insertCell.run(row.notebook_id, row.cell_id, row.type, row.title, row.position_x, row.position_y, row.entity_id, row.prompt, row.model, row.inputs_json, row.output_artifact_id);
799
- }
800
- const insertEdge = context.store.db.prepare(`INSERT INTO edges(
801
- notebook_id, edge_id, source_cell_id, target_cell_id, target_input
802
- ) VALUES (?, ?, ?, ?, ?)`);
803
- for (const row of edges) {
804
- insertEdge.run(row.notebook_id, row.edge_id, row.source_cell_id, row.target_cell_id, row.target_input);
805
- }
806
- const insertRun = context.store.db.prepare(`INSERT INTO runs(
807
- run_id, notebook_id, status, started_at, completed_at,
808
- cell_order_json, outputs_json, error
809
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
810
- for (const row of runs) {
811
- insertRun.run(row.run_id, row.notebook_id, row.status, row.started_at, row.completed_at, row.cell_order_json, row.outputs_json, row.error);
812
- }
813
- }
814
- function operationFromDiff(row) {
815
- const prefix = row.diff_type === "removed" ? "from_" : "to_";
816
- const operationId = row[`${prefix}operation_id`];
817
- const operation = row[`${prefix}operation`];
818
- const detailsJson = row[`${prefix}details_json`];
819
- const writeSetJson = row[`${prefix}write_set_json`];
820
- const createdAt = row[`${prefix}created_at`];
821
- const author = row[`${prefix}author`];
822
- if (typeof operationId !== "string" ||
823
- typeof operation !== "string" ||
824
- typeof detailsJson !== "string" ||
825
- typeof writeSetJson !== "string" ||
826
- typeof createdAt !== "number" ||
827
- typeof author !== "string") {
828
- return null;
829
- }
830
- const artifactId = row[`${prefix}artifact_id`];
831
- const baseRevision = row[`${prefix}base_revision`];
832
- return {
833
- operation_id: operationId,
834
- operation,
835
- artifact_id: typeof artifactId === "string" ? artifactId : null,
836
- details_json: detailsJson,
837
- write_set_json: writeSetJson,
838
- base_revision: typeof baseRevision === "string" ? baseRevision : null,
839
- created_at: createdAt,
840
- author,
841
- };
842
- }
843
- function revisionFileChanges(rows, artifactId) {
844
- const changes = [];
845
- for (const row of rows) {
846
- const fromArtifact = row.from_artifact_id;
847
- const toArtifact = row.to_artifact_id;
848
- if (artifactId &&
849
- fromArtifact !== artifactId &&
850
- toArtifact !== artifactId) {
851
- continue;
852
- }
853
- const fromPath = typeof row.from_path === "string" ? row.from_path : undefined;
854
- const toPath = typeof row.to_path === "string" ? row.to_path : undefined;
855
- const file = toPath ?? fromPath;
856
- if (!file)
857
- continue;
858
- changes.push({
859
- status: row.diff_type,
860
- file,
861
- ...(fromPath && toPath && fromPath !== toPath
862
- ? { oldFile: fromPath }
863
- : {}),
864
- });
483
+ insert.run(row.artifact_id, row.path, row.object_hash, row.created_at);
865
484
  }
866
- return changes;
867
485
  }
868
486
  function artifactSlugAt(context, artifactId, revision) {
869
487
  const row = context.store.db
@@ -871,51 +489,4 @@ function artifactSlugAt(context, artifactId, revision) {
871
489
  .get(revision, artifactId);
872
490
  return row?.slug;
873
491
  }
874
- function resolveArtifactReferences(context, references) {
875
- return [
876
- ...new Set(references.map((reference) => context.artifactRow(reference).artifact_id)),
877
- ];
878
- }
879
- function artifactRef(context, artifactId) {
880
- try {
881
- const artifact = context.artifactRowById(artifactId);
882
- return {
883
- id: artifact.artifact_id,
884
- slug: artifact.slug,
885
- kind: historyArtifactKind(artifact.kind),
886
- };
887
- }
888
- catch {
889
- return { id: artifactId, slug: artifactId, kind: "unknown" };
890
- }
891
- }
892
- function historyArtifactKind(kind) {
893
- return kind;
894
- }
895
- function assertActionPhase(value) {
896
- if (![
897
- "requested",
898
- "started",
899
- "completed",
900
- "failed",
901
- "cancelled",
902
- "conflicted",
903
- ].includes(value)) {
904
- throw new Error(`Invalid action phase: ${value}`);
905
- }
906
- }
907
- function assertActionScope(value) {
908
- if (!["book", "artifact", "layout", "external", "system"].includes(value)) {
909
- throw new Error(`Invalid action scope: ${value}`);
910
- }
911
- }
912
- function titleForOperation(operation) {
913
- return operation.replaceAll("_", " ").replaceAll("-", " ");
914
- }
915
- const ACTION_SELECT = `
916
- SELECT action_id, operation, scope, actor, lane, phase,
917
- base_revision, target_artifact_id, target_action_id,
918
- layout_json, details_json, created_at
919
- FROM actions
920
- `;
921
492
  //# sourceMappingURL=history.js.map