videobook-engine 2.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +87 -41
  2. package/dist/artifact-names.d.ts +6 -0
  3. package/dist/artifact-names.d.ts.map +1 -0
  4. package/dist/artifact-names.js +56 -0
  5. package/dist/artifact-names.js.map +1 -0
  6. package/dist/artifacts.d.ts.map +1 -1
  7. package/dist/artifacts.js +57 -23
  8. package/dist/artifacts.js.map +1 -1
  9. package/dist/books.js +2 -1
  10. package/dist/books.js.map +1 -1
  11. package/dist/cas.d.ts +27 -1
  12. package/dist/cas.d.ts.map +1 -1
  13. package/dist/cas.js +78 -6
  14. package/dist/cas.js.map +1 -1
  15. package/dist/catalog-metadata.d.ts +56 -0
  16. package/dist/catalog-metadata.d.ts.map +1 -0
  17. package/dist/catalog-metadata.js +42 -0
  18. package/dist/catalog-metadata.js.map +1 -0
  19. package/dist/communications.js +4 -2
  20. package/dist/communications.js.map +1 -1
  21. package/dist/context.d.ts +6 -1
  22. package/dist/context.d.ts.map +1 -1
  23. package/dist/context.js +35 -6
  24. package/dist/context.js.map +1 -1
  25. package/dist/domain.d.ts +4 -0
  26. package/dist/domain.d.ts.map +1 -1
  27. package/dist/domain.js +870 -58
  28. package/dist/domain.js.map +1 -1
  29. package/dist/edit-policy.d.ts +3 -0
  30. package/dist/edit-policy.d.ts.map +1 -0
  31. package/dist/edit-policy.js +6 -0
  32. package/dist/edit-policy.js.map +1 -0
  33. package/dist/edits.d.ts +10 -0
  34. package/dist/edits.d.ts.map +1 -0
  35. package/dist/edits.js +1392 -0
  36. package/dist/edits.js.map +1 -0
  37. package/dist/engine-types.d.ts +110 -60
  38. package/dist/engine-types.d.ts.map +1 -1
  39. package/dist/engine-types.js +33 -0
  40. package/dist/engine-types.js.map +1 -1
  41. package/dist/engine.d.ts +69 -8
  42. package/dist/engine.d.ts.map +1 -1
  43. package/dist/engine.js +24 -18
  44. package/dist/engine.js.map +1 -1
  45. package/dist/files.d.ts.map +1 -1
  46. package/dist/files.js +31 -27
  47. package/dist/files.js.map +1 -1
  48. package/dist/fork.d.ts +147 -0
  49. package/dist/fork.d.ts.map +1 -0
  50. package/dist/fork.js +609 -0
  51. package/dist/fork.js.map +1 -0
  52. package/dist/history.d.ts +0 -4
  53. package/dist/history.d.ts.map +1 -1
  54. package/dist/history.js +166 -595
  55. package/dist/history.js.map +1 -1
  56. package/dist/index.d.ts +16 -4
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +12 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/job-queue.d.ts +2 -0
  61. package/dist/job-queue.d.ts.map +1 -1
  62. package/dist/job-queue.js +29 -2
  63. package/dist/job-queue.js.map +1 -1
  64. package/dist/media.d.ts.map +1 -1
  65. package/dist/media.js +1 -0
  66. package/dist/media.js.map +1 -1
  67. package/dist/merge-policy.d.ts +99 -0
  68. package/dist/merge-policy.d.ts.map +1 -0
  69. package/dist/merge-policy.js +310 -0
  70. package/dist/merge-policy.js.map +1 -0
  71. package/dist/metadata.d.ts.map +1 -1
  72. package/dist/metadata.js +15 -20
  73. package/dist/metadata.js.map +1 -1
  74. package/dist/migration.d.ts +9 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +534 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/mvp-contract-fixtures.d.ts +244 -0
  79. package/dist/mvp-contract-fixtures.d.ts.map +1 -0
  80. package/dist/mvp-contract-fixtures.js +556 -0
  81. package/dist/mvp-contract-fixtures.js.map +1 -0
  82. package/dist/mvp-contracts.d.ts +761 -0
  83. package/dist/mvp-contracts.d.ts.map +1 -0
  84. package/dist/mvp-contracts.js +59 -0
  85. package/dist/mvp-contracts.js.map +1 -0
  86. package/dist/mvp-time.d.ts +54 -0
  87. package/dist/mvp-time.d.ts.map +1 -0
  88. package/dist/mvp-time.js +148 -0
  89. package/dist/mvp-time.js.map +1 -0
  90. package/dist/notebook/types.d.ts +113 -8
  91. package/dist/notebook/types.d.ts.map +1 -1
  92. package/dist/order-keys.d.ts +22 -0
  93. package/dist/order-keys.d.ts.map +1 -0
  94. package/dist/order-keys.js +262 -0
  95. package/dist/order-keys.js.map +1 -0
  96. package/dist/runtime-services.js +4 -2
  97. package/dist/runtime-services.js.map +1 -1
  98. package/dist/schema.d.ts +24 -5
  99. package/dist/schema.d.ts.map +1 -1
  100. package/dist/schema.js +644 -132
  101. package/dist/schema.js.map +1 -1
  102. package/dist/search-benchmark.d.ts +4 -0
  103. package/dist/search-benchmark.d.ts.map +1 -0
  104. package/dist/search-benchmark.js +144 -0
  105. package/dist/search-benchmark.js.map +1 -0
  106. package/dist/sequences.d.ts +20 -0
  107. package/dist/sequences.d.ts.map +1 -0
  108. package/dist/sequences.js +825 -0
  109. package/dist/sequences.js.map +1 -0
  110. package/dist/similarity.js +1 -1
  111. package/dist/storage.d.ts +3 -1
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +388 -7
  114. package/dist/storage.js.map +1 -1
  115. package/dist/store.d.ts +63 -2
  116. package/dist/store.d.ts.map +1 -1
  117. package/dist/store.js +351 -65
  118. package/dist/store.js.map +1 -1
  119. package/dist/streams.d.ts +10 -0
  120. package/dist/streams.d.ts.map +1 -0
  121. package/dist/streams.js +183 -0
  122. package/dist/streams.js.map +1 -0
  123. package/dist/temporal-models.d.ts +48 -0
  124. package/dist/temporal-models.d.ts.map +1 -0
  125. package/dist/temporal-models.js +278 -0
  126. package/dist/temporal-models.js.map +1 -0
  127. package/dist/temporal-search.d.ts +29 -0
  128. package/dist/temporal-search.d.ts.map +1 -0
  129. package/dist/temporal-search.js +1507 -0
  130. package/dist/temporal-search.js.map +1 -0
  131. package/dist/transcripts.d.ts +16 -0
  132. package/dist/transcripts.d.ts.map +1 -0
  133. package/dist/transcripts.js +519 -0
  134. package/dist/transcripts.js.map +1 -0
  135. package/docs/engine-layout.md +346 -106
  136. package/docs/mvp-prd.md +918 -0
  137. package/fixtures/media/vancat.mp4 +0 -0
  138. package/fixtures/media/vancat_profile.jpg +0 -0
  139. package/fixtures/v5/contract-fixtures.json +1532 -0
  140. package/package.json +30 -5
  141. package/dist/history-types.d.ts +0 -74
  142. package/dist/history-types.d.ts.map +0 -1
  143. package/dist/history-types.js +0 -2
  144. package/dist/history-types.js.map +0 -1
  145. package/dist/timeline.d.ts +0 -9
  146. package/dist/timeline.d.ts.map +0 -1
  147. package/dist/timeline.js +0 -236
  148. package/dist/timeline.js.map +0 -1
