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/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, tables: readonly SemanticTable[], mutate: (operationId: string, now: number) => T): Promise<SemanticMutation<T>>;
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
@@ -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,EACrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EACV,mBAAmB,EACnB,WAAW,EACX,cAAc,EACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAqBrB,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;gBAEhB,KAAK,EAAE,WAAW;CAK/B;AAED,UAAU,gBAAgB,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,SAAS;IACpB,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,OAAO,CAAC,UAAU,CAAoC;gBAE1C,KAAK,EAAE;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/C,aAAa,CAAC,EAAE,mBAAmB,CAAC;KACrC;IAgBD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,MAAM,IAAI,eAAe,EAAE,CAE9B;IAED,KAAK,IAAI,IAAI;IAIP,QAAQ,CAAC,CAAC,EACd,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,GAC9C,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAkD/B,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC;IAazC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,WAAW,EAAE;IAInE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAO9B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,UAAU;IA8GlB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,YAAY;IA4CpB,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,yBAAyB;IAejC,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,MAAM;CAQf;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAMzD"}
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, tables, mutate) {
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, canonicalJson(touched), commitMessage(input, operationId), now);
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
- .prepare("SELECT dolt_push(?, 'main') AS result")
96
- .get(remoteName);
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
- .prepare(`INSERT INTO dolt_ignore(pattern, ignored)
138
- VALUES ('runtime_%', 1), ('sqlite_sequence', 1)
139
- ON CONFLICT(pattern) DO UPDATE SET ignored=excluded.ignored`)
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.db
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 || row.version !== SCHEMA_VERSION) {
178
+ if (!row) {
163
179
  this.db.close();
164
180
  throw new EngineFault({
165
181
  code: "SCHEMA_INCOMPATIBLE",
166
- message: `Database schema ${row?.version ?? "unknown"} is not supported by engine schema ${SCHEMA_VERSION}`,
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 tables = parseStringArray(row.tables_json);
235
- const changedTables = [];
236
- for (const table of tables) {
237
- if (!isSemanticTable(table)) {
238
- throw new EngineFault({
239
- code: "STORAGE_ERROR",
240
- message: `Commit outbox contains non-semantic table: ${table}`,
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
- this.stageTables(changedTables);
252
- this.assertOnlyVersionedStaged();
253
- const hasStaged = this.db
254
- .doltStatus()
255
- .some((entry) => entry.staged === 1);
256
- const revision = hasStaged ? this.sqlCommit(row.message) : this.head;
257
- this.runtime(() => {
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
- return revision;
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("SELECT dolt_commit('-m', ?) AS hash")
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
- assertOnlyVersionedStaged() {
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
- return `${input.operation}${target}\n\nop-id: ${operationId}`;
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 parseStringArray(text) {
367
- const parsed = parseJson(text, []);
368
- return Array.isArray(parsed)
369
- ? parsed.filter((item) => typeof item === "string")
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
  }