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.
- package/README.md +87 -41
- package/dist/artifact-names.d.ts +6 -0
- package/dist/artifact-names.d.ts.map +1 -0
- package/dist/artifact-names.js +56 -0
- package/dist/artifact-names.js.map +1 -0
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +57 -23
- package/dist/artifacts.js.map +1 -1
- package/dist/books.js +2 -1
- 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 +6 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +35 -6
- package/dist/context.js.map +1 -1
- package/dist/domain.d.ts +4 -0
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +870 -58
- 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 +110 -60
- package/dist/engine-types.d.ts.map +1 -1
- package/dist/engine-types.js +33 -0
- package/dist/engine-types.js.map +1 -1
- package/dist/engine.d.ts +69 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +24 -18
- package/dist/engine.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +31 -27
- package/dist/files.js.map +1 -1
- package/dist/fork.d.ts +147 -0
- package/dist/fork.d.ts.map +1 -0
- package/dist/fork.js +609 -0
- package/dist/fork.js.map +1 -0
- package/dist/history.d.ts +0 -4
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +166 -595
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- 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.map +1 -1
- package/dist/media.js +1 -0
- package/dist/media.js.map +1 -1
- package/dist/merge-policy.d.ts +99 -0
- package/dist/merge-policy.d.ts.map +1 -0
- package/dist/merge-policy.js +310 -0
- package/dist/merge-policy.js.map +1 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +15 -20
- 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 +534 -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 +113 -8
- package/dist/notebook/types.d.ts.map +1 -1
- 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 +4 -2
- package/dist/runtime-services.js.map +1 -1
- package/dist/schema.d.ts +24 -5
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +644 -132
- 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 +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 +63 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +351 -65
- 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 +1507 -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 +346 -106
- 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 +30 -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/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/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync, type DoltDiffRow, type DoltStatusEntry } from "@dolthub/doltlite";
|
|
2
|
-
import type { CatalogBackupConfig, EngineError, OperationInput } from "./engine-types.js";
|
|
2
|
+
import type { CatalogBackupConfig, EngineError, OperationInput, SemanticCommitBoundary } from "./engine-types.js";
|
|
3
3
|
import { type SemanticTable } from "./schema.js";
|
|
4
4
|
export declare class EngineFault extends Error {
|
|
5
5
|
readonly error: EngineError;
|
|
@@ -17,6 +17,9 @@ export declare class DoltStore {
|
|
|
17
17
|
readonly objectsDir: string;
|
|
18
18
|
readonly workspaceDir: string;
|
|
19
19
|
private writeChain;
|
|
20
|
+
private readonly author;
|
|
21
|
+
private readonly workingDiffProbes;
|
|
22
|
+
private readonly semanticCommitBoundary;
|
|
20
23
|
constructor(input: {
|
|
21
24
|
dataDir: string;
|
|
22
25
|
workspaceDir: string;
|
|
@@ -25,21 +28,66 @@ export declare class DoltStore {
|
|
|
25
28
|
slug: string;
|
|
26
29
|
};
|
|
27
30
|
catalogBackup?: CatalogBackupConfig;
|
|
31
|
+
author?: string;
|
|
32
|
+
semanticCommitBoundary?: (boundary: SemanticCommitBoundary, operationId: string) => void;
|
|
28
33
|
});
|
|
29
34
|
get head(): string;
|
|
30
35
|
get status(): DoltStatusEntry[];
|
|
31
36
|
close(): void;
|
|
32
|
-
semantic<T>(input: OperationInput,
|
|
37
|
+
semantic<T>(input: OperationInput, mutate: (operationId: string, now: number) => T): Promise<SemanticMutation<T>>;
|
|
33
38
|
runtime<T>(mutate: (now: number) => T): T;
|
|
34
39
|
diff(from: string, to: string, table: SemanticTable): DoltDiffRow[];
|
|
35
40
|
push(remoteName: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Physically reclaims unreferenced chunks in the Dolt storage layer.
|
|
43
|
+
* doltlite exposes `dolt_gc()` as a SQL function and returns a human
|
|
44
|
+
* readable summary such as "3 chunks removed, 42 chunks kept".
|
|
45
|
+
*/
|
|
46
|
+
doltGc(): string;
|
|
36
47
|
private configureConnection;
|
|
37
48
|
private initialize;
|
|
49
|
+
private ensureIgnorePatterns;
|
|
38
50
|
private configureRemote;
|
|
51
|
+
private initializePrimarySequence;
|
|
39
52
|
private recoverOutbox;
|
|
40
53
|
private commitOutbox;
|
|
54
|
+
private clearOutboxRow;
|
|
41
55
|
private sqlCommit;
|
|
42
56
|
private stageTables;
|
|
57
|
+
/**
|
|
58
|
+
* Full-catalog integrity sweep, run once per open (after outbox
|
|
59
|
+
* recovery). Semantic writes only probe and stage the tables their
|
|
60
|
+
* operation declared, so rows written to an undeclared table would sit
|
|
61
|
+
* in the working set indefinitely; this sweep turns that engine bug into
|
|
62
|
+
* a loud fault at the next open instead of silent attribution drift.
|
|
63
|
+
*
|
|
64
|
+
* doltStatus over-reports (doltlite flags index-bearing tables as
|
|
65
|
+
* modified while any untracked table exists), so every candidate is
|
|
66
|
+
* confirmed against the row-level diff probe before faulting.
|
|
67
|
+
*/
|
|
68
|
+
private verifyCleanSemanticWorktree;
|
|
69
|
+
/**
|
|
70
|
+
* True when `table` still has uncommitted working-set rows.
|
|
71
|
+
*
|
|
72
|
+
* This is the hot path of every semantic write: each declared table is
|
|
73
|
+
* probed once before staging and once after the commit. Staged-but-
|
|
74
|
+
* uncommitted rows also report `to_commit = 'WORKING'`, which is what
|
|
75
|
+
* makes the same probe correct during outbox recovery.
|
|
76
|
+
*
|
|
77
|
+
* The direct check, `doltDiff("HEAD","WORKING",t).length > 0`, materialises
|
|
78
|
+
* every changed row just to test emptiness. Asking the `dolt_diff_<table>`
|
|
79
|
+
* system table for a single row answers the identical question about 4x
|
|
80
|
+
* faster and allocates nothing. The statement is cached per table because
|
|
81
|
+
* preparing it each time cost more than running it, and repeated probes of
|
|
82
|
+
* the same table hit a doltlite-internal cache (~0.01ms) while probing
|
|
83
|
+
* many distinct tables in sequence thrashes it (~0.6ms each) — another
|
|
84
|
+
* reason writes probe only their declared tables.
|
|
85
|
+
*
|
|
86
|
+
* `table` is a SemanticTable, i.e. a member of the SEMANTIC_TABLES
|
|
87
|
+
* allowlist, so interpolating it into the statement cannot inject SQL.
|
|
88
|
+
*/
|
|
89
|
+
private hasWorkingDiff;
|
|
90
|
+
private assertCommittedTablesClean;
|
|
43
91
|
private assertOnlyVersionedStaged;
|
|
44
92
|
private assertRuntimeUnstaged;
|
|
45
93
|
private userTables;
|
|
@@ -51,5 +99,18 @@ export declare class DoltStore {
|
|
|
51
99
|
}
|
|
52
100
|
export declare function canonicalJson(value: unknown): string;
|
|
53
101
|
export declare function parseJson<T>(text: string, fallback: T): T;
|
|
102
|
+
export interface CommitOperation {
|
|
103
|
+
operation: string;
|
|
104
|
+
operationId: string;
|
|
105
|
+
artifactId?: string;
|
|
106
|
+
baseRevision?: string;
|
|
107
|
+
actor?: string;
|
|
108
|
+
writeSet: string[];
|
|
109
|
+
details: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
export declare function commitDateMs(date: string): number;
|
|
112
|
+
/** doltlite commit date normalized to an ISO-8601 UTC timestamp. */
|
|
113
|
+
export declare function commitDateIso(date: string): string;
|
|
114
|
+
export declare function parseCommitMessage(message: string): CommitOperation | null;
|
|
54
115
|
export {};
|
|
55
116
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/store.js
CHANGED
|
@@ -2,7 +2,11 @@ import { mkdirSync, existsSync } from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DatabaseSync, } from "@dolthub/doltlite";
|
|
4
4
|
import { v7 as uuidv7 } from "uuid";
|
|
5
|
+
import { initialOrderKeys } from "./order-keys.js";
|
|
5
6
|
import { RUNTIME_SCHEMA_SQL, RUNTIME_TABLES, SCHEMA_VERSION, SEMANTIC_SCHEMA_SQL, SEMANTIC_TABLES, } from "./schema.js";
|
|
7
|
+
const OUTBOX_ALLOW_EMPTY_FLAG = "allow-empty";
|
|
8
|
+
const DEFAULT_COMMIT_AUTHOR = "Videobook <videobook@localhost>";
|
|
9
|
+
const IGNORE_PATTERNS = ["runtime_%", "sqlite_sequence", "job_runs"];
|
|
6
10
|
export class EngineFault extends Error {
|
|
7
11
|
error;
|
|
8
12
|
constructor(error) {
|
|
@@ -18,10 +22,16 @@ export class DoltStore {
|
|
|
18
22
|
objectsDir;
|
|
19
23
|
workspaceDir;
|
|
20
24
|
writeChain = Promise.resolve();
|
|
25
|
+
author;
|
|
26
|
+
// Cached `dolt_diff_<table>` existence probes; see hasWorkingDiff.
|
|
27
|
+
workingDiffProbes = new Map();
|
|
28
|
+
semanticCommitBoundary;
|
|
21
29
|
constructor(input) {
|
|
22
30
|
this.dataDir = path.resolve(input.dataDir);
|
|
23
31
|
this.workspaceDir = path.resolve(input.workspaceDir);
|
|
24
32
|
this.databasePath = path.join(this.dataDir, "videobook.db");
|
|
33
|
+
this.author = input.author ?? DEFAULT_COMMIT_AUTHOR;
|
|
34
|
+
this.semanticCommitBoundary = input.semanticCommitBoundary;
|
|
25
35
|
this.objectsDir = path.join(this.dataDir, "objects", "sha256");
|
|
26
36
|
mkdirSync(this.dataDir, { recursive: true });
|
|
27
37
|
mkdirSync(this.objectsDir, { recursive: true });
|
|
@@ -42,33 +52,33 @@ export class DoltStore {
|
|
|
42
52
|
close() {
|
|
43
53
|
this.db.close();
|
|
44
54
|
}
|
|
45
|
-
async semantic(input,
|
|
55
|
+
async semantic(input, mutate) {
|
|
46
56
|
return this.serial(async () => {
|
|
47
57
|
const operationId = uuidv7();
|
|
48
58
|
const now = Date.now();
|
|
49
|
-
const touched = uniqueSemanticTables([...tables, "operations"]);
|
|
50
|
-
this.assertRuntimeUnstaged();
|
|
51
59
|
this.begin();
|
|
52
60
|
let value;
|
|
53
61
|
try {
|
|
54
62
|
value = mutate(operationId, now);
|
|
55
|
-
this.db
|
|
56
|
-
.prepare(`INSERT INTO operations(
|
|
57
|
-
operation_id, operation, artifact_id, details_json,
|
|
58
|
-
write_set_json, base_revision, created_at, author
|
|
59
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
60
|
-
.run(operationId, input.operation, input.artifactId ?? null, canonicalJson(input.details ?? {}), canonicalJson(input.writeSet ?? []), input.baseRevision ?? null, now, input.author ?? "videobook");
|
|
61
63
|
this.db
|
|
62
64
|
.prepare(`INSERT INTO runtime_commit_outbox(
|
|
63
65
|
operation_id, tables_json, message, created_at
|
|
64
66
|
) VALUES (?, ?, ?, ?)`)
|
|
65
|
-
.run(operationId,
|
|
67
|
+
.run(operationId,
|
|
68
|
+
// The declared write set commits atomically with the mutation,
|
|
69
|
+
// so recovery after a crash stages exactly the same tables.
|
|
70
|
+
canonicalJson({
|
|
71
|
+
tables: uniqueSemanticTables(input.tables),
|
|
72
|
+
...(input.allowEmpty ? { allowEmpty: true } : {}),
|
|
73
|
+
}), commitMessage(input, operationId), now);
|
|
74
|
+
this.semanticCommitBoundary?.("before-sql-commit", operationId);
|
|
66
75
|
this.commitSql();
|
|
67
76
|
}
|
|
68
77
|
catch (error) {
|
|
69
78
|
this.rollback();
|
|
70
79
|
throw error;
|
|
71
80
|
}
|
|
81
|
+
this.semanticCommitBoundary?.("after-sql-commit", operationId);
|
|
72
82
|
const revision = this.commitOutbox(operationId);
|
|
73
83
|
return { value, revision, operationId };
|
|
74
84
|
});
|
|
@@ -91,9 +101,19 @@ export class DoltStore {
|
|
|
91
101
|
}
|
|
92
102
|
push(remoteName) {
|
|
93
103
|
this.assertRuntimeUnstaged();
|
|
94
|
-
this.db
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
this.db.prepare("SELECT dolt_push(?, 'main') AS result").get(remoteName);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Physically reclaims unreferenced chunks in the Dolt storage layer.
|
|
108
|
+
* doltlite exposes `dolt_gc()` as a SQL function and returns a human
|
|
109
|
+
* readable summary such as "3 chunks removed, 42 chunks kept".
|
|
110
|
+
*/
|
|
111
|
+
doltGc() {
|
|
112
|
+
this.assertRuntimeUnstaged();
|
|
113
|
+
const row = this.db
|
|
114
|
+
.prepare("SELECT dolt_gc() AS result")
|
|
115
|
+
.get();
|
|
116
|
+
return row?.result ?? "";
|
|
97
117
|
}
|
|
98
118
|
configureConnection() {
|
|
99
119
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -133,11 +153,11 @@ export class DoltStore {
|
|
|
133
153
|
ignored TINYINT NOT NULL,
|
|
134
154
|
PRIMARY KEY(pattern)
|
|
135
155
|
)`);
|
|
136
|
-
this.db
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.run();
|
|
156
|
+
const insertIgnorePattern = this.db.prepare(`INSERT INTO dolt_ignore(pattern, ignored)
|
|
157
|
+
VALUES (?, 1)
|
|
158
|
+
ON CONFLICT(pattern) DO UPDATE SET ignored=excluded.ignored`);
|
|
159
|
+
for (const pattern of IGNORE_PATTERNS)
|
|
160
|
+
insertIgnorePattern.run(pattern);
|
|
141
161
|
const now = Date.now();
|
|
142
162
|
this.db
|
|
143
163
|
.prepare("INSERT INTO engine_schema(singleton, version, created_at) VALUES (1, ?, ?)")
|
|
@@ -145,13 +165,9 @@ export class DoltStore {
|
|
|
145
165
|
this.db
|
|
146
166
|
.prepare("INSERT INTO book(book_id, slug, created_at) VALUES (?, ?, ?)")
|
|
147
167
|
.run(initialBook.bookId, initialBook.slug, now);
|
|
148
|
-
this.
|
|
149
|
-
.prepare("INSERT INTO timeline(book_id, render) VALUES (?, 'landscape')")
|
|
150
|
-
.run(initialBook.bookId);
|
|
168
|
+
this.initializePrimarySequence(initialBook.bookId, now);
|
|
151
169
|
this.stageTables(SEMANTIC_TABLES);
|
|
152
|
-
this.db
|
|
153
|
-
.prepare("SELECT dolt_add('dolt_ignore') AS result")
|
|
154
|
-
.get();
|
|
170
|
+
this.db.prepare("SELECT dolt_add('dolt_ignore') AS result").get();
|
|
155
171
|
this.assertOnlyVersionedStaged();
|
|
156
172
|
this.sqlCommit("Initialize Videobook book");
|
|
157
173
|
}
|
|
@@ -159,11 +175,19 @@ export class DoltStore {
|
|
|
159
175
|
const row = this.db
|
|
160
176
|
.prepare("SELECT version FROM engine_schema WHERE singleton = 1")
|
|
161
177
|
.get();
|
|
162
|
-
if (!row
|
|
178
|
+
if (!row) {
|
|
163
179
|
this.db.close();
|
|
164
180
|
throw new EngineFault({
|
|
165
181
|
code: "SCHEMA_INCOMPATIBLE",
|
|
166
|
-
message:
|
|
182
|
+
message: "Database schema unknown is not supported by engine schema " +
|
|
183
|
+
`${SCHEMA_VERSION}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (row.version !== SCHEMA_VERSION) {
|
|
187
|
+
this.db.close();
|
|
188
|
+
throw new EngineFault({
|
|
189
|
+
code: "SCHEMA_INCOMPATIBLE",
|
|
190
|
+
message: `Database schema ${row.version} is not supported by engine schema ${SCHEMA_VERSION}`,
|
|
167
191
|
});
|
|
168
192
|
}
|
|
169
193
|
const books = this.db
|
|
@@ -183,6 +207,7 @@ export class DoltStore {
|
|
|
183
207
|
message: "Videobook only supports the main branch",
|
|
184
208
|
});
|
|
185
209
|
}
|
|
210
|
+
this.ensureIgnorePatterns();
|
|
186
211
|
}
|
|
187
212
|
this.db.exec(RUNTIME_SCHEMA_SQL);
|
|
188
213
|
this.db
|
|
@@ -194,6 +219,16 @@ export class DoltStore {
|
|
|
194
219
|
.run(String(SCHEMA_VERSION), Date.now());
|
|
195
220
|
this.assertRuntimeUnstaged();
|
|
196
221
|
this.recoverOutbox();
|
|
222
|
+
this.verifyCleanSemanticWorktree();
|
|
223
|
+
}
|
|
224
|
+
ensureIgnorePatterns() {
|
|
225
|
+
const insert = this.db.prepare(`INSERT INTO dolt_ignore(pattern, ignored)
|
|
226
|
+
SELECT ?, 1
|
|
227
|
+
WHERE NOT EXISTS(
|
|
228
|
+
SELECT 1 FROM dolt_ignore WHERE pattern = ?
|
|
229
|
+
)`);
|
|
230
|
+
for (const pattern of IGNORE_PATTERNS)
|
|
231
|
+
insert.run(pattern, pattern);
|
|
197
232
|
}
|
|
198
233
|
configureRemote(remote) {
|
|
199
234
|
const rows = this.db
|
|
@@ -213,6 +248,30 @@ export class DoltStore {
|
|
|
213
248
|
.prepare("SELECT dolt_remote('add', ?, ?) AS result")
|
|
214
249
|
.get(remote.name, remote.url);
|
|
215
250
|
}
|
|
251
|
+
initializePrimarySequence(bookId, now) {
|
|
252
|
+
const sequenceId = uuidv7();
|
|
253
|
+
this.db
|
|
254
|
+
.prepare(`INSERT INTO sequences(
|
|
255
|
+
sequence_id, book_id, name, is_primary, width, height,
|
|
256
|
+
pixel_aspect_numerator, pixel_aspect_denominator,
|
|
257
|
+
frame_rate_numerator, frame_rate_denominator,
|
|
258
|
+
audio_sample_rate_hz, audio_channel_layout,
|
|
259
|
+
background_rgba_json, created_at
|
|
260
|
+
) VALUES (?, ?, 'Main', 1, 1920, 1080, 1, 1, 30, 1, 48000, 'stereo', ?, ?)`)
|
|
261
|
+
.run(sequenceId, bookId, canonicalJson([0, 0, 0, 1]), now);
|
|
262
|
+
const insertTrack = this.db.prepare(`INSERT INTO sequence_tracks(
|
|
263
|
+
track_id, sequence_id, kind, order_key, name,
|
|
264
|
+
enabled, locked, muted, solo, blend_mode
|
|
265
|
+
) VALUES (?, ?, ?, ?, ?, 1, 0, ?, ?, ?)`);
|
|
266
|
+
const videoKeys = initialOrderKeys(2);
|
|
267
|
+
insertTrack.run(uuidv7(), sequenceId, "video", videoKeys[0], "Video 1", null, null, "normal");
|
|
268
|
+
insertTrack.run(uuidv7(), sequenceId, "video", videoKeys[1], "Video 2", null, null, "normal");
|
|
269
|
+
const audioKeys = initialOrderKeys(4);
|
|
270
|
+
for (let ordinal = 0; ordinal < 4; ordinal += 1) {
|
|
271
|
+
insertTrack.run(uuidv7(), sequenceId, "audio", audioKeys[ordinal], `Audio ${ordinal + 1}`, 0, 0, null);
|
|
272
|
+
}
|
|
273
|
+
insertTrack.run(uuidv7(), sequenceId, "caption", initialOrderKeys(1)[0], "Captions", null, null, null);
|
|
274
|
+
}
|
|
216
275
|
recoverOutbox() {
|
|
217
276
|
const rows = this.db
|
|
218
277
|
.prepare(`SELECT operation_id, tables_json, message
|
|
@@ -231,40 +290,58 @@ export class DoltStore {
|
|
|
231
290
|
.get(operationId);
|
|
232
291
|
if (!row)
|
|
233
292
|
return this.head;
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
const changed = this.db
|
|
244
|
-
.doltStatus()
|
|
245
|
-
.some((entry) => entry.table_name === table &&
|
|
246
|
-
entry.staged === 0 &&
|
|
247
|
-
entry.status !== "ignored");
|
|
248
|
-
if (changed)
|
|
249
|
-
changedTables.push(table);
|
|
293
|
+
const declared = parseOutboxTables(row.tables_json);
|
|
294
|
+
const dirty = declared.tables.filter((table) => this.hasWorkingDiff(table));
|
|
295
|
+
if (dirty.length === 0 && !declared.allowEmpty) {
|
|
296
|
+
// Either the mutation only touched ignored runtime tables (bookkeeping
|
|
297
|
+
// mints no commit), or this is recovery after a crash that followed the
|
|
298
|
+
// dolt commit. Either way only the outbox row is left to clear.
|
|
299
|
+
this.clearOutboxRow(operationId);
|
|
300
|
+
return this.head;
|
|
250
301
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
this.
|
|
302
|
+
// One status scan guards the staging boundary: anything already staged
|
|
303
|
+
// must be on the allowlist (a runtime table here is corruption), and a
|
|
304
|
+
// dirty dolt_ignore rides along with the commit. Our own dolt_add calls
|
|
305
|
+
// below stage allowlisted tables only, so the pre-staging snapshot
|
|
306
|
+
// covers the post-staging assertion too.
|
|
307
|
+
const status = this.db.doltStatus();
|
|
308
|
+
this.assertOnlyVersionedStaged(status);
|
|
309
|
+
this.stageTables(dirty);
|
|
310
|
+
if (status.some((entry) => entry.table_name === "dolt_ignore" && entry.staged === 0)) {
|
|
311
|
+
this.db.prepare("SELECT dolt_add('dolt_ignore') AS result").get();
|
|
312
|
+
}
|
|
313
|
+
const revision = this.sqlCommit(row.message, dirty.length === 0);
|
|
314
|
+
this.semanticCommitBoundary?.("after-dolt-commit", operationId);
|
|
315
|
+
// The commit is durable at this point: clear the outbox row before
|
|
316
|
+
// asserting, so a failed assertion can neither strand the row nor
|
|
317
|
+
// re-attribute this operation's rows to a later commit at recovery.
|
|
318
|
+
this.clearOutboxRow(operationId);
|
|
319
|
+
this.assertCommittedTablesClean(dirty, revision);
|
|
320
|
+
return revision;
|
|
321
|
+
}
|
|
322
|
+
clearOutboxRow(operationId) {
|
|
323
|
+
// A single-statement delete on a runtime table; the full runtime()
|
|
324
|
+
// pre-flight would re-scan doltStatus on every semantic write for no
|
|
325
|
+
// added safety.
|
|
326
|
+
this.begin();
|
|
327
|
+
try {
|
|
258
328
|
this.db
|
|
259
329
|
.prepare("DELETE FROM runtime_commit_outbox WHERE operation_id = ?")
|
|
260
330
|
.run(operationId);
|
|
261
|
-
|
|
262
|
-
|
|
331
|
+
this.commitSql();
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
this.rollback();
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
263
337
|
}
|
|
264
|
-
sqlCommit(message) {
|
|
338
|
+
sqlCommit(message, allowEmpty = false) {
|
|
339
|
+
const statement = allowEmpty
|
|
340
|
+
? "SELECT dolt_commit('--allow-empty', '-m', ?, '--author', ?) AS hash"
|
|
341
|
+
: "SELECT dolt_commit('-m', ?, '--author', ?) AS hash";
|
|
265
342
|
const row = this.db
|
|
266
|
-
.prepare(
|
|
267
|
-
.get(message);
|
|
343
|
+
.prepare(statement)
|
|
344
|
+
.get(message, this.author);
|
|
268
345
|
const hash = row?.hash;
|
|
269
346
|
if (!hash) {
|
|
270
347
|
throw new EngineFault({
|
|
@@ -272,19 +349,91 @@ export class DoltStore {
|
|
|
272
349
|
message: "Dolt commit did not return a revision hash",
|
|
273
350
|
});
|
|
274
351
|
}
|
|
275
|
-
this.assertRuntimeUnstaged();
|
|
276
352
|
return hash;
|
|
277
353
|
}
|
|
278
354
|
stageTables(tables) {
|
|
355
|
+
// One dolt_add per table: the multi-argument form silently falls back
|
|
356
|
+
// to staging every table doltStatus over-reports as modified, which
|
|
357
|
+
// would commit unrelated storage noise (and, worse, ride over the
|
|
358
|
+
// runtime-table fence).
|
|
279
359
|
const unique = uniqueSemanticTables(tables);
|
|
280
360
|
for (const table of unique) {
|
|
281
|
-
this.db
|
|
282
|
-
.prepare("SELECT dolt_add(?) AS result")
|
|
283
|
-
.get(table);
|
|
361
|
+
this.db.prepare("SELECT dolt_add(?) AS result").get(table);
|
|
284
362
|
}
|
|
285
363
|
}
|
|
286
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Full-catalog integrity sweep, run once per open (after outbox
|
|
366
|
+
* recovery). Semantic writes only probe and stage the tables their
|
|
367
|
+
* operation declared, so rows written to an undeclared table would sit
|
|
368
|
+
* in the working set indefinitely; this sweep turns that engine bug into
|
|
369
|
+
* a loud fault at the next open instead of silent attribution drift.
|
|
370
|
+
*
|
|
371
|
+
* doltStatus over-reports (doltlite flags index-bearing tables as
|
|
372
|
+
* modified while any untracked table exists), so every candidate is
|
|
373
|
+
* confirmed against the row-level diff probe before faulting.
|
|
374
|
+
*/
|
|
375
|
+
verifyCleanSemanticWorktree() {
|
|
376
|
+
const dirty = [];
|
|
287
377
|
for (const entry of this.db.doltStatus()) {
|
|
378
|
+
if (entry.staged !== 0 || entry.status === "ignored")
|
|
379
|
+
continue;
|
|
380
|
+
if (!isSemanticTable(entry.table_name))
|
|
381
|
+
continue;
|
|
382
|
+
if (this.hasWorkingDiff(entry.table_name)) {
|
|
383
|
+
dirty.push(entry.table_name);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (dirty.length > 0) {
|
|
387
|
+
this.db.close();
|
|
388
|
+
throw new EngineFault({
|
|
389
|
+
code: "STORAGE_ERROR",
|
|
390
|
+
message: "Semantic worktree has uncommitted changes not attributed to any operation: " +
|
|
391
|
+
dirty.join(", "),
|
|
392
|
+
details: { dirtyTables: dirty },
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* True when `table` still has uncommitted working-set rows.
|
|
398
|
+
*
|
|
399
|
+
* This is the hot path of every semantic write: each declared table is
|
|
400
|
+
* probed once before staging and once after the commit. Staged-but-
|
|
401
|
+
* uncommitted rows also report `to_commit = 'WORKING'`, which is what
|
|
402
|
+
* makes the same probe correct during outbox recovery.
|
|
403
|
+
*
|
|
404
|
+
* The direct check, `doltDiff("HEAD","WORKING",t).length > 0`, materialises
|
|
405
|
+
* every changed row just to test emptiness. Asking the `dolt_diff_<table>`
|
|
406
|
+
* system table for a single row answers the identical question about 4x
|
|
407
|
+
* faster and allocates nothing. The statement is cached per table because
|
|
408
|
+
* preparing it each time cost more than running it, and repeated probes of
|
|
409
|
+
* the same table hit a doltlite-internal cache (~0.01ms) while probing
|
|
410
|
+
* many distinct tables in sequence thrashes it (~0.6ms each) — another
|
|
411
|
+
* reason writes probe only their declared tables.
|
|
412
|
+
*
|
|
413
|
+
* `table` is a SemanticTable, i.e. a member of the SEMANTIC_TABLES
|
|
414
|
+
* allowlist, so interpolating it into the statement cannot inject SQL.
|
|
415
|
+
*/
|
|
416
|
+
hasWorkingDiff(table) {
|
|
417
|
+
let probe = this.workingDiffProbes.get(table);
|
|
418
|
+
if (!probe) {
|
|
419
|
+
probe = this.db.prepare(`SELECT 1 AS present FROM dolt_diff_${table}
|
|
420
|
+
WHERE to_commit = 'WORKING' LIMIT 1`);
|
|
421
|
+
this.workingDiffProbes.set(table, probe);
|
|
422
|
+
}
|
|
423
|
+
return Boolean(probe.get());
|
|
424
|
+
}
|
|
425
|
+
assertCommittedTablesClean(tables, committedRevision) {
|
|
426
|
+
const dirty = tables.filter((table) => this.hasWorkingDiff(table));
|
|
427
|
+
if (dirty.length > 0) {
|
|
428
|
+
throw new EngineFault({
|
|
429
|
+
code: "STORAGE_ERROR",
|
|
430
|
+
message: "Semantic worktree is dirty after commit: " + dirty.join(", "),
|
|
431
|
+
details: { committedRevision, dirtyTables: dirty },
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
assertOnlyVersionedStaged(status = this.db.doltStatus()) {
|
|
436
|
+
for (const entry of status) {
|
|
288
437
|
if (entry.staged !== 1)
|
|
289
438
|
continue;
|
|
290
439
|
if (!isSemanticTable(entry.table_name) &&
|
|
@@ -359,19 +508,156 @@ function sortJson(value) {
|
|
|
359
508
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
360
509
|
.map(([key, item]) => [key, sortJson(item)]));
|
|
361
510
|
}
|
|
511
|
+
// The commit is the provenance record: the subject names the operation (and
|
|
512
|
+
// the artifact it targeted), and git-style trailers carry the operation ID,
|
|
513
|
+
// base revision, actor, write set, and parameters as canonical JSON.
|
|
514
|
+
//
|
|
515
|
+
// doltlite rejects commit messages of 65536 bytes or more, so oversized
|
|
516
|
+
// payloads degrade deterministically: the details trailer is dropped first
|
|
517
|
+
// (a `details-omitted` trailer records its size), then the write-set trailer
|
|
518
|
+
// (a `write-set-omitted` trailer records its size). History and conflict
|
|
519
|
+
// projections treat omitted trailers as empty.
|
|
520
|
+
const MAX_COMMIT_MESSAGE_BYTES = 65_024;
|
|
362
521
|
function commitMessage(input, operationId) {
|
|
522
|
+
const message = buildCommitMessage(input, operationId, true, true);
|
|
523
|
+
if (byteLength(message) < MAX_COMMIT_MESSAGE_BYTES)
|
|
524
|
+
return message;
|
|
525
|
+
const withoutDetails = buildCommitMessage(input, operationId, false, true);
|
|
526
|
+
if (byteLength(withoutDetails) < MAX_COMMIT_MESSAGE_BYTES) {
|
|
527
|
+
return withoutDetails;
|
|
528
|
+
}
|
|
529
|
+
const minimal = buildCommitMessage(input, operationId, false, false);
|
|
530
|
+
if (byteLength(minimal) >= MAX_COMMIT_MESSAGE_BYTES) {
|
|
531
|
+
throw new EngineFault({
|
|
532
|
+
code: "STORAGE_ERROR",
|
|
533
|
+
message: `Commit message for ${input.operation} exceeds the Dolt size limit`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return minimal;
|
|
537
|
+
}
|
|
538
|
+
// Trailer values are single-line by construction: canonicalJson escapes
|
|
539
|
+
// newlines inside JSON, and every scalar value (operation, artifact ID,
|
|
540
|
+
// base revision, actor) is rejected if it carries a control character —
|
|
541
|
+
// otherwise a crafted actor could inject forged trailer lines.
|
|
542
|
+
function assertSafeTrailerValue(value, label) {
|
|
543
|
+
if (/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
544
|
+
throw new EngineFault({
|
|
545
|
+
code: "INVALID_INPUT",
|
|
546
|
+
message: `${label} must not contain control characters`,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function buildCommitMessage(input, operationId, includeDetails, includeWriteSet) {
|
|
551
|
+
assertSafeTrailerValue(input.operation, "Operation name");
|
|
552
|
+
if (input.artifactId)
|
|
553
|
+
assertSafeTrailerValue(input.artifactId, "Artifact ID");
|
|
554
|
+
if (input.baseRevision) {
|
|
555
|
+
assertSafeTrailerValue(input.baseRevision, "Base revision");
|
|
556
|
+
}
|
|
557
|
+
if (input.author)
|
|
558
|
+
assertSafeTrailerValue(input.author, "Actor");
|
|
363
559
|
const target = input.artifactId ? ` artifact:${input.artifactId}` : "";
|
|
364
|
-
|
|
560
|
+
const trailers = [`op-id: ${operationId}`];
|
|
561
|
+
if (input.baseRevision)
|
|
562
|
+
trailers.push(`base-revision: ${input.baseRevision}`);
|
|
563
|
+
const writeSetJson = canonicalJson(input.writeSet ?? []);
|
|
564
|
+
if (includeWriteSet && input.writeSet && input.writeSet.length > 0) {
|
|
565
|
+
trailers.push(`write-set: ${writeSetJson}`);
|
|
566
|
+
}
|
|
567
|
+
else if (!includeWriteSet && input.writeSet && input.writeSet.length > 0) {
|
|
568
|
+
trailers.push(`write-set-omitted: ${byteLength(writeSetJson)}`);
|
|
569
|
+
}
|
|
570
|
+
const detailsJson = canonicalJson(input.details ?? {});
|
|
571
|
+
if (includeDetails &&
|
|
572
|
+
input.details &&
|
|
573
|
+
Object.keys(input.details).length > 0) {
|
|
574
|
+
trailers.push(`details: ${detailsJson}`);
|
|
575
|
+
}
|
|
576
|
+
else if (!includeDetails &&
|
|
577
|
+
input.details &&
|
|
578
|
+
Object.keys(input.details).length > 0) {
|
|
579
|
+
trailers.push(`details-omitted: ${byteLength(detailsJson)}`);
|
|
580
|
+
}
|
|
581
|
+
// The caller-influenced actor value is emitted last and the parser is
|
|
582
|
+
// first-occurrence-wins, so even a hostile value that slips past
|
|
583
|
+
// validation can never override an engine-emitted trailer.
|
|
584
|
+
if (input.author)
|
|
585
|
+
trailers.push(`actor: ${input.author}`);
|
|
586
|
+
return `${input.operation}${target}\n\n${trailers.join("\n")}`;
|
|
365
587
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
588
|
+
function byteLength(text) {
|
|
589
|
+
return Buffer.byteLength(text, "utf8");
|
|
590
|
+
}
|
|
591
|
+
// doltlite reports commit dates as "YYYY-MM-DD HH:MM:SS" in UTC.
|
|
592
|
+
export function commitDateMs(date) {
|
|
593
|
+
const parsed = Date.parse(`${date.replace(" ", "T")}Z`);
|
|
594
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
595
|
+
}
|
|
596
|
+
/** doltlite commit date normalized to an ISO-8601 UTC timestamp. */
|
|
597
|
+
export function commitDateIso(date) {
|
|
598
|
+
const parsed = Date.parse(`${date.replace(" ", "T")}Z`);
|
|
599
|
+
return Number.isNaN(parsed) ? date : new Date(parsed).toISOString();
|
|
600
|
+
}
|
|
601
|
+
export function parseCommitMessage(message) {
|
|
602
|
+
const lines = message.split("\n");
|
|
603
|
+
const subject = lines[0] ?? "";
|
|
604
|
+
const trailers = new Map();
|
|
605
|
+
for (const line of lines.slice(1)) {
|
|
606
|
+
const match = /^([a-z-]+): (.*)$/.exec(line);
|
|
607
|
+
// First occurrence wins: engine trailers precede the caller-influenced
|
|
608
|
+
// actor trailer, so injected duplicates can never shadow them.
|
|
609
|
+
if (match && !trailers.has(match[1]))
|
|
610
|
+
trailers.set(match[1], match[2]);
|
|
611
|
+
}
|
|
612
|
+
const operationId = trailers.get("op-id");
|
|
613
|
+
if (!operationId)
|
|
614
|
+
return null;
|
|
615
|
+
let operation = subject;
|
|
616
|
+
let artifactId;
|
|
617
|
+
const artifactMarker = subject.lastIndexOf(" artifact:");
|
|
618
|
+
if (artifactMarker > 0) {
|
|
619
|
+
operation = subject.slice(0, artifactMarker);
|
|
620
|
+
artifactId = subject.slice(artifactMarker + " artifact:".length);
|
|
621
|
+
}
|
|
622
|
+
if (!operation)
|
|
623
|
+
return null;
|
|
624
|
+
const baseRevision = trailers.get("base-revision");
|
|
625
|
+
const actor = trailers.get("actor");
|
|
626
|
+
return {
|
|
627
|
+
operation,
|
|
628
|
+
operationId,
|
|
629
|
+
...(artifactId ? { artifactId } : {}),
|
|
630
|
+
...(baseRevision ? { baseRevision } : {}),
|
|
631
|
+
...(actor ? { actor } : {}),
|
|
632
|
+
writeSet: parseJson(trailers.get("write-set") ?? "[]", []),
|
|
633
|
+
details: parseJson(trailers.get("details") ?? "{}", {}),
|
|
634
|
+
};
|
|
371
635
|
}
|
|
372
636
|
function isSemanticTable(table) {
|
|
373
637
|
return SEMANTIC_TABLES.includes(table);
|
|
374
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Decodes the declared write set persisted with an outbox row. Rows written
|
|
641
|
+
* before write sets were declared carried a bare array (empty, or holding
|
|
642
|
+
* only the allow-empty flag); recovering one of those probes the full
|
|
643
|
+
* allowlist because the written tables are unknown.
|
|
644
|
+
*/
|
|
645
|
+
function parseOutboxTables(text) {
|
|
646
|
+
const parsed = parseJson(text, []);
|
|
647
|
+
if (Array.isArray(parsed)) {
|
|
648
|
+
return {
|
|
649
|
+
tables: [...SEMANTIC_TABLES],
|
|
650
|
+
allowEmpty: parsed.includes(OUTBOX_ALLOW_EMPTY_FLAG),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const record = parsed;
|
|
654
|
+
return {
|
|
655
|
+
tables: Array.isArray(record.tables)
|
|
656
|
+
? record.tables.filter((table) => typeof table === "string" && isSemanticTable(table))
|
|
657
|
+
: [...SEMANTIC_TABLES],
|
|
658
|
+
allowEmpty: record.allowEmpty === true,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
375
661
|
function uniqueSemanticTables(tables) {
|
|
376
662
|
return [...new Set(tables)];
|
|
377
663
|
}
|