package/dist/domain.js CHANGED
@@ -1,8 +1,11 @@
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";
5
+ import { isValidNotebookCellSlug, NOTEBOOK_CELL_TYPES } from "./schema.js";
4
6
  import { canonicalJson, parseJson } from "./store.js";
5
7
  import { EngineFault } from "./store.js";
8
+ const NOTEBOOK_CELL_TYPE_SET = new Set(NOTEBOOK_CELL_TYPES);
6
9
  export function createEntitiesApi(context) {
7
10
  return {
8
11
  create: (type, name, input = {}) => createEntity(context, type, name, input),
@@ -26,6 +29,10 @@ export function createNotebooksApi(context) {
26
29
  list: () => listNotebooks(context),
27
30
  read: (notebookId) => syncResultOf(() => requiredNotebook(context, notebookId)),
28
31
  write: (notebook) => writeNotebook(context, notebook),
32
+ insertCell: (notebookId, cell) => insertNotebookCell(context, notebookId, cell),
33
+ updateCell: (notebookId, cell) => updateNotebookCell(context, notebookId, cell),
34
+ moveCell: (notebookId, cellId, slot) => moveNotebookCell(context, notebookId, cellId, slot),
35
+ removeCell: (notebookId, cellId) => removeNotebookCell(context, notebookId, cellId),
29
36
  delete: (notebookId) => deleteNotebook(context, notebookId),
30
37
  recordRun: (run) => recordNotebookRun(context, run),
31
38
  };
@@ -37,9 +44,10 @@ async function createEntity(context, type, name, input) {
37
44
  const entityId = newUuidV7();
38
45
  const mutation = await context.store.semantic({
39
46
  operation: "create_entity",
47
+ tables: ["entities"],
40
48
  details: { entityId, type, name: normalizedName },
41
49
  writeSet: [`entity:${entityId}`],
42
- }, ["entities"], (_operationId, now) => {
50
+ }, (_operationId, now) => {
43
51
  context.store.db
44
52
  .prepare(`INSERT INTO entities(
45
53
  entity_id, type, name, description, prompt, data_json, created_at
@@ -70,9 +78,10 @@ async function writeEntity(context, entity) {
70
78
  const name = requiredText(entity.name, "Entity name");
71
79
  const mutation = await context.store.semantic({
72
80
  operation: "write_entity",
81
+ tables: ["entities"],
73
82
  details: { entityId: entity.id, type: entity.type },
74
83
  writeSet: [`entity:${entity.id}`],
75
- }, ["entities"], () => {
84
+ }, () => {
76
85
  context.store.db
77
86
  .prepare(`UPDATE entities
78
87
  SET type=?, name=?, description=?, prompt=?, data_json=?
@@ -88,7 +97,7 @@ async function deleteEntity(context, entityId) {
88
97
  requiredEntity(context, entityId);
89
98
  const cells = context.store.db
90
99
  .prepare(`SELECT notebook_id, cell_id FROM cells
91
- WHERE entity_id=? ORDER BY notebook_id, cell_id`)
100
+ WHERE output_entity_id=? ORDER BY notebook_id, cell_id`)
92
101
  .all(entityId);
93
102
  if (cells.length > 0) {
94
103
  throw new EngineFault({
@@ -96,7 +105,7 @@ async function deleteEntity(context, entityId) {
96
105
  message: `Entity is still referenced: ${entityId}`,
97
106
  details: {
98
107
  references: cells.map((cell) => ({
99
- kind: "cell.entity",
108
+ kind: "cell.output_entity",
100
109
  id: `${cell.notebook_id}/${cell.cell_id}`,
101
110
  })),
102
111
  },
@@ -104,9 +113,10 @@ async function deleteEntity(context, entityId) {
104
113
  }
105
114
  const mutation = await context.store.semantic({
106
115
  operation: "delete_entity",
116
+ tables: ["entities"],
107
117
  details: { entityId },
108
118
  writeSet: [`entity:${entityId}`],
109
- }, ["entities"], () => {
119
+ }, () => {
110
120
  context.store.db
111
121
  .prepare("DELETE FROM entities WHERE entity_id=?")
112
122
  .run(entityId);
@@ -120,13 +130,14 @@ async function createNotebook(context, name) {
120
130
  const notebookId = newUuidV7();
121
131
  const mutation = await context.store.semantic({
122
132
  operation: "create_notebook",
133
+ tables: ["notebooks"],
123
134
  details: { notebookId, name: normalizedName },
124
135
  writeSet: [`notebook:${notebookId}`],
125
- }, ["notebooks"], (_operationId, now) => {
136
+ }, (_operationId, now) => {
126
137
  context.store.db
127
138
  .prepare(`INSERT INTO notebooks(
128
- notebook_id, name, properties_json, created_at
129
- ) VALUES (?, ?, '{}', ?)`)
139
+ notebook_id, name, created_at
140
+ ) VALUES (?, ?, ?)`)
130
141
  .run(notebookId, normalizedName, now);
131
142
  });
132
143
  return ok(requiredNotebook(context, notebookId), mutation.revision);
@@ -145,31 +156,267 @@ async function writeNotebook(context, notebook) {
145
156
  validateNotebook(context, notebook);
146
157
  const mutation = await context.store.semantic({
147
158
  operation: "write_notebook",
159
+ tables: [
160
+ "notebooks",
161
+ "notebook_fields",
162
+ "cells",
163
+ "notebook_cell_executions",
164
+ "notebook_generation_plans",
165
+ "notebook_run_plans",
166
+ "notebook_transcript_edits",
167
+ "notebook_transcript_attachments",
168
+ "edges",
169
+ "cell_references",
170
+ "pinned_search_results",
171
+ ],
148
172
  details: { notebookId: notebook.id },
149
173
  writeSet: [
150
174
  `notebook:${notebook.id}`,
151
175
  ...notebook.cells.map((cell) => `cell:${notebook.id}:${cell.id}`),
152
176
  ...notebook.edges.map((edge) => `edge:${notebook.id}:${edge.id}`),
153
177
  ],
154
- }, ["notebooks", "cells", "edges"], () => {
178
+ }, () => {
155
179
  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);
180
+ .prepare(`UPDATE notebooks SET name=? WHERE notebook_id=?`)
181
+ .run(requiredText(notebook.name, "Notebook name"), notebook.id);
159
182
  synchronizeNotebookChildren(context, notebook);
183
+ synchronizeNotebookState(context, notebook);
184
+ });
185
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
186
+ });
187
+ }
188
+ async function insertNotebookCell(context, notebookId, cell) {
189
+ return resultOf(async () => {
190
+ assertUuidV7(notebookId, "Notebook ID");
191
+ const notebook = requiredNotebook(context, notebookId);
192
+ if (notebook.cells.some((existing) => existing.id === cell.id)) {
193
+ throw new Error(`Cell already exists: ${cell.id}`);
194
+ }
195
+ const prospective = { ...notebook, cells: [...notebook.cells, cell] };
196
+ validateCellFields(context, prospective, cell);
197
+ assertCellSlugFree(context, notebookId, cell);
198
+ assertCellSlotFree(context, notebookId, cell);
199
+ const mutation = await context.store.semantic({
200
+ operation: "insert_cell",
201
+ tables: ["cells", "cell_references", "pinned_search_results"],
202
+ details: { notebookId, cellId: cell.id },
203
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cell.id}`],
204
+ }, () => {
205
+ upsertNotebookCell(context, notebookId, cell);
206
+ repairCellSlots(context, notebookId, cell.id);
207
+ });
208
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
209
+ });
210
+ }
211
+ async function updateNotebookCell(context, notebookId, cell) {
212
+ return resultOf(async () => {
213
+ assertUuidV7(notebookId, "Notebook ID");
214
+ const notebook = requiredNotebook(context, notebookId);
215
+ if (!notebook.cells.some((existing) => existing.id === cell.id)) {
216
+ throw new EngineFault({
217
+ code: "NOT_FOUND",
218
+ message: `Cell not found: ${cell.id}`,
219
+ });
220
+ }
221
+ const prospective = {
222
+ ...notebook,
223
+ cells: notebook.cells.map((existing) => existing.id === cell.id ? cell : existing),
224
+ };
225
+ validateCellFields(context, prospective, cell);
226
+ assertCellSlugFree(context, notebookId, cell);
227
+ assertCellSlotFree(context, notebookId, cell);
228
+ const mutation = await context.store.semantic({
229
+ operation: "update_cell",
230
+ tables: ["cells", "cell_references", "pinned_search_results"],
231
+ details: { notebookId, cellId: cell.id },
232
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cell.id}`],
233
+ }, () => {
234
+ upsertNotebookCell(context, notebookId, cell);
235
+ repairCellSlots(context, notebookId, cell.id);
236
+ });
237
+ return ok(revisionFor(context, mutation.revision), mutation.revision);
238
+ });
239
+ }
240
+ async function moveNotebookCell(context, notebookId, cellId, slot) {
241
+ return resultOf(async () => {
242
+ assertUuidV7(notebookId, "Notebook ID");
243
+ assertUuidV7(cellId, "Cell ID");
244
+ const notebook = requiredNotebook(context, notebookId);
245
+ const existing = notebook.cells.find((cell) => cell.id === cellId);
246
+ if (!existing) {
247
+ throw new EngineFault({
248
+ code: "NOT_FOUND",
249
+ message: `Cell not found: ${cellId}`,
250
+ });
251
+ }
252
+ return updateNotebookCell(context, notebookId, { ...existing, slot });
253
+ });
254
+ }
255
+ async function removeNotebookCell(context, notebookId, cellId) {
256
+ return resultOf(async () => {
257
+ assertUuidV7(notebookId, "Notebook ID");
258
+ assertUuidV7(cellId, "Cell ID");
259
+ requiredNotebook(context, notebookId);
260
+ const found = context.store.db
261
+ .prepare("SELECT 1 AS present FROM cells WHERE notebook_id=? AND cell_id=?")
262
+ .get(notebookId, cellId);
263
+ if (!found) {
264
+ throw new EngineFault({
265
+ code: "NOT_FOUND",
266
+ message: `Cell not found: ${cellId}`,
267
+ });
268
+ }
269
+ const mutation = await context.store.semantic({
270
+ operation: "remove_cell",
271
+ tables: [
272
+ "cells",
273
+ "notebook_cell_executions",
274
+ "notebook_generation_plans",
275
+ "edges",
276
+ "cell_references",
277
+ "pinned_search_results",
278
+ ],
279
+ details: { notebookId, cellId },
280
+ writeSet: [`notebook:${notebookId}`, `cell:${notebookId}:${cellId}`],
281
+ }, () => {
282
+ context.store.db
283
+ .prepare("DELETE FROM cells WHERE notebook_id=? AND cell_id=?")
284
+ .run(notebookId, cellId);
285
+ repairCellSlots(context, notebookId);
160
286
  });
161
287
  return ok(revisionFor(context, mutation.revision), mutation.revision);
162
288
  });
163
289
  }
