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/domain.js CHANGED
@@ -1,8 +1,12 @@
1
1
  import { ok } from "./engine-types.js";
2
- import { resultOf, syncResultOf, } from "./context.js";
2
+ import { normalizeSearchLocation } from "./mvp-time.js";
3
+ import { resultOf, syncResultOf } from "./context.js";
3
4
  import { assertUuidV7, newUuidV7 } from "./ids.js";
4
- import { canonicalJson, parseJson } from "./store.js";
5
+ import { LEGACY_NOTEBOOK_CELL_TYPE_ALIASES, NOTEBOOK_CELL_TYPES, } from "./schema.js";
6
+ import { firstEmptyNotebookGridSlots, isNotebookGridSlot, } from "./notebook-grid.js";
7
+ import { canonicalJson, commitDateIso, parseCommitMessage, parseJson, } from "./store.js";
5
8
  import { EngineFault } from "./store.js";
9
+ const NOTEBOOK_CELL_TYPE_SET = new Set(NOTEBOOK_CELL_TYPES);
6
10
  export function createEntitiesApi(context) {
7
11
  return {
8
12
  create: (type, name, input = {}) => createEntity(context, type, name, input),
@@ -25,7 +29,13 @@ export function createNotebooksApi(context) {
25
29
  }),
26
30
  list: () => listNotebooks(context),
27
31
  read: (notebookId) => syncResultOf(() => requiredNotebook(context, notebookId)),
28
- write: (notebook) => writeNotebook(context, notebook),
32
+ write: (notebook, options) => writeNotebook(context, notebook, options),
33
+ cellHistory: (notebookId, cellId, options) => cellHistory(context, notebookId, cellId, options),
34
+ executionHistory: (notebookId, cellId, options) => executionHistory(context, notebookId, cellId, options),
35
+ insertCell: (notebookId, cell) => insertNotebookCell(context, notebookId, cell),
36
+ updateCell: (notebookId, cell) => updateNotebookCell(context, notebookId, cell),
37
+ moveCell: (notebookId, cellId, slot) => moveNotebookCell(context, notebookId, cellId, slot),
38
+ removeCell: (notebookId, cellId) => removeNotebookCell(context, notebookId, cellId),
29
39
  delete: (notebookId) => deleteNotebook(context, notebookId),
30
40
  recordRun: (run) => recordNotebookRun(context, run),
31
41
  };
