videobook-engine 2.0.1 → 5.0.1

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 (171) hide show
  1. package/README.md +99 -68
  2. package/dist/artifacts.d.ts +4 -9
  3. package/dist/artifacts.d.ts.map +1 -1
  4. package/dist/artifacts.js +78 -138
  5. package/dist/artifacts.js.map +1 -1
  6. package/dist/books.d.ts +1 -1
  7. package/dist/books.d.ts.map +1 -1
  8. package/dist/books.js +18 -10
  9. package/dist/books.js.map +1 -1
  10. package/dist/cas.d.ts +27 -1
  11. package/dist/cas.d.ts.map +1 -1
  12. package/dist/cas.js +78 -6
  13. package/dist/cas.js.map +1 -1
  14. package/dist/catalog-metadata.d.ts +56 -0
  15. package/dist/catalog-metadata.d.ts.map +1 -0
  16. package/dist/catalog-metadata.js +42 -0
  17. package/dist/catalog-metadata.js.map +1 -0
  18. package/dist/communications.js +4 -2
  19. package/dist/communications.js.map +1 -1
  20. package/dist/context.d.ts +9 -6
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +42 -51
  23. package/dist/context.js.map +1 -1
  24. package/dist/domain.d.ts +39 -2
  25. package/dist/domain.d.ts.map +1 -1
  26. package/dist/domain.js +846 -64
  27. package/dist/domain.js.map +1 -1
  28. package/dist/edit-policy.d.ts +3 -0
  29. package/dist/edit-policy.d.ts.map +1 -0
  30. package/dist/edit-policy.js +6 -0
  31. package/dist/edit-policy.js.map +1 -0
  32. package/dist/edits.d.ts +10 -0
  33. package/dist/edits.d.ts.map +1 -0
  34. package/dist/edits.js +1392 -0
  35. package/dist/edits.js.map +1 -0
  36. package/dist/engine-types.d.ts +171 -86
  37. package/dist/engine-types.d.ts.map +1 -1
  38. package/dist/engine-types.js +32 -0
  39. package/dist/engine-types.js.map +1 -1
  40. package/dist/engine.d.ts +89 -26
  41. package/dist/engine.d.ts.map +1 -1
  42. package/dist/engine.js +27 -21
  43. package/dist/engine.js.map +1 -1
  44. package/dist/files.d.ts.map +1 -1
  45. package/dist/files.js +32 -28
  46. package/dist/files.js.map +1 -1
  47. package/dist/fork.d.ts +148 -0
  48. package/dist/fork.d.ts.map +1 -0
  49. package/dist/fork.js +602 -0
  50. package/dist/fork.js.map +1 -0
  51. package/dist/generations.d.ts +9 -0
  52. package/dist/generations.d.ts.map +1 -0
  53. package/dist/generations.js +175 -0
  54. package/dist/generations.js.map +1 -0
  55. package/dist/history.d.ts +2 -5
  56. package/dist/history.d.ts.map +1 -1
  57. package/dist/history.js +183 -640
  58. package/dist/history.js.map +1 -1
  59. package/dist/index.d.ts +21 -7
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +15 -5
  62. package/dist/index.js.map +1 -1
  63. package/dist/job-queue.d.ts +2 -0
  64. package/dist/job-queue.d.ts.map +1 -1
  65. package/dist/job-queue.js +29 -2
  66. package/dist/job-queue.js.map +1 -1
  67. package/dist/media.d.ts +3 -3
  68. package/dist/media.d.ts.map +1 -1
  69. package/dist/media.js +6 -5
  70. package/dist/media.js.map +1 -1
  71. package/dist/merge-policy.d.ts +83 -0
  72. package/dist/merge-policy.d.ts.map +1 -0
  73. package/dist/merge-policy.js +203 -0
  74. package/dist/merge-policy.js.map +1 -0
  75. package/dist/metadata.d.ts.map +1 -1
  76. package/dist/metadata.js +17 -23
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/migration.d.ts +9 -0
  79. package/dist/migration.d.ts.map +1 -0
  80. package/dist/migration.js +564 -0
  81. package/dist/migration.js.map +1 -0
  82. package/dist/mvp-contract-fixtures.d.ts +244 -0
  83. package/dist/mvp-contract-fixtures.d.ts.map +1 -0
  84. package/dist/mvp-contract-fixtures.js +556 -0
  85. package/dist/mvp-contract-fixtures.js.map +1 -0
  86. package/dist/mvp-contracts.d.ts +761 -0
  87. package/dist/mvp-contracts.d.ts.map +1 -0
  88. package/dist/mvp-contracts.js +59 -0
  89. package/dist/mvp-contracts.js.map +1 -0
  90. package/dist/mvp-time.d.ts +54 -0
  91. package/dist/mvp-time.d.ts.map +1 -0
  92. package/dist/mvp-time.js +148 -0
  93. package/dist/mvp-time.js.map +1 -0
  94. package/dist/notebook/types.d.ts +85 -8
  95. package/dist/notebook/types.d.ts.map +1 -1
  96. package/dist/notebook-grid.d.ts +37 -0
  97. package/dist/notebook-grid.d.ts.map +1 -0
  98. package/dist/notebook-grid.js +160 -0
  99. package/dist/notebook-grid.js.map +1 -0
  100. package/dist/notebook-media-artifact.d.ts +23 -0
  101. package/dist/notebook-media-artifact.d.ts.map +1 -0
  102. package/dist/notebook-media-artifact.js +67 -0
  103. package/dist/notebook-media-artifact.js.map +1 -0
  104. package/dist/notebook-mentions.d.ts +20 -0
  105. package/dist/notebook-mentions.d.ts.map +1 -0
  106. package/dist/notebook-mentions.js +53 -0
  107. package/dist/notebook-mentions.js.map +1 -0
  108. package/dist/order-keys.d.ts +22 -0
  109. package/dist/order-keys.d.ts.map +1 -0
  110. package/dist/order-keys.js +262 -0
  111. package/dist/order-keys.js.map +1 -0
  112. package/dist/runtime-services.js +8 -11
  113. package/dist/runtime-services.js.map +1 -1
  114. package/dist/schema.d.ts +8 -5
  115. package/dist/schema.d.ts.map +1 -1
  116. package/dist/schema.js +624 -141
  117. package/dist/schema.js.map +1 -1
  118. package/dist/search-benchmark.d.ts +4 -0
  119. package/dist/search-benchmark.d.ts.map +1 -0
  120. package/dist/search-benchmark.js +144 -0
  121. package/dist/search-benchmark.js.map +1 -0
  122. package/dist/sequences.d.ts +20 -0
  123. package/dist/sequences.d.ts.map +1 -0
  124. package/dist/sequences.js +825 -0
  125. package/dist/sequences.js.map +1 -0
  126. package/dist/similarity.js +10 -10
  127. package/dist/similarity.js.map +1 -1
  128. package/dist/status.js +8 -10
  129. package/dist/status.js.map +1 -1
  130. package/dist/storage.d.ts +3 -1
  131. package/dist/storage.d.ts.map +1 -1
  132. package/dist/storage.js +388 -7
  133. package/dist/storage.js.map +1 -1
  134. package/dist/store.d.ts +64 -3
  135. package/dist/store.d.ts.map +1 -1
  136. package/dist/store.js +354 -68
  137. package/dist/store.js.map +1 -1
  138. package/dist/streams.d.ts +10 -0
  139. package/dist/streams.d.ts.map +1 -0
  140. package/dist/streams.js +183 -0
  141. package/dist/streams.js.map +1 -0
  142. package/dist/temporal-models.d.ts +48 -0
  143. package/dist/temporal-models.d.ts.map +1 -0
  144. package/dist/temporal-models.js +278 -0
  145. package/dist/temporal-models.js.map +1 -0
  146. package/dist/temporal-search.d.ts +29 -0
  147. package/dist/temporal-search.d.ts.map +1 -0
  148. package/dist/temporal-search.js +1551 -0
  149. package/dist/temporal-search.js.map +1 -0
  150. package/dist/transcripts.d.ts +16 -0
  151. package/dist/transcripts.d.ts.map +1 -0
  152. package/dist/transcripts.js +519 -0
  153. package/dist/transcripts.js.map +1 -0
  154. package/docs/engine-layout.md +345 -110
  155. package/docs/mvp-prd.md +918 -0
  156. package/fixtures/media/vancat.mp4 +0 -0
  157. package/fixtures/media/vancat_profile.jpg +0 -0
  158. package/fixtures/v5/contract-fixtures.json +1532 -0
  159. package/package.json +42 -5
  160. package/dist/history-types.d.ts +0 -74
  161. package/dist/history-types.d.ts.map +0 -1
  162. package/dist/history-types.js +0 -2
  163. package/dist/history-types.js.map +0 -1
  164. package/dist/resolver.d.ts +0 -15
  165. package/dist/resolver.d.ts.map +0 -1
  166. package/dist/resolver.js +0 -81
  167. package/dist/resolver.js.map +0 -1
  168. package/dist/timeline.d.ts +0 -9
  169. package/dist/timeline.d.ts.map +0 -1
  170. package/dist/timeline.js +0 -236
  171. package/dist/timeline.js.map +0 -1