290
+ function assertCellSlugFree(context, notebookId, cell) {
291
+ const found = context.store.db
292
+ .prepare(`SELECT cell_id FROM cells
293
+ WHERE notebook_id=? AND slug=? AND cell_id<>?`)
294
+ .get(notebookId, cell.slug, cell.id);
295
+ if (found) {
296
+ throw new Error(`Duplicate cell slug: ${cell.slug}`);
297
+ }
298
+ }
299
+ function assertCellSlotFree(context, notebookId, cell) {
300
+ const found = context.store.db
301
+ .prepare(`SELECT cell_id FROM cells
302
+ WHERE notebook_id=? AND grid_row=? AND grid_column=? AND cell_id<>?`)
303
+ .get(notebookId, cell.slot.row, cell.slot.column, cell.id);
304
+ if (found) {
305
+ throw new Error(`Cell slot is occupied: ${cell.slot.row}:${cell.slot.column}`);
306
+ }
307
+ }
308
+ /**
309
+ * Repairs grid-slot collisions left behind by a merge (the schema no longer
310
+ * enforces slot uniqueness so two forks can land cells on the same slot).
311
+ * Within each colliding slot the protected cell wins when present, otherwise
312
+ * the lowest cell id; losers keep their column and move to fresh rows below
313
+ * the current grid, in cell-id order.
314
+ */
315
+ function repairCellSlots(context, notebookId, protectedCellId) {
316
+ const rows = context.store.db
317
+ .prepare(`SELECT cell_id, grid_row, grid_column FROM cells
318
+ WHERE notebook_id=?
319
+ ORDER BY grid_row, grid_column, cell_id`)
320
+ .all(notebookId);
321
+ const slots = new Map();
322
+ let maxRow = -1;
323
+ for (const row of rows) {
324
+ maxRow = Math.max(maxRow, row.grid_row);
325
+ const key = `${row.grid_row}:${row.grid_column}`;
326
+ const group = slots.get(key) ?? [];
327
+ group.push({ cell_id: row.cell_id, grid_column: row.grid_column });
328
+ slots.set(key, group);
329
+ }
330
+ let nextRow = maxRow + 1;
331
+ const move = context.store.db.prepare("UPDATE cells SET grid_row=? WHERE notebook_id=? AND cell_id=?");
332
+ for (const group of slots.values()) {
333
+ if (group.length < 2)
334
+ continue;
335
+ const winner = protectedCellId && group.some((row) => row.cell_id === protectedCellId)
336
+ ? protectedCellId
337
+ : group[0].cell_id;
338
+ for (const row of group) {
339
+ if (row.cell_id === winner)
340
+ continue;
341
+ move.run(nextRow, notebookId, row.cell_id);
342
+ nextRow += 1;
343
+ }
344
+ }
345
+ }
346
+ /**
347
+ * `UNIQUE(notebook_id, slug)` is immediate and the upsert loop writes cells
348
+ * one at a time, so a document that validly swaps or rotates slugs between
349
+ * surviving cells would collide mid-loop. Every surviving cell whose slug is
350
+ * about to change is first parked on a unique temporary slug derived from
351
+ * its current one — same type prefix, so the slug/type CHECK holds — and
352
+ * the upsert pass then assigns the final slugs.
353
+ */
354
+ function evacuateChangedCellSlugs(context, notebook) {
355
+ const existing = context.store.db
356
+ .prepare("SELECT cell_id, slug FROM cells WHERE notebook_id=?")
357
+ .all(notebook.id);
358
+ if (existing.length === 0)
359
+ return;
360
+ const incomingSlugs = new Map(notebook.cells.map((cell) => [
361
+ cell.id,
362
+ normalizeCellForWrite(cell).slug ?? "",
363
+ ]));
364
+ const evacuate = context.store.db.prepare("UPDATE cells SET slug=? WHERE notebook_id=? AND cell_id=?");
365
+ for (const row of existing) {
366
+ const nextSlug = incomingSlugs.get(row.cell_id);
367
+ if (nextSlug === undefined || nextSlug === row.slug)
368
+ continue;
369
+ evacuate.run(`${row.slug}-evac-${newUuidV7().replace(/-/g, "")}`, notebook.id, row.cell_id);
370
+ }
371
+ }
372
+ function upsertNotebookCell(context, notebookId, cell) {
373
+ const normalized = normalizeCellForWrite(cell);
374
+ context.store.db
375
+ .prepare(`INSERT INTO cells(
376
+ notebook_id, cell_id, type, slug, grid_row, grid_column,
377
+ output_entity_id, prompt, provider, model, operation, tool,
378
+ inputs_json, output_artifact_id
379
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
380
+ ON CONFLICT(notebook_id, cell_id) DO UPDATE SET
381
+ type=excluded.type,
382
+ slug=excluded.slug,
383
+ grid_row=excluded.grid_row,
384
+ grid_column=excluded.grid_column,
385
+ output_entity_id=excluded.output_entity_id,
386
+ prompt=excluded.prompt,
387
+ provider=excluded.provider,
388
+ model=excluded.model,
389
+ operation=excluded.operation,
390
+ tool=excluded.tool,
391
+ inputs_json=excluded.inputs_json,
392
+ output_artifact_id=excluded.output_artifact_id`)
393
+ .run(notebookId, normalized.id, normalized.type, requiredText(normalized.slug, "Cell slug"), 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);
394
+ synchronizeCellReferences(context, notebookId, normalized);
395
+ synchronizePinnedResults(context, notebookId, normalized);
396
+ }
164
397
  async function deleteNotebook(context, notebookId) {
165
398
  return resultOf(async () => {
166
399
  assertUuidV7(notebookId, "Notebook ID");
167
400
  requiredNotebook(context, notebookId);
168
401
  const mutation = await context.store.semantic({
169
402
  operation: "delete_notebook",
403
+ tables: [
404
+ "notebooks",
405
+ "notebook_fields",
406
+ "cells",
407
+ "notebook_cell_executions",
408
+ "notebook_generation_plans",
409
+ "notebook_run_plans",
410
+ "notebook_transcript_edits",
411
+ "notebook_transcript_attachments",
412
+ "edges",
413
+ "runs",
414
+ "cell_references",
415
+ "pinned_search_results",
416
+ ],
170
417
  details: { notebookId },
171
418
  writeSet: [`notebook:${notebookId}`],
172
- }, ["notebooks", "cells", "edges", "runs"], () => {
419
+ }, () => {
173
420
  context.store.db
174
421
  .prepare("DELETE FROM notebooks WHERE notebook_id=?")
175
422
  .run(notebookId);
@@ -190,9 +437,10 @@ async function recordNotebookRun(context, run) {
190
437
  }
191
438
  const mutation = await context.store.semantic({
192
439
  operation: "record_notebook_run",
440
+ tables: ["runs"],
193
441
  details: { notebookId: run.notebookId, runId: run.id },
194
442
  writeSet: [`run:${run.id}`],
195
- }, ["runs"], () => {
443
+ }, () => {
196
444
  context.store.db
197
445
  .prepare(`INSERT INTO runs(
198
446
  run_id, notebook_id, status, started_at,
@@ -231,47 +479,131 @@ function requiredNotebook(context, notebookId) {
231
479
  return notebookFromRows(context, row);
232
480
  }
233
481
  function notebookFromRows(context, row) {
482
+ const fields = context.store.db
483
+ .prepare(`SELECT field, value_json
484
+ FROM notebook_fields WHERE notebook_id=?
485
+ ORDER BY field`)
486
+ .all(row.notebook_id);
234
487
  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`)
488
+ .prepare(`SELECT cell_id, type, slug, grid_row, grid_column, output_entity_id,
489
+ prompt, provider, model, operation, tool,
490
+ inputs_json, output_artifact_id
491
+ FROM cells WHERE notebook_id=?
492
+ ORDER BY grid_row, grid_column, cell_id`)
238
493
  .all(row.notebook_id);
239
494
  const edges = context.store.db
240
495
  .prepare(`SELECT edge_id, source_cell_id, target_cell_id, target_input
241
496
  FROM edges WHERE notebook_id=? ORDER BY edge_id`)
242
497
  .all(row.notebook_id);
498
+ const references = context.store.db
499
+ .prepare(`SELECT cell_id, reference_id, kind, target_id, snapshot_json, ordinal
500
+ FROM cell_references
501
+ WHERE notebook_id=?
502
+ ORDER BY cell_id, ordinal, reference_id`)
503
+ .all(row.notebook_id);
504
+ const pinnedResults = context.store.db
505
+ .prepare(`SELECT cell_id, result_id, artifact_id, object_hash, location_json,
506
+ representative_json, query_json, signals_json,
507
+ selected_revision, ordinal, created_at
508
+ FROM pinned_search_results
509
+ WHERE notebook_id=?
510
+ ORDER BY cell_id, ordinal, result_id`)
511
+ .all(row.notebook_id);
512
+ const executions = context.store.db
513
+ .prepare(`SELECT cell_id, fingerprint, status, output_artifact_id,
514
+ provider_artifact_id, run_id, completed_at, started_at,
515
+ updated_at, tool, error, stale, fixture_baseline
516
+ FROM notebook_cell_executions
517
+ WHERE notebook_id=? ORDER BY cell_id`)
518
+ .all(row.notebook_id);
519
+ const generationPlans = context.store.db
520
+ .prepare(`SELECT plan_id, cell_id, status, plan_json, output_artifact_id,
521
+ error, created_at, updated_at
522
+ FROM notebook_generation_plans
523
+ WHERE notebook_id=? ORDER BY created_at, plan_id`)
524
+ .all(row.notebook_id);
525
+ const runPlans = context.store.db
526
+ .prepare(`SELECT plan_id, status, plan_json, paid_cell_ids_json,
527
+ cell_fingerprints_json, known_cost_usd, unknown_cost_count,
528
+ created_at, updated_at, run_id, outputs_json, error
529
+ FROM notebook_run_plans
530
+ WHERE notebook_id=? ORDER BY created_at, plan_id`)
531
+ .all(row.notebook_id);
532
+ const transcriptEdits = context.store.db
533
+ .prepare(`SELECT action_id, kind, restored, payload_json
534
+ FROM notebook_transcript_edits
535
+ WHERE notebook_id=? ORDER BY action_id`)
536
+ .all(row.notebook_id);
537
+ const transcriptAttachments = context.store.db
538
+ .prepare(`SELECT attachment_id, payload_json
539
+ FROM notebook_transcript_attachments
540
+ WHERE notebook_id=? ORDER BY attachment_id`)
541
+ .all(row.notebook_id);
542
+ const field = new Map(fields.map((item) => [
543
+ item.field,
544
+ parseJson(item.value_json, null),
545
+ ]));
243
546
  return {
244
547
  id: row.notebook_id,
245
548
  name: row.name,
246
- properties: parseJson(row.properties_json, {}),
247
- cells: cells.map(rowToCell),
549
+ ...(typeof field.get("description") === "string"
550
+ ? { description: field.get("description") }
551
+ : {}),
552
+ ...(typeof field.get("lifecycle_state") === "string"
553
+ ? { lifecycleState: field.get("lifecycle_state") }
554
+ : {}),
555
+ ...(typeof field.get("workflow_version") === "number"
556
+ ? { workflowVersion: field.get("workflow_version") }
557
+ : {}),
558
+ ...(typeof field.get("analysis_revision") === "string"
559
+ ? { analysisRevision: field.get("analysis_revision") }
560
+ : {}),
561
+ ...(isRecord(field.get("audio_spine"))
562
+ ? {
563
+ audioSpine: field.get("audio_spine"),
564
+ }
565
+ : {}),
566
+ ...(isRecord(field.get("current_selection"))
567
+ ? {
568
+ currentSelection: field.get("current_selection"),
569
+ }
570
+ : {}),
571
+ ...(isRecord(field.get("fixture"))
572
+ ? { fixture: field.get("fixture") }
573
+ : {}),
574
+ execution: Object.fromEntries(executions.map((execution) => [
575
+ execution.cell_id,
576
+ executionFromRow(execution),
577
+ ])),
578
+ generationPlans: generationPlans.map(generationPlanFromRow),
579
+ notebookRunPlans: runPlans.map(runPlanFromRow),
580
+ transcriptEdits: transcriptEdits.map(transcriptEditFromRow),
581
+ transcriptAttachments: transcriptAttachments.map(transcriptAttachmentFromRow),
582
+ 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
583
  edges: edges.map(rowToEdge),
249
584
  createdAt: new Date(row.created_at).toISOString(),
250
585
  };
251
586
  }
252
587
  function validateNotebook(context, notebook) {
253
588
  const cellIds = new Set();
589
+ const cellSlugs = new Set();
590
+ const occupiedSlots = new Set();
254
591
  for (const cell of notebook.cells) {
255
- assertUuidV7(cell.id, "Cell ID");
256
592
  if (cellIds.has(cell.id))
257
593
  throw new Error(`Duplicate cell ID: ${cell.id}`);
258
594
  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);
595
+ if (cellSlugs.has(cell.slug)) {
596
+ throw new Error(`Duplicate cell slug: ${cell.slug}`);
272
597
  }
598
+ cellSlugs.add(cell.slug);
599
+ const slot = `${cell.slot.row}:${cell.slot.column}`;
600
+ if (occupiedSlots.has(slot))
601
+ throw new Error(`Duplicate cell slot: ${slot}`);
602
+ occupiedSlots.add(slot);
603
+ validateCellFields(context, notebook, cell);
273
604
  }
274
605
  const edgeIds = new Set();
606
+ const occupiedInputs = new Set();
275
607
  for (const edge of notebook.edges) {
276
608
  assertUuidV7(edge.id, "Edge ID");
277
609
  if (edgeIds.has(edge.id))
@@ -280,29 +612,47 @@ function validateNotebook(context, notebook) {
280
612
  if (!cellIds.has(edge.source) || !cellIds.has(edge.target)) {
281
613
  throw new Error(`Edge ${edge.id} must reference cells in the notebook`);
282
614
  }
283
- requiredText(edge.targetInput, "Edge targetInput");
615
+ const targetInput = requiredText(edge.targetInput, "Edge targetInput");
616
+ const inputKey = `${edge.target}:${targetInput}`;
617
+ if (occupiedInputs.has(inputKey)) {
618
+ throw new Error(`Duplicate target input: ${edge.target} ${targetInput}`);
619
+ }
620
+ occupiedInputs.add(inputKey);
621
+ }
622
+ }
623
+ function validateCellFields(context, notebook, cell) {
624
+ assertUuidV7(cell.id, "Cell ID");
625
+ if (!NOTEBOOK_CELL_TYPE_SET.has(cell.type)) {
626
+ throw new Error(`Invalid cell type: ${cell.type}`);
627
+ }
628
+ const slug = requiredText(cell.slug, "Cell slug");
629
+ if (slug !== cell.slug || !isValidNotebookCellSlug(cell.type, slug)) {
630
+ throw new Error(`Invalid ${cell.type} cell slug: ${cell.slug}`);
631
+ }
632
+ if (!Number.isInteger(cell.slot.row) ||
633
+ cell.slot.row < 0 ||
634
+ !Number.isInteger(cell.slot.column)) {
635
+ throw new Error(`Cell slot row must be nonnegative and column must be an integer: ${cell.id}`);
636
+ }
637
+ if (cell.outputEntityId) {
638
+ assertUuidV7(cell.outputEntityId, "Cell output entity ID");
639
+ requiredEntity(context, cell.outputEntityId);
284
640
  }
641
+ if (cell.outputArtifactId) {
642
+ assertUuidV7(cell.outputArtifactId, "Cell output artifact ID");
643
+ context.artifactRowById(cell.outputArtifactId);
644
+ }
645
+ validateCellReferences(context, notebook, cell);
646
+ validatePinnedResults(context, cell);
285
647
  }
286
648
  function synchronizeNotebookChildren(context, notebook) {
287
649
  deleteMissing(context, "edges", "edge_id", notebook.id, notebook.edges.map((edge) => edge.id));
288
650
  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`);
651
+ evacuateChangedCellSlugs(context, notebook);
303
652
  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);
653
+ upsertNotebookCell(context, notebook.id, cell);
305
654
  }
655
+ repairCellSlots(context, notebook.id);
306
656
  const upsertEdge = context.store.db.prepare(`INSERT INTO edges(
307
657
  notebook_id, edge_id, source_cell_id, target_cell_id, target_input
308
658
  ) VALUES (?, ?, ?, ?, ?)
@@ -314,6 +664,168 @@ function synchronizeNotebookChildren(context, notebook) {
314
664
  upsertEdge.run(notebook.id, edge.id, edge.source, edge.target, edge.targetInput);
315
665
  }
316
666
  }
667
+ function synchronizeNotebookState(context, notebook) {
668
+ synchronizeNotebookFields(context, notebook);
669
+ synchronizeCellExecutions(context, notebook);
670
+ synchronizeGenerationPlans(context, notebook);
671
+ synchronizeRunPlans(context, notebook);
672
+ synchronizeTranscriptEdits(context, notebook);
673
+ synchronizeTranscriptAttachments(context, notebook);
674
+ }
675
+ function synchronizeNotebookFields(context, notebook) {
676
+ const values = new Map();
677
+ if (notebook.description !== undefined) {
678
+ values.set("description", notebook.description);
679
+ }
680
+ if (notebook.lifecycleState !== undefined) {
681
+ values.set("lifecycle_state", notebook.lifecycleState);
682
+ }
683
+ if (notebook.workflowVersion !== undefined) {
684
+ if (!Number.isSafeInteger(notebook.workflowVersion)) {
685
+ throw new Error("Notebook workflow version must be an integer");
686
+ }
687
+ values.set("workflow_version", notebook.workflowVersion);
688
+ }
689
+ if (notebook.analysisRevision !== undefined) {
690
+ values.set("analysis_revision", notebook.analysisRevision);
691
+ }
692
+ if (notebook.audioSpine !== undefined) {
693
+ values.set("audio_spine", notebook.audioSpine);
694
+ }
695
+ if (notebook.currentSelection !== undefined) {
696
+ values.set("current_selection", notebook.currentSelection);
697
+ }
698
+ if (notebook.fixture !== undefined) {
699
+ values.set("fixture", notebook.fixture);
700
+ }
701
+ deleteMissingNotebookRows(context, "notebook_fields", "field", notebook.id, [
702
+ ...values.keys(),
703
+ ]);
704
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_fields(notebook_id, field, value_json)
705
+ VALUES (?, ?, ?)
706
+ ON CONFLICT(notebook_id, field) DO UPDATE SET
707
+ value_json=excluded.value_json`);
708
+ for (const [field, value] of values) {
709
+ upsert.run(notebook.id, field, canonicalJson(value));
710
+ }
711
+ }
712
+ function synchronizeCellExecutions(context, notebook) {
713
+ const entries = Object.entries(notebook.execution ?? {});
714
+ const cellIds = new Set(notebook.cells.map((cell) => cell.id));
715
+ for (const [cellId] of entries) {
716
+ if (!cellIds.has(cellId)) {
717
+ throw new Error(`Notebook execution references missing cell: ${cellId}`);
718
+ }
719
+ }
720
+ deleteMissingNotebookRows(context, "notebook_cell_executions", "cell_id", notebook.id, entries.map(([cellId]) => cellId));
721
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_cell_executions(
722
+ notebook_id, cell_id, fingerprint, status, output_artifact_id,
723
+ provider_artifact_id, run_id, completed_at, started_at, updated_at,
724
+ tool, error, stale, fixture_baseline
725
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
726
+ ON CONFLICT(notebook_id, cell_id) DO UPDATE SET
727
+ fingerprint=excluded.fingerprint,
728
+ status=excluded.status,
729
+ output_artifact_id=excluded.output_artifact_id,
730
+ provider_artifact_id=excluded.provider_artifact_id,
731
+ run_id=excluded.run_id,
732
+ completed_at=excluded.completed_at,
733
+ started_at=excluded.started_at,
734
+ updated_at=excluded.updated_at,
735
+ tool=excluded.tool,
736
+ error=excluded.error,
737
+ stale=excluded.stale,
738
+ fixture_baseline=excluded.fixture_baseline`);
739
+ for (const [cellId, execution] of entries) {
740
+ 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);
741
+ }
742
+ }
743
+ function synchronizeGenerationPlans(context, notebook) {
744
+ const plans = notebook.generationPlans ?? [];
745
+ assertUnique(plans.map((plan) => plan.planId), "generation plan ID");
746
+ deleteMissingNotebookRows(context, "notebook_generation_plans", "plan_id", notebook.id, plans.map((plan) => plan.planId));
747
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_generation_plans(
748
+ notebook_id, plan_id, cell_id, status, plan_json,
749
+ output_artifact_id, error, created_at, updated_at
750
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
751
+ ON CONFLICT(notebook_id, plan_id) DO UPDATE SET
752
+ cell_id=excluded.cell_id,
753
+ status=excluded.status,
754
+ plan_json=excluded.plan_json,
755
+ output_artifact_id=excluded.output_artifact_id,
756
+ error=excluded.error,
757
+ created_at=excluded.created_at,
758
+ updated_at=excluded.updated_at`);
759
+ for (const plan of plans) {
760
+ 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"));
761
+ }
762
+ }
763
+ function synchronizeRunPlans(context, notebook) {
764
+ const plans = notebook.notebookRunPlans ?? [];
765
+ assertUnique(plans.map((plan) => plan.planId), "notebook run plan ID");
766
+ deleteMissingNotebookRows(context, "notebook_run_plans", "plan_id", notebook.id, plans.map((plan) => plan.planId));
767
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_run_plans(
768
+ notebook_id, plan_id, status, plan_json, paid_cell_ids_json,
769
+ cell_fingerprints_json, known_cost_usd, unknown_cost_count,
770
+ created_at, updated_at, run_id, outputs_json, error
771
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
772
+ ON CONFLICT(notebook_id, plan_id) DO UPDATE SET
773
+ status=excluded.status,
774
+ plan_json=excluded.plan_json,
775
+ paid_cell_ids_json=excluded.paid_cell_ids_json,
776
+ cell_fingerprints_json=excluded.cell_fingerprints_json,
777
+ known_cost_usd=excluded.known_cost_usd,
778
+ unknown_cost_count=excluded.unknown_cost_count,
779
+ created_at=excluded.created_at,
780
+ updated_at=excluded.updated_at,
781
+ run_id=excluded.run_id,
782
+ outputs_json=excluded.outputs_json,
783
+ error=excluded.error`);
784
+ for (const plan of plans) {
785
+ 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);
786
+ }
787
+ }
788
+ function synchronizeTranscriptEdits(context, notebook) {
789
+ const edits = notebook.transcriptEdits ?? [];
790
+ assertUnique(edits.map((edit) => edit.actionId), "transcript edit action ID");
791
+ deleteMissingNotebookRows(context, "notebook_transcript_edits", "action_id", notebook.id, edits.map((edit) => edit.actionId));
792
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_transcript_edits(
793
+ notebook_id, action_id, kind, restored, payload_json
794
+ ) VALUES (?, ?, ?, ?, ?)
795
+ ON CONFLICT(notebook_id, action_id) DO UPDATE SET
796
+ kind=excluded.kind,
797
+ restored=excluded.restored,
798
+ payload_json=excluded.payload_json`);
799
+ for (const edit of edits) {
800
+ upsert.run(notebook.id, requiredText(edit.actionId, "Transcript edit action ID"), requiredText(edit.kind, "Transcript edit kind"), edit.restored === true ? 1 : 0, canonicalJson(edit));
801
+ }
802
+ }
803
+ function synchronizeTranscriptAttachments(context, notebook) {
804
+ const attachments = notebook.transcriptAttachments ?? [];
805
+ assertUnique(attachments.map((attachment) => attachment.id), "transcript attachment ID");
806
+ deleteMissingNotebookRows(context, "notebook_transcript_attachments", "attachment_id", notebook.id, attachments.map((attachment) => attachment.id));
807
+ const upsert = context.store.db.prepare(`INSERT INTO notebook_transcript_attachments(
808
+ notebook_id, attachment_id, payload_json
809
+ ) VALUES (?, ?, ?)
810
+ ON CONFLICT(notebook_id, attachment_id) DO UPDATE SET
811
+ payload_json=excluded.payload_json`);
812
+ for (const attachment of attachments) {
813
+ upsert.run(notebook.id, requiredText(attachment.id, "Transcript attachment ID"), canonicalJson(attachment));
814
+ }
815
+ }
816
+ function deleteMissingNotebookRows(context, table, idColumn, notebookId, ids) {
817
+ if (ids.length === 0) {
818
+ context.store.db
819
+ .prepare(`DELETE FROM ${table} WHERE notebook_id=?`)
820
+ .run(notebookId);
821
+ return;
822
+ }
823
+ const placeholders = ids.map(() => "?").join(", ");
824
+ context.store.db
825
+ .prepare(`DELETE FROM ${table}
826
+ WHERE notebook_id=? AND ${idColumn} NOT IN (${placeholders})`)
827
+ .run(notebookId, ...ids);
828
+ }
317
829
  function deleteMissing(context, table, idColumn, notebookId, ids) {
318
830
  if (ids.length === 0) {
319
831
  context.store.db
@@ -327,21 +839,230 @@ function deleteMissing(context, table, idColumn, notebookId, ids) {
327
839
  WHERE notebook_id=? AND ${idColumn} NOT IN (${placeholders})`)
328
840
  .run(notebookId, ...ids);
329
841
  }
330
- function rowToCell(row) {
842
+ function validateCellReferences(context, notebook, cell) {
843
+ const ids = new Set();
844
+ const ordinals = new Set();
845
+ for (const reference of cell.references ?? []) {
846
+ assertUuidV7(reference.id, "Cell reference ID");
847
+ if (ids.has(reference.id)) {
848
+ throw new Error(`Duplicate cell reference ID: ${reference.id}`);
849
+ }
850
+ ids.add(reference.id);
851
+ validateOrdinal(reference.ordinal, "Cell reference ordinal");
852
+ if (ordinals.has(reference.ordinal)) {
853
+ throw new Error(`Duplicate cell reference ordinal: ${reference.ordinal}`);
854
+ }
855
+ ordinals.add(reference.ordinal);
856
+ requiredText(reference.targetId, "Cell reference target ID");
857
+ assertReferenceTarget(context, notebook, reference);
858
+ }
859
+ }
860
+ function assertReferenceTarget(context, notebook, reference) {
861
+ if (reference.kind === "artifact") {
862
+ context.artifactRowById(reference.targetId);
863
+ return;
864
+ }
865
+ if (reference.kind === "cell-output") {
866
+ if (!notebook.cells.some((cell) => cell.id === reference.targetId)) {
867
+ throw new Error(`Cell output target not found: ${reference.targetId}`);
868
+ }
869
+ return;
870
+ }
871
+ const table = reference.kind === "transcript"
872
+ ? "transcripts"
873
+ : reference.kind === "sequence"
874
+ ? "sequences"
875
+ : "artifact_streams";
876
+ const column = reference.kind === "transcript"
877
+ ? "transcript_id"
878
+ : reference.kind === "sequence"
879
+ ? "sequence_id"
880
+ : "stream_id";
881
+ const found = context.store.db
882
+ .prepare(`SELECT 1 AS present FROM ${table} WHERE ${column}=?`)
883
+ .get(reference.targetId);
884
+ if (!found) {
885
+ throw new Error(`${reference.kind} target not found: ${reference.targetId}`);
886
+ }
887
+ }
888
+ function validatePinnedResults(context, cell) {
889
+ const ids = new Set();
890
+ const ordinals = new Set();
891
+ for (const result of cell.pinnedResults ?? []) {
892
+ assertUuidV7(result.id, "Pinned search result ID");
893
+ if (ids.has(result.id)) {
894
+ throw new Error(`Duplicate pinned search result ID: ${result.id}`);
895
+ }
896
+ ids.add(result.id);
897
+ validateOrdinal(result.ordinal, "Pinned search result ordinal");
898
+ if (ordinals.has(result.ordinal)) {
899
+ throw new Error(`Duplicate pinned search result ordinal: ${result.ordinal}`);
900
+ }
901
+ ordinals.add(result.ordinal);
902
+ context.artifactRowById(result.artifactId);
903
+ requiredText(result.objectHash, "Pinned search result object hash");
904
+ normalizeSearchLocation(result.location);
905
+ requiredText(result.selectedRevision, "Pinned search result revision");
906
+ if (!Number.isSafeInteger(result.createdAt) || result.createdAt < 0) {
907
+ throw new Error("Pinned search result createdAt must be a positive integer");
908
+ }
909
+ const object = context.store.db
910
+ .prepare("SELECT 1 AS present FROM objects WHERE object_hash=?")
911
+ .get(result.objectHash);
912
+ if (!object) {
913
+ throw new Error(`Pinned search result object not found: ${result.objectHash}`);
914
+ }
915
+ }
916
+ }
917
+ function validateOrdinal(value, label) {
918
+ if (!Number.isSafeInteger(value) || value < 0) {
919
+ throw new Error(`${label} must be a non-negative safe integer`);
920
+ }
921
+ }
922
+ function synchronizeCellReferences(context, notebookId, cell) {
923
+ const references = cell.references ?? [];
924
+ deleteMissingChildren(context, "cell_references", "reference_id", notebookId, cell.id, references.map((reference) => reference.id));
925
+ const upsert = context.store.db.prepare(`INSERT INTO cell_references(
926
+ notebook_id, cell_id, reference_id, kind,
927
+ target_id, snapshot_json, ordinal
928
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
929
+ ON CONFLICT(notebook_id, cell_id, reference_id) DO UPDATE SET
930
+ kind=excluded.kind,
931
+ target_id=excluded.target_id,
932
+ snapshot_json=excluded.snapshot_json,
933
+ ordinal=excluded.ordinal`);
934
+ for (const reference of references) {
935
+ upsert.run(notebookId, cell.id, reference.id, reference.kind, reference.targetId, canonicalJson(reference.snapshot), reference.ordinal);
936
+ }
937
+ }
938
+ function synchronizePinnedResults(context, notebookId, cell) {
939
+ const pinnedResults = cell.pinnedResults ?? [];
940
+ deleteMissingChildren(context, "pinned_search_results", "result_id", notebookId, cell.id, pinnedResults.map((result) => result.id));
941
+ const upsert = context.store.db.prepare(`INSERT INTO pinned_search_results(
942
+ notebook_id, cell_id, result_id, artifact_id, object_hash,
943
+ location_json, representative_json, query_json, signals_json,
944
+ selected_revision, ordinal, created_at
945
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
946
+ ON CONFLICT(notebook_id, cell_id, result_id) DO UPDATE SET
947
+ artifact_id=excluded.artifact_id,
948
+ object_hash=excluded.object_hash,
949
+ location_json=excluded.location_json,
950
+ representative_json=excluded.representative_json,
951
+ query_json=excluded.query_json,
952
+ signals_json=excluded.signals_json,
953
+ selected_revision=excluded.selected_revision,
954
+ ordinal=excluded.ordinal,
955
+ created_at=excluded.created_at`);
956
+ for (const result of pinnedResults) {
957
+ upsert.run(notebookId, cell.id, result.id, result.artifactId, result.objectHash, canonicalJson(normalizeSearchLocation(result.location)), result.representativeTick === undefined
958
+ ? null
959
+ : canonicalJson(result.representativeTick), canonicalJson(result.query), canonicalJson(result.signals), result.selectedRevision, result.ordinal, result.createdAt);
960
+ }
961
+ }
962
+ function deleteMissingChildren(context, table, idColumn, notebookId, cellId, ids) {
963
+ if (ids.length === 0) {
964
+ context.store.db
965
+ .prepare(`DELETE FROM ${table} WHERE notebook_id=? AND cell_id=?`)
966
+ .run(notebookId, cellId);
967
+ return;
968
+ }
969
+ const placeholders = ids.map(() => "?").join(", ");
970
+ context.store.db
971
+ .prepare(`DELETE FROM ${table}
972
+ WHERE notebook_id=? AND cell_id=? AND ${idColumn} NOT IN (${placeholders})`)
973
+ .run(notebookId, cellId, ...ids);
974
+ }
975
+ function rowToCellReference(row) {
976
+ return {
977
+ id: row.reference_id,
978
+ kind: row.kind,
979
+ targetId: row.target_id,
980
+ snapshot: parseJson(row.snapshot_json, {}),
981
+ ordinal: row.ordinal,
982
+ };
983
+ }
984
+ function rowToPinnedSearchResult(row) {
985
+ const representativeTick = row.representative_json === null
986
+ ? undefined
987
+ : parseJson(row.representative_json, undefined);
988
+ return {
989
+ id: row.result_id,
990
+ artifactId: row.artifact_id,
991
+ objectHash: row.object_hash,
992
+ location: normalizeSearchLocation(parseJson(row.location_json, {
993
+ kind: "still",
994
+ artifactId: row.artifact_id,
995
+ sourcePath: "unknown",
996
+ objectHash: row.object_hash,
997
+ })),
998
+ ...(representativeTick === undefined ? {} : { representativeTick }),
999
+ query: parseJson(row.query_json, {}),
1000
+ signals: parseJson(row.signals_json, []),
1001
+ selectedRevision: row.selected_revision,
1002
+ ordinal: row.ordinal,
1003
+ createdAt: row.created_at,
1004
+ };
1005
+ }
1006
+ function rowToCell(row, references, pinnedResults) {
1007
+ const inputs = parseJson(row.inputs_json, {});
1008
+ const legacyProvider = optionalString(inputs.provider);
1009
+ const legacyOperation = optionalString(inputs.operation);
1010
+ const provider = optionalString(row.provider) ?? legacyProvider;
1011
+ const operation = optionalString(row.operation) ?? legacyOperation;
1012
+ const model = optionalString(row.model);
1013
+ const tool = optionalString(row.tool) ??
1014
+ (looksLikeGenerationTool(model) ? model : undefined);
331
1015
  return {
332
1016
  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 } : {}),
1017
+ type: notebookCellType(row.type),
1018
+ slug: row.slug,
1019
+ slot: { row: row.grid_row, column: row.grid_column },
1020
+ ...(row.output_entity_id ? { outputEntityId: row.output_entity_id } : {}),
337
1021
  ...(row.prompt ? { prompt: row.prompt } : {}),
338
- ...(row.model ? { model: row.model } : {}),
339
- inputs: parseJson(row.inputs_json, {}),
1022
+ ...(provider ? { provider } : {}),
1023
+ ...(model ? { model } : {}),
1024
+ ...(operation ? { operation } : {}),
1025
+ ...(tool ? { tool } : {}),
1026
+ inputs,
340
1027
  ...(row.output_artifact_id
341
1028
  ? { outputArtifactId: row.output_artifact_id }
342
1029
  : {}),
1030
+ references: references.map(rowToCellReference),
1031
+ pinnedResults: pinnedResults.map(rowToPinnedSearchResult),
1032
+ };
1033
+ }
1034
+ function notebookCellType(type) {
1035
+ if (!NOTEBOOK_CELL_TYPE_SET.has(type)) {
1036
+ throw new Error(`Invalid cell type: ${type}`);
1037
+ }
1038
+ return type;
1039
+ }
1040
+ function normalizeCellForWrite(cell) {
1041
+ const inputs = { ...(cell.inputs ?? {}) };
1042
+ const provider = optionalString(cell.provider) ?? optionalString(inputs.provider);
1043
+ const operation = optionalString(cell.operation) ?? optionalString(inputs.operation);
1044
+ const model = optionalString(cell.model);
1045
+ const tool = optionalString(cell.tool) ??
1046
+ (looksLikeGenerationTool(model) ? model : undefined);
1047
+ return {
1048
+ ...cell,
1049
+ ...(provider ? { provider } : { provider: undefined }),
1050
+ ...(operation ? { operation } : { operation: undefined }),
1051
+ ...(tool ? { tool } : { tool: undefined }),
1052
+ inputs,
343
1053
  };
344
1054
  }
1055
+ function optionalString(value) {
1056
+ if (typeof value !== "string")
1057
+ return undefined;
1058
+ const trimmed = value.trim();
1059
+ return trimmed ? trimmed : undefined;
1060
+ }
1061
+ function looksLikeGenerationTool(value) {
1062
+ if (!value)
1063
+ return false;
1064
+ return value.startsWith("generate_") || value.includes("/");
1065
+ }
345
1066
  function rowToEdge(row) {
346
1067
  return {
347
1068
  id: row.edge_id,
@@ -383,6 +1104,97 @@ function validateRunStatus(status) {
383
1104
  throw new Error(`Invalid notebook run status: ${status}`);
384
1105
  }
385
1106
  }
1107
+ function executionFromRow(row) {
1108
+ return {
1109
+ ...(row.fingerprint ? { fingerprint: row.fingerprint } : {}),
1110
+ ...(row.status ? { status: row.status } : {}),
1111
+ ...(row.output_artifact_id
1112
+ ? { outputArtifactId: row.output_artifact_id }
1113
+ : {}),
1114
+ ...(row.provider_artifact_id
1115
+ ? { providerArtifactId: row.provider_artifact_id }
1116
+ : {}),
1117
+ ...(row.run_id ? { runId: row.run_id } : {}),
1118
+ ...(row.completed_at ? { completedAt: row.completed_at } : {}),
1119
+ ...(row.started_at ? { startedAt: row.started_at } : {}),
1120
+ ...(row.updated_at ? { updatedAt: row.updated_at } : {}),
1121
+ ...(row.tool ? { tool: row.tool } : {}),
1122
+ ...(row.error ? { error: row.error } : {}),
1123
+ ...(row.stale === 1 ? { stale: true } : {}),
1124
+ ...(row.fixture_baseline === 1 ? { fixtureBaseline: true } : {}),
1125
+ };
1126
+ }
1127
+ function generationPlanFromRow(row) {
1128
+ return {
1129
+ planId: row.plan_id,
1130
+ cellId: row.cell_id,
1131
+ status: row.status,
1132
+ plan: parseJson(row.plan_json, {}),
1133
+ ...(row.output_artifact_id
1134
+ ? { outputArtifactId: row.output_artifact_id }
1135
+ : {}),
1136
+ ...(row.error ? { error: row.error } : {}),
1137
+ createdAt: row.created_at,
1138
+ updatedAt: row.updated_at,
1139
+ };
1140
+ }
1141
+ function runPlanFromRow(row) {
1142
+ return {
1143
+ planId: row.plan_id,
1144
+ status: row.status,
1145
+ plan: parseJson(row.plan_json, {}),
1146
+ paidCellIds: parseJson(row.paid_cell_ids_json, []),
1147
+ cellDefinitionFingerprints: parseJson(row.cell_fingerprints_json, {}),
1148
+ knownCostUsd: row.known_cost_usd,
1149
+ unknownCostCount: row.unknown_cost_count,
1150
+ createdAt: row.created_at,
1151
+ updatedAt: row.updated_at,
1152
+ ...(row.run_id ? { runId: row.run_id } : {}),
1153
+ ...(row.outputs_json
1154
+ ? {
1155
+ outputs: parseJson(row.outputs_json, {}),
1156
+ }
1157
+ : {}),
1158
+ ...(row.error ? { error: row.error } : {}),
1159
+ };
1160
+ }
1161
+ function transcriptEditFromRow(row) {
1162
+ const payload = parseJson(row.payload_json, {});
1163
+ return {
1164
+ ...payload,
1165
+ actionId: row.action_id,
1166
+ kind: row.kind,
1167
+ ...(row.restored === 1 ? { restored: true } : {}),
1168
+ };
1169
+ }
1170
+ function transcriptAttachmentFromRow(row) {
1171
+ const payload = parseJson(row.payload_json, {});
1172
+ return {
1173
+ ...payload,
1174
+ id: row.attachment_id,
1175
+ };
1176
+ }
1177
+ function assertUnique(values, label) {
1178
+ const unique = new Set(values);
1179
+ if (unique.size !== values.length) {
1180
+ throw new Error(`Duplicate ${label}`);
1181
+ }
1182
+ }
1183
+ function nonNegativeFinite(value, label) {
1184
+ if (!Number.isFinite(value) || value < 0) {
1185
+ throw new Error(`${label} must be a non-negative finite number`);
1186
+ }
1187
+ return value;
1188
+ }
1189
+ function nonNegativeInteger(value, label) {
1190
+ if (!Number.isSafeInteger(value) || value < 0) {
1191
+ throw new Error(`${label} must be a non-negative integer`);
1192
+ }
1193
+ return value;
1194
+ }
1195
+ function isRecord(value) {
1196
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1197
+ }
386
1198
  function callerOrNewId(id, label) {
387
1199
  const resolved = id ?? newUuidV7();
388
1200
  assertUuidV7(resolved, label);
@@ -393,7 +1205,7 @@ const ENTITY_SELECT = `
393
1205
  FROM entities
394
1206
  `;
395
1207
  const NOTEBOOK_SELECT = `
396
- SELECT notebook_id, name, properties_json, created_at
1208
+ SELECT notebook_id, name, created_at
397
1209
  FROM notebooks
398
1210
  `;
399
1211
  //# sourceMappingURL=domain.js.map