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