package/dist/history.js CHANGED
@@ -1,23 +1,21 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import { ok } from "./engine-types.js";
3
- import { resultOf, syncResultOf, } from "./context.js";
4
- import { artifactSlug } from "./artifacts.js";
3
+ import { resultOf } from "./context.js";
5
4
  import { materializeArtifact } from "./files.js";
6
5
  import { assertUuidV7, isUuidV7, newUuidV7 } from "./ids.js";
7
- import { canonicalJson, EngineFault, parseJson, } from "./store.js";
6
+ import { SCHEMA_VERSION, SEMANTIC_TABLES } from "./schema.js";
7
+ import { canonicalJson, commitDateIso, EngineFault, parseCommitMessage, } from "./store.js";
8
8
  export function createHistoryApi(context) {
9
9
  return {
10
10
  revisions: (limit = 20) => revisionHistory(context, limit),
11
11
  artifact: (artifact, limit = 20) => artifactHistory(context, artifact, limit),
12
12
  resolveRevision: (revision) => resolveRevision(context, revision),
13
+ diff: (fromRevision, toRevision, table) => context.store.db.doltDiff(fromRevision, toRevision, table),
13
14
  recordOperation: (operation, artifact, details) => recordOperation(context, operation, artifact, details),
14
- restoreArtifact: (artifactId, revision, slug) => restoreArtifact(context, artifactId, revision, slug),
15
+ restoreArtifact: (artifactId, revision) => restoreArtifact(context, artifactId, revision),
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,94 +28,74 @@ 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
- let artifactId;
46
- if (isUuidV7(artifactReference)) {
47
- artifactId = artifactReference;
48
- }
49
- else {
50
- try {
51
- artifactId = context.artifactRow(artifactReference).artifact_id;
52
- }
53
- catch {
54
- const historical = revisions.find((revision) => revision.artifactSlug === artifactReference ||
55
- revision.details?.slug === artifactReference ||
56
- revision.details?.oldSlug === artifactReference);
57
- if (!historical?.artifactId)
58
- return [];
59
- artifactId = historical.artifactId;
60
- }
61
- }
45
+ const revisions = commitRevisions(context);
46
+ if (!isUuidV7(artifactReference))
47
+ return [];
48
+ const artifactId = artifactReference;
62
49
  return revisions
63
50
  .filter((revision) => revision.artifactId === artifactId)
64
51
  .slice(0, Math.max(0, limit));
65
52
  }
66
- function allRevisions(context) {
67
- const commits = context.store.db.doltLog();
53
+ // History listings are direct commit-log reads: every engine commit carries
54
+ // its operation, parameters, and write set in its structured message, so no
55
+ // per-commit table diffs are needed to describe a revision.
56
+ function commitRevisions(context) {
68
57
  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
- }
58
+ for (const commit of context.store.db.doltLog()) {
59
+ const parsed = parseCommitMessage(commit.message);
60
+ if (!parsed)
61
+ continue;
62
+ revisions.push(revisionFromCommit(context, commit, parsed));
108
63
  }
109
64
  return revisions;
110
65
  }
66
+ function revisionFromCommit(context, commit, parsed) {
67
+ const artifactLabelAtRevision = parsed.artifactId
68
+ ? artifactLabelAt(context, parsed.artifactId, commit.commit_hash)
69
+ : undefined;
70
+ return {
71
+ hash: commit.commit_hash,
72
+ message: commit.message,
73
+ date: commitDateIso(commit.date),
74
+ author: parsed.actor ?? commit.committer,
75
+ operationId: parsed.operationId,
76
+ operation: parsed.operation,
77
+ ...(parsed.artifactId ? { artifactId: parsed.artifactId } : {}),
78
+ ...(artifactLabelAtRevision ? { artifactLabel: artifactLabelAtRevision } : {}),
79
+ details: {
80
+ ...parsed.details,
81
+ ...(parsed.writeSet.length > 0 ? { writeSet: parsed.writeSet } : {}),
82
+ ...(parsed.baseRevision ? { baseRevision: parsed.baseRevision } : {}),
83
+ },
84
+ };
85
+ }
111
86
  function resolveRevision(context, reference) {
112
- const historical = allRevisions(context).find((revision) => revision.hash === reference || revision.hash.startsWith(reference));
87
+ const historical = commitRevisions(context).find((revision) => revision.hash === reference || revision.hash.startsWith(reference));
113
88
  if (historical)
114
89
  return historical;
115
- const commit = context.store.db.doltLog().find((item) => item.commit_hash === reference || item.commit_hash.startsWith(reference));
90
+ const commit = context.store.db
91
+ .doltLog()
92
+ .find((item) => item.commit_hash === reference ||
93
+ item.commit_hash.startsWith(reference));
116
94
  return commit
117
95
  ? {
118
96
  hash: commit.commit_hash,
119
97
  message: commit.message,
120
- date: commit.date,
98
+ date: commitDateIso(commit.date),
121
99
  author: commit.committer,
122
100
  }
123
101
  : null;
@@ -139,24 +117,26 @@ async function recordOperation(context, operation, artifactReference, details) {
139
117
  : null;
140
118
  const mutation = await context.store.semantic({
141
119
  operation,
120
+ tables: [],
142
121
  ...(artifact ? { artifactId: artifact.artifact_id } : {}),
143
122
  details: {
144
123
  ...(details ?? {}),
145
- ...(artifact ? { artifactSlug: artifact.slug } : {}),
124
+ ...(artifact?.label ? { artifactLabel: artifact.label } : {}),
146
125
  },
147
- writeSet: artifact
148
- ? [`artifact:${artifact.artifact_id}`]
149
- : ["book"],
150
- }, [], () => undefined);
126
+ writeSet: artifact ? [`artifact:${artifact.artifact_id}`] : ["book"],
127
+ // Provenance-only operations change no semantic rows; the commit
128
+ // itself is their record, so it must be minted even when empty.
129
+ allowEmpty: true,
130
+ }, () => undefined);
151
131
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
152
132
  });