@@ -37,9 +47,10 @@ async function createEntity(context, type, name, input) {
37
47
  const entityId = newUuidV7();
38
48
  const mutation = await context.store.semantic({
39
49
  operation: "create_entity",
50
+ tables: ["entities"],
40
51
  details: { entityId, type, name: normalizedName },
41
52
  writeSet: [`entity:${entityId}`],
42
- }, ["entities"], (_operationId, now) => {
53
+ }, (_operationId, now) => {
43
54
  context.store.db
44
55
  .prepare(`INSERT INTO entities(
45
56
  entity_id, type, name, description, prompt, data_json, created_at
@@ -70,9 +81,10 @@ async function writeEntity(context, entity) {
70
81
  const name = requiredText(entity.name, "Entity name");
71
82
  const mutation = await context.store.semantic({
72
83
  operation: "write_entity",
84
+ tables: ["entities"],
73
85
  details: { entityId: entity.id, type: entity.type },
74
86
  writeSet: [`entity:${entity.id}`],
75
- }, ["entities"], () => {
87
+ }, () => {
76
88
  context.store.db
77
89
  .prepare(`UPDATE entities
78
90
  SET type=?, name=?, description=?, prompt=?, data_json=?
@@ -88,7 +100,7 @@ async function deleteEntity(context, entityId) {
88
100
  requiredEntity(context, entityId);
89
101
  const cells = context.store.db
90
102
  .prepare(`SELECT notebook_id, cell_id FROM cells
91
- WHERE entity_id=? ORDER BY notebook_id, cell_id`)
103
+ WHERE output_entity_id=? ORDER BY notebook_id, cell_id`)
92
104
  .all(entityId);
93
105
  if (cells.length > 0) {
94
106
  throw new EngineFault({
@@ -96,7 +108,7 @@ async function deleteEntity(context, entityId) {
96
108
  message: `Entity is still referenced: ${entityId}`,
97
109
  details: {
98
110
  references: cells.map((cell) => ({
99
- kind: "cell.entity",
111
+ kind: "cell.output_entity",
100
112
  id: `${cell.notebook_id}/${cell.cell_id}`,
101
113
  })),
102
114
  },
@@ -104,9 +116,10 @@ async function deleteEntity(context, entityId) {
104
116
  }
105
117
  const mutation = await context.store.semantic({
106
118
  operation: "delete_entity",
119
+ tables: ["entities"],
107
120
  details: { entityId },
108
121
  writeSet: [`entity:${entityId}`],
109
- }, ["entities"], () => {
122
+ }, () => {
110
123
  context.store.db
111
124
  .prepare("DELETE FROM entities WHERE entity_id=?")
112
125
  .run(entityId);
@@ -120,13 +133,14 @@ async function createNotebook(context, name) {
120
133
  const notebookId = newUuidV7();
121
134
  const mutation = await context.store.semantic({
122
135
  operation: "create_notebook",
136
+ tables: ["notebooks"],
123
137
  details: { notebookId, name: normalizedName },
124
138
  writeSet: [`notebook:${notebookId}`],
125
- }, ["notebooks"], (_operationId, now) => {
139
+ }, (_operationId, now) => {
126
140
  context.store.db
127
141
  .prepare(`INSERT INTO notebooks(
128
- notebook_id, name, properties_json, created_at
129
- ) VALUES (?, ?, '{}', ?)`)
142
+ notebook_id, name, created_at
143
+ ) VALUES (?, ?, ?)`)
130
144
  .run(notebookId, normalizedName, now);
131
145
  });
132
146
  return ok(requiredNotebook(context, notebookId), mutation.revision);
@@ -138,38 +152,326 @@ function listNotebooks(context) {
138
152
  .all();
139
153
  return rows.map((row) => notebookFromRows(context, row));
140
154
  }
141
- async function writeNotebook(context, notebook) {
155
+ async function writeNotebook(context, notebook, options) {
142
156
  return resultOf(async () => {
143
157
  assertUuidV7(notebook.id, "Notebook ID");
144
158
  requiredNotebook(context, notebook.id);
145
159
  validateNotebook(context, notebook);
146
160
  const mutation = await context.store.semantic({
147
- operation: "write_notebook",
148
- details: { notebookId: notebook.id },
161
+ operation: options?.operation ?? "write_notebook",
162
+ tables: [
163
+ "notebooks",
164
+ "notebook_fields",
165
+ "cells",
166
+ "notebook_cell_executions",
167
+ "notebook_generation_plans",
168
+ "notebook_run_plans",
169
+ "edges",
170
+ "cell_references",
171
+ "pinned_search_results",
172
+ ],
173
+ details: { notebookId: notebook.id, ...options?.details },
174
+ ...(options?.artifactId ? { artifactId: options.artifactId } : {}),
149
175
  writeSet: [
150
176
  `notebook:${notebook.id}`,
151
177
  ...notebook.cells.map((cell) => `cell:${notebook.id}:${cell.id}`),
152
178
  ...notebook.edges.map((edge) => `edge:${notebook.id}:${edge.id}`),
153
179
  ],
154
- }, ["notebooks", "cells", "edges"], () => {
180
+ }, () => {
155
181
  context.store.db
156
- .prepare(`UPDATE notebooks SET name=?, properties_json=?
157
- WHERE notebook_id=?`)
158
- .run(requiredText(notebook.name, "Notebook name"), canonicalJson(notebook.properties ?? {}), notebook.id);
182
+ .prepare(`UPDATE notebooks SET name=? WHERE notebook_id=?`)
183
+ .run(requiredText(notebook.name, "Notebook name"), notebook.id);
159
184
  synchronizeNotebookChildren(context, notebook);
185
+ synchronizeNotebookState(context, notebook);
186
+ });
187
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
188
+ });
189
+ }
190
+ function cellHistory(context, notebookId, cellId, options) {
191
+ assertUuidV7(notebookId, "Notebook ID");
192
+ assertUuidV7(cellId, "Cell ID");
193
+ const limit = Math.max(1, Math.min(options?.limit ?? 50, 500));
194
+ const commitMeta = commitMetaByHash(context);
195
+ let rows;
196
+ try {
197
+ rows = context.store.db
198
+ .prepare(`SELECT notebook_id, cell_id, type, label, grid_row, grid_column,
199
+ output_entity_id, prompt, provider, model, operation, tool,
200
+ inputs_json, output_artifact_id, commit_hash, committer, commit_date
201
+ FROM dolt_history_cells
202
+ WHERE notebook_id=? AND cell_id=?
203
+ ORDER BY commit_date DESC
204
+ LIMIT ?`)
205
+ .all(notebookId, cellId, limit);
206
+ }
207
+ catch {
208
+ rows = context.store.db.doltHistoryOf("cells")
209
+ .filter((row) => row.notebook_id === notebookId && row.cell_id === cellId)
210
+ .sort((left, right) => right.commit_date.localeCompare(left.commit_date))
211
+ .slice(0, limit);
212
+ }
213
+ return rows.map((row) => {
214
+ const meta = commitMeta.get(row.commit_hash);
215
+ return {
216
+ commitHash: row.commit_hash,
217
+ commitDate: commitDateIso(row.commit_date),
218
+ committer: row.committer,
219
+ ...(meta?.operation ? { operation: meta.operation } : {}),
220
+ ...(meta?.operationId ? { operationId: meta.operationId } : {}),
221
+ ...(meta?.artifactId ? { artifactId: meta.artifactId } : {}),
222
+ ...(meta?.details ? { details: meta.details } : {}),
223
+ cell: rowToCell(row, [], []),
224
+ };
225
+ });
226
+ }
227
+ function executionHistory(context, notebookId, cellId, options) {
228
+ assertUuidV7(notebookId, "Notebook ID");
229
+ assertUuidV7(cellId, "Cell ID");
230
+ const limit = Math.max(1, Math.min(options?.limit ?? 50, 500));
231
+ const commitMeta = commitMetaByHash(context);
232
+ let rows;
233
+ try {
234
+ rows = context.store.db
235
+ .prepare(`SELECT notebook_id, cell_id, fingerprint, status, output_artifact_id,
236
+ provider_artifact_id, run_id, completed_at, started_at,
237
+ updated_at, tool, error, stale, fixture_baseline,
238
+ commit_hash, committer, commit_date
239
+ FROM dolt_history_notebook_cell_executions
240
+ WHERE notebook_id=? AND cell_id=?
241
+ ORDER BY commit_date DESC
242
+ LIMIT ?`)
243
+ .all(notebookId, cellId, limit);
244
+ }
245
+ catch {
246
+ rows = context.store.db.doltHistoryOf("notebook_cell_executions")
247
+ .filter((row) => row.notebook_id === notebookId && row.cell_id === cellId)
248
+ .sort((left, right) => right.commit_date.localeCompare(left.commit_date))
249
+ .slice(0, limit);
250
+ }
251
+ return rows.map((row) => {
252
+ const meta = commitMeta.get(row.commit_hash);
253
+ return {
254
+ commitHash: row.commit_hash,
255
+ commitDate: commitDateIso(row.commit_date),
256
+ committer: row.committer,
257
+ notebookId: row.notebook_id,
258
+ cellId: row.cell_id,
259
+ ...(meta?.operation ? { operation: meta.operation } : {}),
260
+ ...(meta?.operationId ? { operationId: meta.operationId } : {}),
261
+ ...(meta?.artifactId ? { artifactId: meta.artifactId } : {}),
262
+ ...(meta?.details ? { details: meta.details } : {}),
263
+ execution: executionFromRow(row),
264
+ };
265
+ });
266
+ }
267
+ function commitMetaByHash(context) {
268
+ const map = new Map();
269
+ for (const commit of context.store.db.doltLog()) {
270
+ const parsed = parseCommitMessage(commit.message);
271
+ if (!parsed)
272
+ continue;
273
+ map.set(commit.commit_hash, {
274
+ operation: parsed.operation,
275
+ operationId: parsed.operationId,
276
+ ...(parsed.artifactId ? { artifactId: parsed.artifactId } : {}),
277
+ details: parsed.details,
278
+ });
279
+ }
280
+ return map;
281
+ }
282
+ async function insertNotebookCell(context, notebookId, cell) {
283
+ return resultOf(async () => {
284
+ assertUuidV7(notebookId, "Notebook ID");
285
+ const notebook = requiredNotebook(context, notebookId);
286
+ if (notebook.cells.some((existing) => existing.id === cell.id)) {
287
+ throw new Error(`Cell already exists: ${cell.id}`);
288
+ }
289
+ const prospective = { ...notebook, cells: [...notebook.cells, cell] };
290
+ validateCellFields(context, prospective, cell);
291
+ assertCellSlotFree(context, notebookId, cell);
292
+ const mutation = await context.store.semantic({
293
+ operation: "insert_cell",
294
+ tables: ["cells", "cell_references", "pinned_search_results"],
295
+ details: { notebookId, cellId: cell.id },
296
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cell.id}`],
297
+ }, () => {
298
+ upsertNotebookCell(context, notebookId, cell);
299
+ repairCellSlots(context, notebookId, cell.id);
300
+ });
301
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
302
+ });
303
+ }
304
+ async function updateNotebookCell(context, notebookId, cell) {
305
+ return resultOf(async () => {
306
+ assertUuidV7(notebookId, "Notebook ID");
307
+ const notebook = requiredNotebook(context, notebookId);
308
+ if (!notebook.cells.some((existing) => existing.id === cell.id)) {
309
+ throw new EngineFault({
310
+ code: "NOT_FOUND",
311
+ message: `Cell not found: ${cell.id}`,
312
+ });
313
+ }
314
+ const prospective = {
315
+ ...notebook,
316
+ cells: notebook.cells.map((existing) => existing.id === cell.id ? cell : existing),
317
+ };
318
+ validateCellFields(context, prospective, cell);
319
+ assertCellSlotFree(context, notebookId, cell);
320
+ const mutation = await context.store.semantic({
321
+ operation: "update_cell",
322
+ tables: ["cells", "cell_references", "pinned_search_results"],
323
+ details: { notebookId, cellId: cell.id },
324
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cell.id}`],
325
+ }, () => {
326
+ upsertNotebookCell(context, notebookId, cell);
327
+ repairCellSlots(context, notebookId, cell.id);
160
328
  });
161
329
  return ok(revisionFor(context, mutation.revision), mutation.revision);
162
330
  });
163
331
  }
332
+ async function moveNotebookCell(context, notebookId, cellId, slot) {
333
+ return resultOf(async () => {
334
+ assertUuidV7(notebookId, "Notebook ID");
335
+ assertUuidV7(cellId, "Cell ID");
336
+ const notebook = requiredNotebook(context, notebookId);
337
+ const existing = notebook.cells.find((cell) => cell.id === cellId);
338
+ if (!existing) {
339
+ throw new EngineFault({
340
+ code: "NOT_FOUND",
341
+ message: `Cell not found: ${cellId}`,
342
+ });
343
+ }
344
+ return updateNotebookCell(context, notebookId, { ...existing, slot });
345
+ });
346
+ }
347
+ async function removeNotebookCell(context, notebookId, cellId) {
348
+ return resultOf(async () => {
349
+ assertUuidV7(notebookId, "Notebook ID");
350
+ assertUuidV7(cellId, "Cell ID");
351
+ requiredNotebook(context, notebookId);
352
+ const found = context.store.db
353
+ .prepare("SELECT 1 AS present FROM cells WHERE notebook_id=? AND cell_id=?")
354
+ .get(notebookId, cellId);
355
+ if (!found) {
356
+ throw new EngineFault({
357
+ code: "NOT_FOUND",
358
+ message: `Cell not found: ${cellId}`,
359
+ });
360
+ }
361
+ const mutation = await context.store.semantic({
362
+ operation: "remove_cell",
363
+ tables: [
364
+ "cells",
365
+ "generations",
366
+ "notebook_cell_executions",
367
+ "notebook_generation_plans",
368
+ "edges",
369
+ "cell_references",
370
+ "pinned_search_results",
371
+ ],
372
+ details: { notebookId, cellId },
373
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cellId}`],
374
+ }, () => {
375
+ context.store.db
376
+ .prepare("DELETE FROM cells WHERE notebook_id=? AND cell_id=?")
377
+ .run(notebookId, cellId);
378
+ repairCellSlots(context, notebookId);
379
+ });
380
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
381
+ });
382
+ }
383
+ function assertCellSlotFree(context, notebookId, cell) {
384
+ const found = context.store.db
385
+ .prepare(`SELECT cell_id FROM cells
386
+ WHERE notebook_id=? AND grid_row=? AND grid_column=? AND cell_id<>?`)
387
+ .get(notebookId, cell.slot.row, cell.slot.column, cell.id);
388
+ if (found) {
389
+ throw new Error(`Cell slot is occupied: ${cell.slot.row}:${cell.slot.column}`);
390
+ }
391
+ }
392
+ function repairCellSlots(context, notebookId, protectedCellId) {
393
+ const rows = context.store.db
394
+ .prepare(`SELECT cell_id, grid_row, grid_column FROM cells
395
+ WHERE notebook_id=?
396
+ ORDER BY grid_row, grid_column, cell_id`)
397
+ .all(notebookId);
398
+ const slots = new Map();
399
+ for (const row of rows) {
400
+ const key = `${row.grid_row}:${row.grid_column}`;
401
+ const group = slots.get(key) ?? [];
402
+ group.push(row);
403
+ slots.set(key, group);
404
+ }
405
+ const occupied = [];
406
+ const losers = [];
407
+ for (const group of slots.values()) {
408
+ const winner = protectedCellId && group.some((row) => row.cell_id === protectedCellId)
409
+ ? protectedCellId
410
+ : group[0].cell_id;
411
+ const winnerRow = group.find((row) => row.cell_id === winner);
412
+ occupied.push({
413
+ row: winnerRow.grid_row,
414
+ column: winnerRow.grid_column,
415
+ });
416
+ losers.push(...group
417
+ .filter((row) => row.cell_id !== winner)
418
+ .map((row) => row.cell_id));
419
+ }
420
+ const destinations = firstEmptyNotebookGridSlots(occupied, losers.length);
421
+ const move = context.store.db.prepare(`UPDATE cells SET grid_row=?, grid_column=?
422
+ WHERE notebook_id=? AND cell_id=?`);
423
+ for (const [index, cellId] of losers.entries()) {
424
+ const slot = destinations[index];
425
+ move.run(slot.row, slot.column, notebookId, cellId);
426
+ }
427
+ }
428
+ function upsertNotebookCell(context, notebookId, cell) {
429
+ const normalized = normalizeCellForWrite(cell);
430
+ context.store.db
431
+ .prepare(`INSERT INTO cells(
432
+ notebook_id, cell_id, type, label, grid_row, grid_column,
433
+ output_entity_id, prompt, provider, model, operation, tool,
434
+ inputs_json, output_artifact_id
435
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
436
+ ON CONFLICT(notebook_id, cell_id) DO UPDATE SET
437
+ type=excluded.type,
438
+ label=excluded.label,
439
+ grid_row=excluded.grid_row,
440
+ grid_column=excluded.grid_column,
441
+ output_entity_id=excluded.output_entity_id,
442
+ prompt=excluded.prompt,
443
+ provider=excluded.provider,
444
+ model=excluded.model,
445
+ operation=excluded.operation,
446
+ tool=excluded.tool,
447
+ inputs_json=excluded.inputs_json,
448
+ output_artifact_id=excluded.output_artifact_id`)
449
+ .run(notebookId, normalized.id, normalized.type, normalized.label?.trim() || null, normalized.slot.row, normalized.slot.column, normalized.outputEntityId ?? null, normalized.prompt ?? null, normalized.provider ?? null, normalized.model ?? null, normalized.operation ?? null, normalized.tool ?? null, canonicalJson(normalized.inputs ?? {}), normalized.outputArtifactId ?? null);
450
+ synchronizeCellReferences(context, notebookId, normalized);
451
+ synchronizePinnedResults(context, notebookId, normalized);
452
+ }
164
453
  async function deleteNotebook(context, notebookId) {
165
454
  return resultOf(async () => {
166
455
  assertUuidV7(notebookId, "Notebook ID");
167
456
  requiredNotebook(context, notebookId);
168
457
  const mutation = await context.store.semantic({
169
458
  operation: "delete_notebook",
459
+ tables: [
460
+ "notebooks",
461
+ "notebook_fields",
462
+ "cells",
463
+ "generations",
464
+ "notebook_cell_executions",
465
+ "notebook_generation_plans",
466
+ "notebook_run_plans",
467
+ "edges",
468
+ "runs",
469
+ "cell_references",
470
+ "pinned_search_results",
471
+ ],
170
472
  details: { notebookId },
171
473
  writeSet: [`notebook:${notebookId}`],
172
- }, ["notebooks", "cells", "edges", "runs"], () => {
474
+ }, () => {
173
475
  context.store.db
174
476
  .prepare("DELETE FROM notebooks WHERE notebook_id=?")
175
477
  .run(notebookId);
@@ -190,9 +492,10 @@ async function recordNotebookRun(context, run) {
190
492
  }
191
493
  const mutation = await context.store.semantic({
192
494
  operation: "record_notebook_run",
495
+ tables: ["runs"],
193
496
  details: { notebookId: run.notebookId, runId: run.id },
194
497
  writeSet: [`run:${run.id}`],
195
- }, ["runs"], () => {
498
+ }, () => {
196
499
  context.store.db
197
500
  .prepare(`INSERT INTO runs(
198
501
  run_id, notebook_id, status, started_at,
@@ -231,47 +534,104 @@ function requiredNotebook(context, notebookId) {
231
534
  return notebookFromRows(context, row);
232
535
  }
233
536
  function notebookFromRows(context, row) {
537
+ const fields = context.store.db
538
+ .prepare(`SELECT field, value_json
539
+ FROM notebook_fields WHERE notebook_id=?
540
+ ORDER BY field`)
541
+ .all(row.notebook_id);
234
542
  const cells = context.store.db
235
- .prepare(`SELECT cell_id, type, title, position_x, position_y, entity_id,
236
- prompt, model, inputs_json, output_artifact_id
237
- FROM cells WHERE notebook_id=? ORDER BY cell_id`)
543
+ .prepare(`SELECT cell_id, type, label, grid_row, grid_column, output_entity_id,
544
+ prompt, provider, model, operation, tool,
545
+ inputs_json, output_artifact_id
546
+ FROM cells WHERE notebook_id=?
547
+ ORDER BY grid_row, grid_column, cell_id`)
238
548
  .all(row.notebook_id);
239
549
  const edges = context.store.db
240
550
  .prepare(`SELECT edge_id, source_cell_id, target_cell_id, target_input
241
551
  FROM edges WHERE notebook_id=? ORDER BY edge_id`)
242
552
  .all(row.notebook_id);
553
+ const references = context.store.db
554
+ .prepare(`SELECT cell_id, reference_id, kind, target_id, snapshot_json, ordinal
555
+ FROM cell_references
556
+ WHERE notebook_id=?
557
+ ORDER BY cell_id, ordinal, reference_id`)
558
+ .all(row.notebook_id);
559
+ const pinnedResults = context.store.db
560
+ .prepare(`SELECT cell_id, result_id, artifact_id, object_hash, location_json,
561
+ representative_json, query_json, signals_json,
562
+ selected_revision, ordinal, created_at
563
+ FROM pinned_search_results
564
+ WHERE notebook_id=?
565
+ ORDER BY cell_id, ordinal, result_id`)
566
+ .all(row.notebook_id);
567
+ const executions = context.store.db
568
+ .prepare(`SELECT cell_id, fingerprint, status, output_artifact_id,
569
+ provider_artifact_id, run_id, completed_at, started_at,
570
+ updated_at, tool, error, stale, fixture_baseline
571
+ FROM notebook_cell_executions
572
+ WHERE notebook_id=? ORDER BY cell_id`)
573
+ .all(row.notebook_id);
574
+ const generationPlans = context.store.db
575
+ .prepare(`SELECT plan_id, cell_id, status, plan_json, output_artifact_id,
576
+ error, created_at, updated_at
577
+ FROM notebook_generation_plans
578
+ WHERE notebook_id=? ORDER BY created_at, plan_id`)
579
+ .all(row.notebook_id);
580
+ const runPlans = context.store.db
581
+ .prepare(`SELECT plan_id, status, plan_json, paid_cell_ids_json,
582
+ cell_fingerprints_json, known_cost_usd, unknown_cost_count,
583
+ created_at, updated_at, run_id, outputs_json, error
584
+ FROM notebook_run_plans
585
+ WHERE notebook_id=? ORDER BY created_at, plan_id`)
586
+ .all(row.notebook_id);
587
+ const field = new Map(fields.map((item) => [
588
+ item.field,
589
+ parseJson(item.value_json, null),
590
+ ]));
243
591
  return {
244
592
  id: row.notebook_id,
245
593
  name: row.name,
246
- properties: parseJson(row.properties_json, {}),
247
- cells: cells.map(rowToCell),
594
+ ...(typeof field.get("description") === "string"
595
+ ? { description: field.get("description") }
596
+ : {}),
597
+ ...(typeof field.get("lifecycle_state") === "string"
598
+ ? { lifecycleState: field.get("lifecycle_state") }
599
+ : {}),
600
+ ...(typeof field.get("workflow_version") === "number"
601
+ ? { workflowVersion: field.get("workflow_version") }
602
+ : {}),
603
+ ...(typeof field.get("analysis_revision") === "string"
604
+ ? { analysisRevision: field.get("analysis_revision") }
605
+ : {}),
606
+ ...(isRecord(field.get("fixture"))
607
+ ? { fixture: field.get("fixture") }
608
+ : {}),
609
+ execution: Object.fromEntries(executions.map((execution) => [
610
+ execution.cell_id,
611
+ executionFromRow(execution),
612
+ ])),
613
+ generationPlans: generationPlans.map(generationPlanFromRow),
614
+ notebookRunPlans: runPlans.map(runPlanFromRow),
615
+ cells: cells.map((cell) => rowToCell(cell, references.filter((reference) => reference.cell_id === cell.cell_id), pinnedResults.filter((result) => result.cell_id === cell.cell_id))),
248
616
  edges: edges.map(rowToEdge),
249
617
  createdAt: new Date(row.created_at).toISOString(),
250
618
  };
251
619
  }
252
620
  function validateNotebook(context, notebook) {
253
621
  const cellIds = new Set();
622
+ const occupiedSlots = new Set();
254
623
  for (const cell of notebook.cells) {
255
- assertUuidV7(cell.id, "Cell ID");
256
624
  if (cellIds.has(cell.id))
257
625
  throw new Error(`Duplicate cell ID: ${cell.id}`);
258
626
  cellIds.add(cell.id);
259
- if (!["prompt", "character", "scene", "asset", "image", "video"].includes(cell.type)) {
260
- throw new Error(`Invalid cell type: ${cell.type}`);
261
- }
262
- if (!Number.isFinite(cell.position.x) || !Number.isFinite(cell.position.y)) {
263
- throw new Error(`Cell position must be finite: ${cell.id}`);
264
- }
265
- if (cell.entityId) {
266
- assertUuidV7(cell.entityId, "Cell entity ID");
267
- requiredEntity(context, cell.entityId);
268
- }
269
- if (cell.outputArtifactId) {
270
- assertUuidV7(cell.outputArtifactId, "Cell output artifact ID");
271
- context.artifactRowById(cell.outputArtifactId);
272
- }
627
+ const slot = `${cell.slot.row}:${cell.slot.column}`;
628
+ if (occupiedSlots.has(slot))
629
+ throw new Error(`Duplicate cell slot: ${slot}`);
630
+ occupiedSlots.add(slot);
631
+ validateCellFields(context, notebook, cell);
273
632
  }
274
633
  const edgeIds = new Set();
634
+ const occupiedInputs = new Set();
275
635
  for (const edge of notebook.edges) {
276
636
  assertUuidV7(edge.id, "Edge ID");
277
637
  if (edgeIds.has(edge.id))
@@ -280,29 +640,40 @@ function validateNotebook(context, notebook) {
280
640
  if (!cellIds.has(edge.source) || !cellIds.has(edge.target)) {
281
641
  throw new Error(`Edge ${edge.id} must reference cells in the notebook`);
282
642
  }
283
- requiredText(edge.targetInput, "Edge targetInput");
643
+ const targetInput = requiredText(edge.targetInput, "Edge targetInput");
644
+ const inputKey = `${edge.target}:${targetInput}`;
645
+ if (occupiedInputs.has(inputKey)) {
646
+ throw new Error(`Duplicate target input: ${edge.target} ${targetInput}`);
647
+ }
648
+ occupiedInputs.add(inputKey);
649
+ }
650
+ }
651
+ function validateCellFields(context, notebook, cell) {
652
+ assertUuidV7(cell.id, "Cell ID");
653
+ if (!NOTEBOOK_CELL_TYPE_SET.has(cell.type)) {
654
+ throw new Error(`Invalid cell type: ${cell.type}`);
655
+ }
656
+ if (!isNotebookGridSlot(cell.slot)) {
657
+ throw new Error(`Cell slot must be within @a1-@z13: ${cell.id}`);
284
658
  }
659
+ if (cell.outputEntityId) {
660
+ assertUuidV7(cell.outputEntityId, "Cell output entity ID");
661
+ requiredEntity(context, cell.outputEntityId);
662
+ }
663
+ if (cell.outputArtifactId) {
664
+ assertUuidV7(cell.outputArtifactId, "Cell output artifact ID");
665
+ context.artifactRowById(cell.outputArtifactId);
666
+ }
667
+ validateCellReferences(context, notebook, cell);
668
+ validatePinnedResults(context, cell);
285
669
  }
286
670
  function synchronizeNotebookChildren(context, notebook) {
287
671
  deleteMissing(context, "edges", "edge_id", notebook.id, notebook.edges.map((edge) => edge.id));
288
672
  deleteMissing(context, "cells", "cell_id", notebook.id, notebook.cells.map((cell) => cell.id));
289
- const upsertCell = context.store.db.prepare(`INSERT INTO cells(
290
- notebook_id, cell_id, type, title, position_x, position_y,
291
- entity_id, prompt, model, inputs_json, output_artifact_id
292
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
293
- ON CONFLICT(notebook_id, cell_id) DO UPDATE SET
294
- type=excluded.type,
295
- title=excluded.title,
296
- position_x=excluded.position_x,
297
- position_y=excluded.position_y,
298
- entity_id=excluded.entity_id,
299
- prompt=excluded.prompt,
300
- model=excluded.model,
301
- inputs_json=excluded.inputs_json,
302
- output_artifact_id=excluded.output_artifact_id`);
303
673
  for (const cell of notebook.cells) {
304
- upsertCell.run(notebook.id, cell.id, cell.type, requiredText(cell.title, "Cell title"), cell.position.x, cell.position.y, cell.entityId ?? null, cell.prompt ?? null, cell.model ?? null, canonicalJson(cell.inputs ?? {}), cell.outputArtifactId ?? null);
674
+ upsertNotebookCell(context, notebook.id, cell);
305
675
  }
676
+ repairCellSlots(context, notebook.id);
306
677
  const upsertEdge = context.store.db.prepare(`INSERT INTO edges(
307
678
  notebook_id, edge_id, source_cell_id, target_cell_id, target_input
308
679
  ) VALUES (?, ?, ?, ?, ?)
@@ -314,6 +685,132 @@ function synchronizeNotebookChildren(context, notebook) {
314
685
  upsertEdge.run(notebook.id, edge.id, edge.source, edge.target, edge.targetInput);
315
686
  }
316
687
  }
688
+ function synchronizeNotebookState(context, notebook) {
689
+ synchronizeNotebookFields(context, notebook);
690
+ synchronizeCellExecutions(context, notebook);
691
+ synchronizeGenerationPlans(context, notebook);
692
+ synchronizeRunPlans(context, notebook);
693
+ }
694
+ function synchronizeNotebookFields(context, notebook) {
695
+ const values = new Map();
696
+ if (notebook.description !== undefined) {
697
+ values.set("description", notebook.description);
698
+ }
699
+ if (notebook.lifecycleState !== undefined) {
700
+ values.set("lifecycle_state", notebook.lifecycleState);
701
+ }
702
+ if (notebook.workflowVersion !== undefined) {
703
+ if (!Number.isSafeInteger(notebook.workflowVersion)) {
704
+ throw new Error("Notebook workflow version must be an integer");
705
+ }
706
+ values.set("workflow_version", notebook.workflowVersion);
707
+ }
708
+ if (notebook.analysisRevision !== undefined) {
709
+ values.set("analysis_revision", notebook.analysisRevision);
710
+ }
711
+ if (notebook.fixture !== undefined) {
712
+ values.set("fixture", notebook.fixture);
713
+ }
714
+ deleteMissingNotebookRows(context, "notebook_fields", "field", notebook.id, [
715
+ ...values.keys(),
716
+ ]);
717
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_fields(notebook_id, field, value_json)
718
+ VALUES (?, ?, ?)
719
+ ON CONFLICT(notebook_id, field) DO UPDATE SET
720
+ value_json=excluded.value_json`);
721
+ for (const [field, value] of values) {
722
+ upsert.run(notebook.id, field, canonicalJson(value));
723
+ }
724
+ }
725
+ function synchronizeCellExecutions(context, notebook) {
726
+ const entries = Object.entries(notebook.execution ?? {});
727
+ const cellIds = new Set(notebook.cells.map((cell) => cell.id));
728
+ for (const [cellId] of entries) {
729
+ if (!cellIds.has(cellId)) {
730
+ throw new Error(`Notebook execution references missing cell: ${cellId}`);
731
+ }
732
+ }
733
+ deleteMissingNotebookRows(context, "notebook_cell_executions", "cell_id", notebook.id, entries.map(([cellId]) => cellId));
734
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_cell_executions(
735
+ notebook_id, cell_id, fingerprint, status, output_artifact_id,
736
+ provider_artifact_id, run_id, completed_at, started_at, updated_at,
737
+ tool, error, stale, fixture_baseline
738
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
739
+ ON CONFLICT(notebook_id, cell_id) DO UPDATE SET
740
+ fingerprint=excluded.fingerprint,
741
+ status=excluded.status,
742
+ output_artifact_id=excluded.output_artifact_id,
743
+ provider_artifact_id=excluded.provider_artifact_id,
744
+ run_id=excluded.run_id,
745
+ completed_at=excluded.completed_at,
746
+ started_at=excluded.started_at,
747
+ updated_at=excluded.updated_at,
748
+ tool=excluded.tool,
749
+ error=excluded.error,
750
+ stale=excluded.stale,
751
+ fixture_baseline=excluded.fixture_baseline`);
752
+ for (const [cellId, execution] of entries) {
753
+ upsert.run(notebook.id, cellId, execution.fingerprint ?? null, execution.status ?? null, execution.outputArtifactId ?? null, execution.providerArtifactId ?? null, execution.runId ?? null, execution.completedAt ?? null, execution.startedAt ?? null, execution.updatedAt ?? null, execution.tool ?? null, execution.error ?? null, execution.stale === true ? 1 : 0, execution.fixtureBaseline === true ? 1 : 0);
754
+ }
755
+ }
756
+ function synchronizeGenerationPlans(context, notebook) {
757
+ const plans = notebook.generationPlans ?? [];
758
+ assertUnique(plans.map((plan) => plan.planId), "generation plan ID");
759
+ deleteMissingNotebookRows(context, "notebook_generation_plans", "plan_id", notebook.id, plans.map((plan) => plan.planId));
760
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_generation_plans(
761
+ notebook_id, plan_id, cell_id, status, plan_json,
762
+ output_artifact_id, error, created_at, updated_at
763
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
764
+ ON CONFLICT(notebook_id, plan_id) DO UPDATE SET
765
+ cell_id=excluded.cell_id,
766
+ status=excluded.status,
767
+ plan_json=excluded.plan_json,
768
+ output_artifact_id=excluded.output_artifact_id,
769
+ error=excluded.error,
770
+ created_at=excluded.created_at,
771
+ updated_at=excluded.updated_at`);
772
+ for (const plan of plans) {
773
+ upsert.run(notebook.id, requiredText(plan.planId, "Generation plan ID"), requiredText(plan.cellId, "Generation plan cell ID"), requiredText(plan.status, "Generation plan status"), canonicalJson(plan.plan), plan.outputArtifactId ?? null, plan.error ?? null, requiredText(plan.createdAt, "Generation plan created time"), requiredText(plan.updatedAt, "Generation plan updated time"));
774
+ }
775
+ }
776
+ function synchronizeRunPlans(context, notebook) {
777
+ const plans = notebook.notebookRunPlans ?? [];
778
+ assertUnique(plans.map((plan) => plan.planId), "notebook run plan ID");
779
+ deleteMissingNotebookRows(context, "notebook_run_plans", "plan_id", notebook.id, plans.map((plan) => plan.planId));
780
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_run_plans(
781
+ notebook_id, plan_id, status, plan_json, paid_cell_ids_json,
782
+ cell_fingerprints_json, known_cost_usd, unknown_cost_count,
783
+ created_at, updated_at, run_id, outputs_json, error
784
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
785
+ ON CONFLICT(notebook_id, plan_id) DO UPDATE SET
786
+ status=excluded.status,
787
+ plan_json=excluded.plan_json,
788
+ paid_cell_ids_json=excluded.paid_cell_ids_json,
789
+ cell_fingerprints_json=excluded.cell_fingerprints_json,
790
+ known_cost_usd=excluded.known_cost_usd,
791
+ unknown_cost_count=excluded.unknown_cost_count,
792
+ created_at=excluded.created_at,
793
+ updated_at=excluded.updated_at,
794
+ run_id=excluded.run_id,
795
+ outputs_json=excluded.outputs_json,
796
+ error=excluded.error`);
797
+ for (const plan of plans) {
798
+ upsert.run(notebook.id, requiredText(plan.planId, "Notebook run plan ID"), requiredText(plan.status, "Notebook run plan status"), canonicalJson(plan.plan), canonicalJson(plan.paidCellIds), canonicalJson(plan.cellDefinitionFingerprints), nonNegativeFinite(plan.knownCostUsd, "Notebook run plan known cost"), nonNegativeInteger(plan.unknownCostCount, "Notebook run plan unknown cost count"), requiredText(plan.createdAt, "Notebook run plan created time"), requiredText(plan.updatedAt, "Notebook run plan updated time"), plan.runId ?? null, plan.outputs === undefined ? null : canonicalJson(plan.outputs), plan.error ?? null);
799
+ }
800
+ }
801
+ function deleteMissingNotebookRows(context, table, idColumn, notebookId, ids) {
802
+ if (ids.length === 0) {
803
+ context.store.db
804
+ .prepare(`DELETE FROM ${table} WHERE notebook_id=?`)
805
+ .run(notebookId);
806
+ return;
807
+ }
808
+ const placeholders = ids.map(() => "?").join(", ");
809
+ context.store.db
810
+ .prepare(`DELETE FROM ${table}
811
+ WHERE notebook_id=? AND ${idColumn} NOT IN (${placeholders})`)
812
+ .run(notebookId, ...ids);
813
+ }
317
814
  function deleteMissing(context, table, idColumn, notebookId, ids) {
318
815
  if (ids.length === 0) {
319
816
  context.store.db
@@ -327,21 +824,231 @@ function deleteMissing(context, table, idColumn, notebookId, ids) {
327
824
  WHERE notebook_id=? AND ${idColumn} NOT IN (${placeholders})`)
328
825
  .run(notebookId, ...ids);
329
826
  }
330
- function rowToCell(row) {
827
+ function validateCellReferences(context, notebook, cell) {
828
+ const ids = new Set();
829
+ const ordinals = new Set();
830
+ for (const reference of cell.references ?? []) {
831
+ assertUuidV7(reference.id, "Cell reference ID");
832
+ if (ids.has(reference.id)) {
833
+ throw new Error(`Duplicate cell reference ID: ${reference.id}`);
834
+ }
835
+ ids.add(reference.id);
836
+ validateOrdinal(reference.ordinal, "Cell reference ordinal");
837
+ if (ordinals.has(reference.ordinal)) {
838
+ throw new Error(`Duplicate cell reference ordinal: ${reference.ordinal}`);
839
+ }
840
+ ordinals.add(reference.ordinal);
841
+ requiredText(reference.targetId, "Cell reference target ID");
842
+ assertReferenceTarget(context, notebook, reference);
843
+ }
844
+ }
845
+ function assertReferenceTarget(context, notebook, reference) {
846
+ if (reference.kind === "artifact") {
847
+ context.artifactRowById(reference.targetId);
848
+ return;
849
+ }
850
+ if (reference.kind === "cell-output") {
851
+ if (!notebook.cells.some((cell) => cell.id === reference.targetId)) {
852
+ throw new Error(`Cell output target not found: ${reference.targetId}`);
853
+ }
854
+ return;
855
+ }
856
+ const table = reference.kind === "transcript"
857
+ ? "transcripts"
858
+ : reference.kind === "sequence"
859
+ ? "sequences"
860
+ : "artifact_streams";
861
+ const column = reference.kind === "transcript"
862
+ ? "transcript_id"
863
+ : reference.kind === "sequence"
864
+ ? "sequence_id"
865
+ : "stream_id";
866
+ const found = context.store.db
867
+ .prepare(`SELECT 1 AS present FROM ${table} WHERE ${column}=?`)
868
+ .get(reference.targetId);
869
+ if (!found) {
870
+ throw new Error(`${reference.kind} target not found: ${reference.targetId}`);
871
+ }
872
+ }
873
+ function validatePinnedResults(context, cell) {
874
+ const ids = new Set();
875
+ const ordinals = new Set();
876
+ for (const result of cell.pinnedResults ?? []) {
877
+ assertUuidV7(result.id, "Pinned search result ID");
878
+ if (ids.has(result.id)) {
879
+ throw new Error(`Duplicate pinned search result ID: ${result.id}`);
880
+ }
881
+ ids.add(result.id);
882
+ validateOrdinal(result.ordinal, "Pinned search result ordinal");
883
+ if (ordinals.has(result.ordinal)) {
884
+ throw new Error(`Duplicate pinned search result ordinal: ${result.ordinal}`);
885
+ }
886
+ ordinals.add(result.ordinal);
887
+ context.artifactRowById(result.artifactId);
888
+ requiredText(result.objectHash, "Pinned search result object hash");
889
+ normalizeSearchLocation(result.location);
890
+ requiredText(result.selectedRevision, "Pinned search result revision");
891
+ if (!Number.isSafeInteger(result.createdAt) || result.createdAt < 0) {
892
+ throw new Error("Pinned search result createdAt must be a positive integer");
893
+ }
894
+ const object = context.store.db
895
+ .prepare("SELECT 1 AS present FROM objects WHERE object_hash=?")
896
+ .get(result.objectHash);
897
+ if (!object) {
898
+ throw new Error(`Pinned search result object not found: ${result.objectHash}`);
899
+ }
900
+ }
901
+ }
902
+ function validateOrdinal(value, label) {
903
+ if (!Number.isSafeInteger(value) || value < 0) {
904
+ throw new Error(`${label} must be a non-negative safe integer`);
905
+ }
906
+ }
907
+ function synchronizeCellReferences(context, notebookId, cell) {
908
+ const references = cell.references ?? [];
909
+ deleteMissingChildren(context, "cell_references", "reference_id", notebookId, cell.id, references.map((reference) => reference.id));
910
+ const upsert = context.store.db.prepare(`INSERT INTO cell_references(
911
+ notebook_id, cell_id, reference_id, kind,
912
+ target_id, snapshot_json, ordinal
913
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
914
+ ON CONFLICT(notebook_id, cell_id, reference_id) DO UPDATE SET
915
+ kind=excluded.kind,
916
+ target_id=excluded.target_id,
917
+ snapshot_json=excluded.snapshot_json,
918
+ ordinal=excluded.ordinal`);
919
+ for (const reference of references) {
920
+ upsert.run(notebookId, cell.id, reference.id, reference.kind, reference.targetId, canonicalJson(reference.snapshot), reference.ordinal);
921
+ }
922
+ }
923
+ function synchronizePinnedResults(context, notebookId, cell) {
924
+ const pinnedResults = cell.pinnedResults ?? [];
925
+ deleteMissingChildren(context, "pinned_search_results", "result_id", notebookId, cell.id, pinnedResults.map((result) => result.id));
926
+ const upsert = context.store.db.prepare(`INSERT INTO pinned_search_results(
927
+ notebook_id, cell_id, result_id, artifact_id, object_hash,
928
+ location_json, representative_json, query_json, signals_json,
929
+ selected_revision, ordinal, created_at
930
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
931
+ ON CONFLICT(notebook_id, cell_id, result_id) DO UPDATE SET
932
+ artifact_id=excluded.artifact_id,
933
+ object_hash=excluded.object_hash,
934
+ location_json=excluded.location_json,
935
+ representative_json=excluded.representative_json,
936
+ query_json=excluded.query_json,
937
+ signals_json=excluded.signals_json,
938
+ selected_revision=excluded.selected_revision,
939
+ ordinal=excluded.ordinal,
940
+ created_at=excluded.created_at`);
941
+ for (const result of pinnedResults) {
942
+ upsert.run(notebookId, cell.id, result.id, result.artifactId, result.objectHash, canonicalJson(normalizeSearchLocation(result.location)), result.representativeTick === undefined
943
+ ? null
944
+ : canonicalJson(result.representativeTick), canonicalJson(result.query), canonicalJson(result.signals), result.selectedRevision, result.ordinal, result.createdAt);
945
+ }
946
+ }
947
+ function deleteMissingChildren(context, table, idColumn, notebookId, cellId, ids) {
948
+ if (ids.length === 0) {
949
+ context.store.db
950
+ .prepare(`DELETE FROM ${table} WHERE notebook_id=? AND cell_id=?`)
951
+ .run(notebookId, cellId);
952
+ return;
953
+ }
954
+ const placeholders = ids.map(() => "?").join(", ");
955
+ context.store.db
956
+ .prepare(`DELETE FROM ${table}
957
+ WHERE notebook_id=? AND cell_id=? AND ${idColumn} NOT IN (${placeholders})`)
958
+ .run(notebookId, cellId, ...ids);
959
+ }
960
+ function rowToCellReference(row) {
961
+ return {
962
+ id: row.reference_id,
963
+ kind: row.kind,
964
+ targetId: row.target_id,
965
+ snapshot: parseJson(row.snapshot_json, {}),
966
+ ordinal: row.ordinal,
967
+ };
968
+ }
969
+ function rowToPinnedSearchResult(row) {
970
+ const representativeTick = row.representative_json === null
971
+ ? undefined
972
+ : parseJson(row.representative_json, undefined);
973
+ return {
974
+ id: row.result_id,
975
+ artifactId: row.artifact_id,
976
+ objectHash: row.object_hash,
977
+ location: normalizeSearchLocation(parseJson(row.location_json, {
978
+ kind: "still",
979
+ artifactId: row.artifact_id,
980
+ sourcePath: "unknown",
981
+ objectHash: row.object_hash,
982
+ })),
983
+ ...(representativeTick === undefined ? {} : { representativeTick }),
984
+ query: parseJson(row.query_json, {}),
985
+ signals: parseJson(row.signals_json, []),
986
+ selectedRevision: row.selected_revision,
987
+ ordinal: row.ordinal,
988
+ createdAt: row.created_at,
989
+ };
990
+ }
991
+ function rowToCell(row, references, pinnedResults) {
992
+ const inputs = parseJson(row.inputs_json, {});
993
+ const legacyProvider = optionalString(inputs.provider);
994
+ const legacyOperation = optionalString(inputs.operation);
995
+ const provider = optionalString(row.provider) ?? legacyProvider;
996
+ const operation = optionalString(row.operation) ?? legacyOperation;
997
+ const model = optionalString(row.model);
998
+ const tool = optionalString(row.tool) ??
999
+ (looksLikeGenerationTool(model) ? model : undefined);
331
1000
  return {
332
1001
  id: row.cell_id,
333
- type: row.type,
334
- title: row.title,
335
- position: { x: row.position_x, y: row.position_y },
336
- ...(row.entity_id ? { entityId: row.entity_id } : {}),
1002
+ type: notebookCellType(row.type),
1003
+ ...(row.label ? { label: row.label } : {}),
1004
+ slot: { row: row.grid_row, column: row.grid_column },
1005
+ ...(row.output_entity_id ? { outputEntityId: row.output_entity_id } : {}),
337
1006
  ...(row.prompt ? { prompt: row.prompt } : {}),
338
- ...(row.model ? { model: row.model } : {}),
339
- inputs: parseJson(row.inputs_json, {}),
1007
+ ...(provider ? { provider } : {}),
1008
+ ...(model ? { model } : {}),
1009
+ ...(operation ? { operation } : {}),
1010
+ ...(tool ? { tool } : {}),
1011
+ inputs,
340
1012
  ...(row.output_artifact_id
341
1013
  ? { outputArtifactId: row.output_artifact_id }
342
1014
  : {}),
1015
+ references: references.map(rowToCellReference),
1016
+ pinnedResults: pinnedResults.map(rowToPinnedSearchResult),
343
1017
  };
344
1018
  }
1019
+ function notebookCellType(type) {
1020
+ const resolved = LEGACY_NOTEBOOK_CELL_TYPE_ALIASES[type] ?? type;
1021
+ if (!NOTEBOOK_CELL_TYPE_SET.has(resolved)) {
1022
+ throw new Error(`Invalid cell type: ${type}`);
1023
+ }
1024
+ return resolved;
1025
+ }
1026
+ function normalizeCellForWrite(cell) {
1027
+ const inputs = { ...(cell.inputs ?? {}) };
1028
+ const provider = optionalString(cell.provider) ?? optionalString(inputs.provider);
1029
+ const operation = optionalString(cell.operation) ?? optionalString(inputs.operation);
1030
+ const model = optionalString(cell.model);
1031
+ const tool = optionalString(cell.tool) ??
1032
+ (looksLikeGenerationTool(model) ? model : undefined);
1033
+ return {
1034
+ ...cell,
1035
+ ...(provider ? { provider } : { provider: undefined }),
1036
+ ...(operation ? { operation } : { operation: undefined }),
1037
+ ...(tool ? { tool } : { tool: undefined }),
1038
+ inputs,
1039
+ };
1040
+ }
1041
+ function optionalString(value) {
1042
+ if (typeof value !== "string")
1043
+ return undefined;
1044
+ const trimmed = value.trim();
1045
+ return trimmed ? trimmed : undefined;
1046
+ }
1047
+ function looksLikeGenerationTool(value) {
1048
+ if (!value)
1049
+ return false;
1050
+ return value.startsWith("generate_") || value.includes("/");
1051
+ }
345
1052
  function rowToEdge(row) {
346
1053
  return {
347
1054
  id: row.edge_id,
@@ -383,6 +1090,81 @@ function validateRunStatus(status) {
383
1090
  throw new Error(`Invalid notebook run status: ${status}`);
384
1091
  }
385
1092
  }
1093
+ function executionFromRow(row) {
1094
+ return {
1095
+ ...(row.fingerprint ? { fingerprint: row.fingerprint } : {}),
1096
+ ...(row.status ? { status: row.status } : {}),
1097
+ ...(row.output_artifact_id
1098
+ ? { outputArtifactId: row.output_artifact_id }
1099
+ : {}),
1100
+ ...(row.provider_artifact_id
1101
+ ? { providerArtifactId: row.provider_artifact_id }
1102
+ : {}),
1103
+ ...(row.run_id ? { runId: row.run_id } : {}),
1104
+ ...(row.completed_at ? { completedAt: row.completed_at } : {}),
1105
+ ...(row.started_at ? { startedAt: row.started_at } : {}),
1106
+ ...(row.updated_at ? { updatedAt: row.updated_at } : {}),
1107
+ ...(row.tool ? { tool: row.tool } : {}),
1108
+ ...(row.error ? { error: row.error } : {}),
1109
+ ...(row.stale === 1 ? { stale: true } : {}),
1110
+ ...(row.fixture_baseline === 1 ? { fixtureBaseline: true } : {}),
1111
+ };
1112
+ }
1113
+ function generationPlanFromRow(row) {
1114
+ return {
1115
+ planId: row.plan_id,
1116
+ cellId: row.cell_id,
1117
+ status: row.status,
1118
+ plan: parseJson(row.plan_json, {}),
1119
+ ...(row.output_artifact_id
1120
+ ? { outputArtifactId: row.output_artifact_id }
1121
+ : {}),
1122
+ ...(row.error ? { error: row.error } : {}),
1123
+ createdAt: row.created_at,
1124
+ updatedAt: row.updated_at,
1125
+ };
1126
+ }
1127
+ function runPlanFromRow(row) {
1128
+ return {
1129
+ planId: row.plan_id,
1130
+ status: row.status,
1131
+ plan: parseJson(row.plan_json, {}),
1132
+ paidCellIds: parseJson(row.paid_cell_ids_json, []),
1133
+ cellDefinitionFingerprints: parseJson(row.cell_fingerprints_json, {}),
1134
+ knownCostUsd: row.known_cost_usd,
1135
+ unknownCostCount: row.unknown_cost_count,
1136
+ createdAt: row.created_at,
1137
+ updatedAt: row.updated_at,
1138
+ ...(row.run_id ? { runId: row.run_id } : {}),
1139
+ ...(row.outputs_json
1140
+ ? {
1141
+ outputs: parseJson(row.outputs_json, {}),
1142
+ }
1143
+ : {}),
1144
+ ...(row.error ? { error: row.error } : {}),
1145
+ };
1146
+ }
1147
+ function assertUnique(values, label) {
1148
+ const unique = new Set(values);
1149
+ if (unique.size !== values.length) {
1150
+ throw new Error(`Duplicate ${label}`);
1151
+ }
1152
+ }
1153
+ function nonNegativeFinite(value, label) {
1154
+ if (!Number.isFinite(value) || value < 0) {
1155
+ throw new Error(`${label} must be a non-negative finite number`);
1156
+ }
1157
+ return value;
1158
+ }
1159
+ function nonNegativeInteger(value, label) {
1160
+ if (!Number.isSafeInteger(value) || value < 0) {
1161
+ throw new Error(`${label} must be a non-negative integer`);
1162
+ }
1163
+ return value;
1164
+ }
1165
+ function isRecord(value) {
1166
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1167
+ }
386
1168
  function callerOrNewId(id, label) {
387
1169
  const resolved = id ?? newUuidV7();
388
1170
  assertUuidV7(resolved, label);
@@ -393,7 +1175,7 @@ const ENTITY_SELECT = `
393
1175
  FROM entities
394
1176
  `;
395
1177
  const NOTEBOOK_SELECT = `
396
- SELECT notebook_id, name, properties_json, created_at
1178
+ SELECT notebook_id, name, created_at
397
1179
  FROM notebooks
398
1180
  `;
399
1181
  //# sourceMappingURL=domain.js.map