153
133
  }
154
- async function restoreArtifact(context, artifactId, revisionReference, replacementSlug) {
134
+ async function restoreArtifact(context, artifactId, revisionReference) {
155
135
  return resultOf(async () => {
156
136
  assertUuidV7(artifactId, "Artifact ID");
157
137
  const revision = requiredRevision(context, revisionReference);
158
138
  const target = context.store.db
159
- .prepare(`SELECT artifact_id, slug, kind, created_at
139
+ .prepare(`SELECT artifact_id, label, kind, created_at
160
140
  FROM dolt_at_artifacts(?)
161
141
  WHERE artifact_id=?`)
162
142
  .get(revision.hash, artifactId);
@@ -166,20 +146,6 @@ async function restoreArtifact(context, artifactId, revisionReference, replaceme
166
146
  message: `Artifact ${artifactId} did not exist at ${revision.hash}`,
167
147
  });
168
148
  }
169
- const desiredSlug = replacementSlug
170
- ? artifactSlug(target.kind, replacementSlug)
171
- : target.slug;
172
- const owner = context.store.db
173
- .prepare(`SELECT artifact_id FROM artifacts
174
- WHERE slug=? AND artifact_id<>?`)
175
- .get(desiredSlug, artifactId);
176
- if (owner) {
177
- throw new EngineFault({
178
- code: "SLUG_CONFLICT",
179
- message: `Slug ${desiredSlug} is owned by active artifact ${owner.artifact_id}`,
180
- ownerId: owner.artifact_id,
181
- });
182
- }
183
149
  const files = filesAt(context, revision.hash, artifactId);
184
150
  const metadata = context.store.db
185
151
  .prepare(`SELECT key, value_json
@@ -191,26 +157,24 @@ async function restoreArtifact(context, artifactId, revisionReference, replaceme
191
157
  .get(revision.hash, artifactId);
192
158
  const mutation = await context.store.semantic({
193
159
  operation: "restore_artifact",
194
- artifactId,
195
- details: { fromRevision: revision.hash, slug: desiredSlug },
196
- writeSet: [
197
- `artifact:${artifactId}`,
198
- `artifact-slug:${desiredSlug}`,
160
+ tables: [
161
+ "artifacts",
162
+ "artifact_files",
163
+ "artifact_metadata",
164
+ "audio_waveforms",
199
165
  ],
200
- }, [
201
- "artifacts",
202
- "artifact_files",
203
- "artifact_metadata",
204
- "audio_waveforms",
205
- ], (_operationId, now) => {
166
+ artifactId,
167
+ details: { fromRevision: revision.hash },
168
+ writeSet: [`artifact:${artifactId}`],
169
+ }, (_operationId, now) => {
206
170
  context.store.db
207
171
  .prepare(`INSERT INTO artifacts(
208
- artifact_id, slug, kind, created_at
172
+ artifact_id, label, kind, created_at
209
173
  ) VALUES (?, ?, ?, ?)
210
174
  ON CONFLICT(artifact_id) DO UPDATE SET
211
- slug=excluded.slug,
175
+ label=excluded.label,
212
176
  kind=excluded.kind`)
213
- .run(target.artifact_id, desiredSlug, target.kind, target.created_at);
177
+ .run(target.artifact_id, target.label, target.kind, target.created_at);
214
178
  context.store.db
215
179
  .prepare("DELETE FROM artifact_files WHERE artifact_id=?")
216
180
  .run(artifactId);
@@ -235,8 +199,11 @@ async function restoreArtifact(context, artifactId, revisionReference, replaceme
235
199
  }
236
200
  resetArtifactRuntime(context, artifactId, now);
237
201
  });
238
- await rm(context.artifactPath(artifactId), { recursive: true, force: true });
239
- await materializeArtifact(context, artifactId);
202
+ await rm(context.artifactPath(artifactId), {
203
+ recursive: true,
204
+ force: true,
205
+ });
206
+ await materializeIgnoringForgotten(context, artifactId);
240
207
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
241
208
  });
242
209
  }
@@ -244,7 +211,7 @@ async function restoreBook(context, revisionReference) {
244
211
  return resultOf(async () => {
245
212
  const revision = requiredRevision(context, revisionReference);
246
213
  const targetBook = context.store.db
247
- .prepare("SELECT book_id, slug, created_at FROM dolt_at_book(?)")
214
+ .prepare("SELECT book_id, name, created_at FROM dolt_at_book(?)")
248
215
  .get(revision.hash);
249
216
  if (!targetBook) {
250
217
  throw new EngineFault({
@@ -259,160 +226,33 @@ async function restoreBook(context, revisionReference) {
259
226
  message: "A restore cannot replace the catalog's stable book ID",
260
227
  });
261
228
  }
229
+ const targetSchema = context.store.db
230
+ .prepare("SELECT version FROM dolt_at_engine_schema(?) WHERE singleton=1")
231
+ .get(revision.hash);
232
+ if (!targetSchema || targetSchema.version !== SCHEMA_VERSION) {
233
+ throw new EngineFault({
234
+ code: "SCHEMA_INCOMPATIBLE",
235
+ message: `Cannot restore ${revision.hash}: it records schema version ` +
236
+ `${targetSchema?.version ?? "unknown"}, not the current ${SCHEMA_VERSION}`,
237
+ });
238
+ }
262
239
  const targetArtifacts = context.store.db
263
- .prepare(`SELECT artifact_id, slug, kind, created_at
240
+ .prepare(`SELECT artifact_id, label, kind, created_at
264
241
  FROM dolt_at_artifacts(?)
265
242
  ORDER BY artifact_id`)
266
243
  .all(revision.hash);
267
244
  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
245
  const currentArtifactIds = context.store.db
311
246
  .prepare("SELECT artifact_id FROM artifacts")
312
247
  .all()
313
248
  .map((row) => row.artifact_id);
314
249
  const mutation = await context.store.semantic({
315
250
  operation: "restore",
251
+ tables: SEMANTIC_TABLES,
316
252
  details: { fromRevision: revision.hash },
317
253
  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
- }
254
+ }, (_operationId, now) => {
255
+ reloadSemanticTables(context, revision.hash);
416
256
  abortActiveJobs(context, now, "Book restored");
417
257
  context.store.db
418
258
  .prepare(`UPDATE runtime_resource_leases
@@ -424,7 +264,9 @@ async function restoreBook(context, revisionReference) {
424
264
  context.store.db.prepare("DELETE FROM runtime_artifact_views").run();
425
265
  context.store.db.prepare("DELETE FROM runtime_pending_tasks").run();
426
266
  context.store.db.prepare("DELETE FROM runtime_generation_errors").run();
427
- context.store.db.prepare("DELETE FROM runtime_similarity_embeddings").run();
267
+ context.store.db
268
+ .prepare("DELETE FROM runtime_similarity_embeddings")
269
+ .run();
428
270
  context.store.db
429
271
  .prepare("DELETE FROM runtime_text_similarity_documents")
430
272
  .run();
@@ -433,14 +275,94 @@ async function restoreBook(context, revisionReference) {
433
275
  }
434
276
  });
435
277
  for (const artifactId of new Set([...currentArtifactIds, ...targetIds])) {
436
- await rm(context.artifactPath(artifactId), { recursive: true, force: true });
278
+ await rm(context.artifactPath(artifactId), {
279
+ recursive: true,
280
+ force: true,
281
+ });
437
282
  }
438
283
  for (const artifact of targetArtifacts) {
439
- await materializeArtifact(context, artifact.artifact_id);
284
+ await materializeIgnoringForgotten(context, artifact.artifact_id);
440
285
  }
441
286
  return ok(revisionForHash(context, mutation.revision), mutation.revision);
442
287
  });
443
288
  }
289
+ /**
290
+ * Workspace materialization after a restore tolerates forgotten objects:
291
+ * the restore commit is already durable, so a file whose bytes were
292
+ * deliberately deleted must not fail the whole restore — reads of that file
293
+ * surface OBJECT_UNAVAILABLE instead.
294
+ */
295
+ async function materializeIgnoringForgotten(context, artifactId) {
296
+ try {
297
+ await materializeArtifact(context, artifactId);
298
+ }
299
+ catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ if (!/object unavailable/i.test(message))
302
+ throw error;
303
+ }
304
+ }
305
+ /**
306
+ * Restore is mechanical: every semantic table is reloaded from its
307
+ * `dolt_at_<table>` projection at the target revision, so the resulting
308
+ * state is exactly the state that revision recorded and no hand-maintained
309
+ * table list can drift. `SEMANTIC_TABLES` is ordered parent-before-child,
310
+ * so deleting in reverse order and reinserting in forward order satisfies
311
+ * foreign keys; columns come from `PRAGMA table_info`, not per-table code.
312
+ * Reinserted rows identical to HEAD produce no diff, so unchanged tables
313
+ * are never staged by the surrounding commit.
314
+ *
315
+ * The single deliberate exception is `objects`: its rows are permanent
316
+ * tombstone records, so restore merges instead of wiping — rows are never
317
+ * deleted and `forgotten_at` never rewinds (see `restoreObjectRows`).
318
+ */
319
+ function reloadSemanticTables(context, revision) {
320
+ const db = context.store.db;
321
+ const snapshots = SEMANTIC_TABLES.map((table) => {
322
+ const columns = db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name);
323
+ const rows = db
324
+ .prepare(`SELECT * FROM dolt_at_${table}(?)`)
325
+ .all(revision);
326
+ return { table, columns, rows };
327
+ });
328
+ for (const { table } of [...snapshots].reverse()) {
329
+ // `objects` is the one exception to the mechanical wipe: rows are never
330
+ // deleted (a row is the permanent tombstone for every historical
331
+ // reference) and `forgotten_at` never rewinds — see the upsert below.
332
+ if (table === "objects")
333
+ continue;
334
+ db.prepare(`DELETE FROM ${table}`).run();
335
+ }
336
+ for (const { table, columns, rows } of snapshots) {
337
+ if (rows.length === 0)
338
+ continue;
339
+ if (table === "objects") {
340
+ restoreObjectRows(db, columns, rows);
341
+ continue;
342
+ }
343
+ const insert = db.prepare(`INSERT INTO ${table}(${columns.join(", ")})
344
+ VALUES (${columns.map(() => "?").join(", ")})`);
345
+ for (const row of rows) {
346
+ insert.run(...columns.map((column) => row[column] ?? null));
347
+ }
348
+ }
349
+ }
350
+ /**
351
+ * Restores `objects` with merge semantics instead of wipe-and-reload:
352
+ * rows created after the target revision survive (object rows are never
353
+ * deleted), and an existing forget tombstone always wins over the target
354
+ * revision's live state (`forgotten_at` never rewinds — deleted bytes do
355
+ * not come back because history was restored).
356
+ */
357
+ function restoreObjectRows(db, columns, rows) {
358
+ const upsert = db.prepare(`INSERT INTO objects(${columns.join(", ")})
359
+ VALUES (${columns.map(() => "?").join(", ")})
360
+ ON CONFLICT(object_hash) DO UPDATE SET
361
+ forgotten_at=COALESCE(objects.forgotten_at, excluded.forgotten_at)`);
362
+ for (const row of rows) {
363
+ upsert.run(...columns.map((column) => row[column] ?? null));
364
+ }
365
+ }
444
366
  async function logAction(context, actionName, payload) {
445
367
  return resultOf(async () => {
446
368
  const result = await recordOperation(context, `action:${actionName}`, undefined, {
@@ -464,248 +386,10 @@ function actionLog(context, options = {}) {
464
386
  .map((revision) => ({
465
387
  hash: revision.hash,
466
388
  action: revision.operation.slice("action:".length),
467
- payload: revision.details?.payload ??
468
- {},
389
+ payload: revision.details?.payload ?? {},
469
390
  date: revision.date,
470
391
  }));
471
392
  }
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
393
  function resetArtifactRuntime(context, artifactId, now) {
710
394
  context.store.db
711
395
  .prepare(`INSERT INTO runtime_artifact_views(
@@ -759,163 +443,22 @@ function abortActiveJobs(context, now, message) {
759
443
  }
760
444
  function filesAt(context, revision, artifactId) {
761
445
  return context.store.db
762
- .prepare(`SELECT artifact_id, path, object_hash, mtime_ms, created_at
446
+ .prepare(`SELECT artifact_id, path, object_hash, created_at
763
447
  FROM dolt_at_artifact_files(?) WHERE artifact_id=? ORDER BY path`)
764
448
  .all(revision, artifactId);
765
449
  }
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
450
  function insertFiles(context, files) {
785
451
  const insert = context.store.db.prepare(`INSERT INTO artifact_files(
786
- artifact_id, path, object_hash, mtime_ms, created_at
787
- ) VALUES (?, ?, ?, ?, ?)`);
452
+ artifact_id, path, object_hash, created_at
453
+ ) VALUES (?, ?, ?, ?)`);
788
454
  for (const row of files) {
789
- insert.run(row.artifact_id, row.path, row.object_hash, row.mtime_ms, row.created_at);
455
+ insert.run(row.artifact_id, row.path, row.object_hash, row.created_at);
790
456
  }
791
457
  }
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
- });
865
- }
866
- return changes;
867
- }
868
- function artifactSlugAt(context, artifactId, revision) {
458
+ function artifactLabelAt(context, artifactId, revision) {
869
459
  const row = context.store.db
870
- .prepare(`SELECT slug FROM dolt_at_artifacts(?) WHERE artifact_id=?`)
460
+ .prepare(`SELECT label FROM dolt_at_artifacts(?) WHERE artifact_id=?`)
871
461
  .get(revision, artifactId);
872
- return row?.slug;
873
- }
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("-", " ");
462
+ return row?.label ?? undefined;
914
463
  }
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
464
  //# sourceMappingURL=history.js.map