@hraness/oh 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +164 -114
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +811 -97
  5. package/dist/errors.d.ts +39 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/graph.d.ts.map +1 -1
  8. package/dist/index.js +676 -61
  9. package/dist/libsql.d.ts.map +1 -1
  10. package/dist/libsql.js +162 -35
  11. package/dist/memory-page.js +2 -2
  12. package/dist/memory.d.ts +87 -6
  13. package/dist/memory.d.ts.map +1 -1
  14. package/dist/memory.js +1106 -148
  15. package/dist/operation.d.ts +3 -1
  16. package/dist/operation.d.ts.map +1 -1
  17. package/dist/projection-public.js +2 -2
  18. package/dist/projection-suss.js +2 -2
  19. package/dist/sdk.js +780 -88
  20. package/dist/semantic-cloud.js +2 -2
  21. package/dist/semantic.js +2 -2
  22. package/dist/sqlite/index.js +1251 -306
  23. package/dist/sqlite/port.d.ts +31 -3
  24. package/dist/sqlite/port.d.ts.map +1 -1
  25. package/dist/sqlite/store.d.ts +18 -2
  26. package/dist/sqlite/store.d.ts.map +1 -1
  27. package/dist/store.d.ts +3 -12
  28. package/dist/store.d.ts.map +1 -1
  29. package/dist/store.js +154 -32
  30. package/dist/sync.d.ts +7 -1
  31. package/dist/sync.d.ts.map +1 -1
  32. package/dist/sync.js +668 -35
  33. package/package.json +5 -1
  34. package/skills/oh/SKILL.md +42 -16
  35. package/spec/README.md +2 -2
  36. package/spec/v1/memory.md +134 -16
  37. package/spec/v1/storage.md +8 -5
  38. package/spec/v1/store.md +20 -0
  39. package/spec/v1/sync.md +77 -8
  40. package/src/cli.test.ts +53 -1
  41. package/src/cli.ts +34 -8
  42. package/src/errors.test.ts +87 -0
  43. package/src/errors.ts +185 -0
  44. package/src/graph.ts +2 -2
  45. package/src/libsql.test.ts +36 -0
  46. package/src/libsql.ts +26 -5
  47. package/src/memory.test.ts +1488 -18
  48. package/src/memory.ts +1199 -122
  49. package/src/operation.ts +13 -3
  50. package/src/sqlite/port.test.ts +209 -0
  51. package/src/sqlite/port.ts +118 -4
  52. package/src/sqlite/store.test.ts +106 -1
  53. package/src/sqlite/store.ts +168 -30
  54. package/src/store.test.ts +12 -0
  55. package/src/store.ts +30 -20
  56. package/src/sync.test.ts +570 -2
  57. package/src/sync.ts +586 -36
package/src/operation.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  safeCode,
9
9
  type Sha256Hex,
10
10
  } from "./canonical";
11
+ import { OhOperationSizeError } from "./errors";
11
12
  import { OH_CONTRACT_ID_V1 } from "./ontology";
12
13
  import { canonicalKnowledgeGraphChangesV1, OH_GRAPH_LIMITS_V1, type KnowledgeGraphChangeV1 } from "./graph";
13
14
 
@@ -58,12 +59,21 @@ function parsePayload(value: unknown): OhOperationPayloadV1 | null {
58
59
  : null;
59
60
  }
60
61
 
61
- export function createOhOperationV1(input: OhOperationPayloadV1): OhOperationV1 {
62
+ export function createOhOperationV1(
63
+ input: OhOperationPayloadV1,
64
+ options: Readonly<{ maximumOperationBytes?: number }> = {},
65
+ ): OhOperationV1 {
66
+ const maximumOperationBytes = options.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
67
+ if (!Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1
68
+ || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
69
+ throw new TypeError("Invalid Oh operation byte bound.");
70
+ }
62
71
  const payload = parsePayload(input);
63
72
  if (payload === null) throw new TypeError("Invalid Oh operation payload.");
64
73
  const operation = { ...payload, operationSha256: canonicalSha256(payload) };
65
- if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) {
66
- throw new RangeError("Oh operation exceeds its canonical byte limit.");
74
+ const operationBytes = Buffer.byteLength(canonicalJson(operation), "utf8");
75
+ if (operationBytes > maximumOperationBytes) {
76
+ throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
67
77
  }
68
78
  return operation;
69
79
  }
@@ -1,16 +1,22 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
+ import { Database } from "bun:sqlite";
2
3
  import { mkdtemp, rm } from "node:fs/promises";
3
4
  import { join } from "node:path";
4
5
  import { tmpdir } from "node:os";
5
6
 
7
+ import { canonicalSha256 } from "../canonical";
6
8
  import { createKnowledgeGraphRecordV1 } from "../graph";
9
+ import { createOhOperationV1 } from "../operation";
7
10
  import {
8
11
  createOhStoreBindingV1,
9
12
  OH_CANONICAL_STORE_PROFILE_V1,
10
13
  OH_WORKING_STORE_PROFILE_V1,
14
+ OhConflictError,
11
15
  OhProfileError,
12
16
  OhPurgedSpaceError,
17
+ type OhHeadRefV1,
13
18
  } from "../store";
19
+ import { createOhSyncBundleV1 } from "../sync";
14
20
  import { createOhSqliteStoreAuthorityV1 } from "./port";
15
21
  import { OhSqliteStore } from "./store";
16
22
 
@@ -115,12 +121,215 @@ describe("promise-based SQLite store port", () => {
115
121
  await authority.store.close();
116
122
  });
117
123
 
124
+ test("keeps pinned canonical replication on host control", async () => {
125
+ const authority = createOhSqliteStoreAuthorityV1({ path: ":memory:",
126
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:replication", spaceId: "replication" });
127
+ expect("replication" in authority.store).toBe(false);
128
+ const replication = authority.host.replication;
129
+ expect(replication).not.toBeNull();
130
+ if (replication === null) throw new Error("Expected canonical replication authority.");
131
+ expect(Object.isFrozen(replication)).toBe(true);
132
+ const empty = await authority.store.head();
133
+ const first = await authority.store.commit({ actorId: "host.test", changes: [{ kind: "put",
134
+ record: entity("entity:first", "First"), v: 1 }], expectedHead: empty,
135
+ instant: "2026-09-06T01:00:00.000Z", operationId: "op_first" });
136
+ const second = await authority.store.commit({ actorId: "host.test", changes: [{ kind: "put",
137
+ record: entity("entity:second", "Second"), v: 1 }], expectedHead: await authority.store.head(),
138
+ instant: "2026-09-06T01:01:00.000Z", operationId: "op_second" });
139
+ const pinned = await authority.store.head();
140
+ await authority.store.commit({ actorId: "host.test", changes: [{ kind: "put",
141
+ record: entity("entity:third", "Third"), v: 1 }], expectedHead: pinned,
142
+ instant: "2026-09-06T01:02:00.000Z", operationId: "op_third" });
143
+
144
+ const firstPage = await replication.exportBundle({
145
+ after: { operationSha256: empty.operationSha256, sequence: empty.sequence },
146
+ limit: 1,
147
+ through: { operationSha256: pinned.operationSha256, sequence: pinned.sequence },
148
+ });
149
+ expect(firstPage.bundle.operations.map(({ operationId }) => operationId)).toEqual(["op_first"]);
150
+ expect(firstPage).toMatchObject({
151
+ from: { operationSha256: null, sequence: 0 },
152
+ hasMore: true,
153
+ through: pinned,
154
+ to: { operationSha256: first.operationSha256, sequence: first.sequence },
155
+ v: 1,
156
+ });
157
+ const finalPage = await replication.exportBundle({
158
+ after: firstPage.to,
159
+ limit: 1,
160
+ through: { operationSha256: pinned.operationSha256, sequence: pinned.sequence },
161
+ });
162
+ expect(finalPage.bundle.operations.map(({ operationId }) => operationId)).toEqual(["op_second"]);
163
+ expect(finalPage).toMatchObject({
164
+ from: { operationSha256: first.operationSha256, sequence: first.sequence },
165
+ hasMore: false,
166
+ through: pinned,
167
+ to: { operationSha256: second.operationSha256, sequence: second.sequence },
168
+ v: 1,
169
+ });
170
+
171
+ const bundle = await replication.exportBundle({
172
+ after: { operationSha256: empty.operationSha256, sequence: empty.sequence },
173
+ through: { operationSha256: pinned.operationSha256, sequence: pinned.sequence },
174
+ });
175
+ expect(bundle.bundle.operations.map(({ operationId }) => operationId)).toEqual(["op_first", "op_second"]);
176
+ expect(bundle.bundle.operations.at(-1)?.operationSha256).toBe(second.operationSha256);
177
+ expect(bundle.bundle.operations).not.toContainEqual(expect.objectContaining({ operationId: "op_third" }));
178
+ expect(bundle).toMatchObject({ from: { sequence: 0 }, hasMore: false,
179
+ through: pinned, to: { sequence: 2 }, v: 1 });
180
+ await expect(replication.exportBundle({
181
+ after: { operationSha256: first.operationSha256, sequence: first.sequence },
182
+ through: { operationSha256: canonicalSha256("wrong through head"), sequence: pinned.sequence },
183
+ })).rejects.toThrow();
184
+
185
+ const working = createOhSqliteStoreAuthorityV1({ path: ":memory:",
186
+ profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:working-replication",
187
+ spaceId: "working-replication" });
188
+ expect(working.host.replication).toBeNull();
189
+ await working.store.close();
190
+ await authority.store.close();
191
+ });
192
+
193
+ test("imports canonical replication bundles atomically and replays them exactly", async () => {
194
+ const source = createOhSqliteStoreAuthorityV1({ path: ":memory:",
195
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:atomic", spaceId: "atomic" });
196
+ const targetDatabase = new Database(":memory:", { strict: true });
197
+ const target = createOhSqliteStoreAuthorityV1({ database: targetDatabase,
198
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:atomic", spaceId: "atomic" });
199
+ const empty = await target.store.head();
200
+ const first = await source.store.commit({ actorId: "host.test", changes: [{ kind: "put",
201
+ record: entity("entity:first", "First"), v: 1 }], expectedHead: await source.store.head(),
202
+ instant: "2026-09-06T02:00:00.000Z", operationId: "op_atomic_first" });
203
+ const second = await source.store.commit({ actorId: "host.test", changes: [{ kind: "put",
204
+ record: entity("entity:second", "Second"), v: 1 }], expectedHead: await source.store.head(),
205
+ instant: "2026-09-06T02:01:00.000Z", operationId: "op_atomic_second" });
206
+ const { operationSha256: _operationSha256, ...secondPayload } = second;
207
+ const hostileSecond = createOhOperationV1({
208
+ ...secondPayload,
209
+ graphRevisionSha256: canonicalSha256("hostile graph revision"),
210
+ recordsSha256: canonicalSha256("hostile records"),
211
+ });
212
+ const hostile = createOhSyncBundleV1("atomic", [first, hostileSecond]);
213
+ const targetReplication = target.host.replication;
214
+ const sourceReplication = source.host.replication;
215
+ if (targetReplication === null || sourceReplication === null) {
216
+ throw new Error("Expected canonical replication authority.");
217
+ }
218
+ const firstBundle = createOhSyncBundleV1("atomic", [first]);
219
+ await expect(targetReplication.importBundle({
220
+ bundle: firstBundle,
221
+ expectedHead: { operationSha256: null, sequence: -0 },
222
+ })).rejects.toThrow(TypeError);
223
+ let rejectedBundleReads = 0;
224
+ const rejectedBundle = new Proxy({}, {
225
+ get() { rejectedBundleReads += 1; throw new Error("must not read bundle"); },
226
+ getOwnPropertyDescriptor() { rejectedBundleReads += 1; throw new Error("must not inspect bundle"); },
227
+ getPrototypeOf() { rejectedBundleReads += 1; throw new Error("must not inspect bundle"); },
228
+ ownKeys() { rejectedBundleReads += 1; throw new Error("must not inspect bundle"); },
229
+ });
230
+ await expect(targetReplication.importBundle({
231
+ bundle: rejectedBundle,
232
+ expectedHead: { operationSha256: null, sequence: -0 },
233
+ })).rejects.toThrow(TypeError);
234
+ expect(rejectedBundleReads).toBe(0);
235
+ let expectedHeadAccessorReads = 0;
236
+ const accessorExpectedHead = { operationSha256: null } as Record<PropertyKey, unknown>;
237
+ Object.defineProperty(accessorExpectedHead, "sequence", { enumerable: true,
238
+ get() { expectedHeadAccessorReads += 1; throw new Error("must not execute"); } });
239
+ await expect(targetReplication.importBundle({
240
+ bundle: firstBundle,
241
+ expectedHead: accessorExpectedHead as OhHeadRefV1,
242
+ })).rejects.toThrow(TypeError);
243
+ let expectedHeadProxyReads = 0;
244
+ const proxyExpectedHead = new Proxy({ operationSha256: null, sequence: 0 } as const, {
245
+ get() { expectedHeadProxyReads += 1; throw new Error("must not execute"); },
246
+ });
247
+ await expect(targetReplication.importBundle({
248
+ bundle: firstBundle,
249
+ expectedHead: proxyExpectedHead,
250
+ })).rejects.toThrow(TypeError);
251
+ expect(expectedHeadAccessorReads).toBe(0);
252
+ expect(expectedHeadProxyReads).toBe(0);
253
+ expect(await target.store.head()).toEqual(empty);
254
+ await expect(targetReplication.importBundle({
255
+ bundle: hostile,
256
+ expectedHead: { operationSha256: empty.operationSha256, sequence: empty.sequence },
257
+ })).rejects.toThrow("does not reproduce");
258
+ expect(await target.store.head()).toEqual(empty);
259
+ expect((await target.store.snapshot()).records).toEqual([]);
260
+ expect(await target.store.verify()).toMatchObject({ operations: 0, records: 0 });
261
+ for (const table of ["oh_operations", "oh_operation_records", "oh_records", "oh_dependencies",
262
+ "oh_search_documents", "oh_search_fts", "oh_sync_outbox"]) {
263
+ const count = targetDatabase.query<{ count: number }, []>(
264
+ `SELECT count(*) AS count FROM ${table}`,
265
+ ).get()?.count;
266
+ expect(count, table).toBe(0);
267
+ }
268
+
269
+ const sourceHead = await source.store.head();
270
+ const exact = await sourceReplication.exportBundle({
271
+ after: { operationSha256: empty.operationSha256, sequence: empty.sequence },
272
+ through: { operationSha256: sourceHead.operationSha256, sequence: sourceHead.sequence },
273
+ });
274
+ expect(await targetReplication.importBundle({
275
+ bundle: exact.bundle,
276
+ expectedHead: { operationSha256: empty.operationSha256, sequence: empty.sequence },
277
+ })).toMatchObject({ head: sourceHead, imported: 2, status: "imported" });
278
+ expect(await targetReplication.importBundle({
279
+ bundle: exact.bundle,
280
+ expectedHead: { operationSha256: empty.operationSha256, sequence: empty.sequence },
281
+ })).toMatchObject({ head: sourceHead, imported: 0, status: "already-present" });
282
+
283
+ const third = await target.store.commit({ actorId: "host.test", changes: [{ kind: "put",
284
+ record: entity("entity:third", "Third"), v: 1 }], expectedHead: await target.store.head(),
285
+ instant: "2026-09-06T02:02:00.000Z", operationId: "op_atomic_third" });
286
+ expect(await targetReplication.importBundle({
287
+ bundle: exact.bundle,
288
+ expectedHead: { operationSha256: empty.operationSha256, sequence: empty.sequence },
289
+ })).toMatchObject({ head: { operationSha256: third.operationSha256, sequence: third.sequence },
290
+ imported: 0, status: "already-present" });
291
+
292
+ const fork = createOhSqliteStoreAuthorityV1({ path: ":memory:",
293
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:atomic", spaceId: "atomic" });
294
+ const forkReplication = fork.host.replication;
295
+ if (forkReplication === null) throw new Error("Expected fork replication authority.");
296
+ await forkReplication.importBundle({
297
+ bundle: createOhSyncBundleV1("atomic", [first]),
298
+ expectedHead: { operationSha256: empty.operationSha256, sequence: empty.sequence },
299
+ });
300
+ const divergent = await fork.store.commit({ actorId: "host.fork", changes: [{ kind: "put",
301
+ record: entity("entity:fork", "Fork"), v: 1 }], expectedHead: await fork.store.head(),
302
+ instant: "2026-09-06T02:01:30.000Z", operationId: "op_atomic_fork" });
303
+ await expect(targetReplication.importBundle({
304
+ bundle: createOhSyncBundleV1("atomic", [divergent]),
305
+ expectedHead: { operationSha256: first.operationSha256, sequence: first.sequence },
306
+ })).rejects.toThrow(OhConflictError);
307
+ expect(await target.store.head()).toMatchObject({
308
+ operationSha256: third.operationSha256,
309
+ sequence: third.sequence,
310
+ });
311
+ const history = await target.store.changesSince({ operationSha256: null, sequence: 0 }, {
312
+ through: { operationSha256: third.operationSha256, sequence: third.sequence },
313
+ });
314
+ expect(history.operations.map(({ operationId }) => operationId))
315
+ .toEqual(["op_atomic_first", "op_atomic_second", "op_atomic_third"]);
316
+ expect(await target.store.verify()).toMatchObject({ operations: 3, records: 3 });
317
+
318
+ await fork.store.close();
319
+ await target.store.close();
320
+ await source.store.close();
321
+ });
322
+
118
323
  test("refuses operation replication for a bound working profile", () => {
119
324
  const store = new OhSqliteStore({ path: ":memory:", spaceId: "local-only" });
120
325
  store.bind(createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1,
121
326
  realmId: "realm:local-only", spaceId: "local-only", v: 1 }));
122
327
  expect(() => store.exportOperations()).toThrow(OhProfileError);
123
328
  expect(() => store.importOperation({})).toThrow(OhProfileError);
329
+ expect(() => store.importOperations({
330
+ expectedHead: { operationSha256: null, sequence: 0 },
331
+ operations: [],
332
+ })).toThrow(OhProfileError);
124
333
  expect(store.verifyReplay()).toMatchObject({ operations: 0, records: 0 });
125
334
  store.close();
126
335
  });
@@ -1,3 +1,5 @@
1
+ import { isProxy } from "node:util/types";
2
+
1
3
  import { canonicalJson } from "../canonical";
2
4
  import {
3
5
  createOhStoreBindingV1,
@@ -11,7 +13,6 @@ import {
11
13
  type OhHeadV1,
12
14
  type OhSnapshotV1,
13
15
  type OhSpacePurgeReceiptV1,
14
- type OhStoreAuthorityV1,
15
16
  type OhStoreBindingV1,
16
17
  type OhStoreHostControlV1,
17
18
  type OhStoreProfileV1,
@@ -19,8 +20,17 @@ import {
19
20
  type OhStoreVerificationV1,
20
21
  } from "../store";
21
22
  import type { OhOperationV1 } from "../operation";
23
+ import {
24
+ createOhSyncBundleV1,
25
+ parseOhSyncBundleV1,
26
+ parseOhSyncHeadRefV1,
27
+ type OhSyncBundleV1,
28
+ } from "../sync";
22
29
  import type { OhSqliteDatabase } from "./driver";
23
- import { OhSqliteStore } from "./store";
30
+ import {
31
+ OhSqliteStore,
32
+ type OhOperationImportResultV1,
33
+ } from "./store";
24
34
 
25
35
  export type OhSqliteStoreAuthorityOptionsV1 = Readonly<{
26
36
  database?: OhSqliteDatabase;
@@ -30,6 +40,59 @@ export type OhSqliteStoreAuthorityOptionsV1 = Readonly<{
30
40
  spaceId?: string;
31
41
  }>;
32
42
 
43
+ export interface OhSqliteCanonicalReplicationV1 {
44
+ readonly binding: OhStoreBindingV1;
45
+ exportBundle(input: Readonly<{
46
+ after: OhHeadRefV1;
47
+ limit?: number;
48
+ through: OhHeadRefV1;
49
+ }>): Promise<Readonly<{
50
+ bundle: OhSyncBundleV1;
51
+ from: OhChangesPageV1["from"];
52
+ hasMore: boolean;
53
+ through: OhChangesPageV1["through"];
54
+ to: OhChangesPageV1["to"];
55
+ v: 1;
56
+ }>>;
57
+ head(): Promise<OhHeadV1>;
58
+ importBundle(input: Readonly<{
59
+ bundle: unknown;
60
+ expectedHead: OhHeadRefV1;
61
+ }>): Promise<OhOperationImportResultV1>;
62
+ }
63
+
64
+ export interface OhSqliteStoreHostControlV1 extends OhStoreHostControlV1 {
65
+ readonly replication: OhSqliteCanonicalReplicationV1 | null;
66
+ }
67
+
68
+ export type OhSqliteStoreAuthorityV1 = Readonly<{
69
+ host: OhSqliteStoreHostControlV1;
70
+ store: OhStoreV1;
71
+ }>;
72
+
73
+ function exactReplicationImportInputV1(value: unknown): Readonly<{
74
+ bundle: unknown;
75
+ expectedHead: unknown;
76
+ }> | null {
77
+ try {
78
+ if (typeof value !== "object" || value === null || Array.isArray(value) || isProxy(value)) return null;
79
+ const prototype = Object.getPrototypeOf(value);
80
+ const keys = Reflect.ownKeys(value);
81
+ if ((prototype !== Object.prototype && prototype !== null)
82
+ || keys.length !== 2 || !keys.includes("bundle") || !keys.includes("expectedHead")
83
+ || keys.some((key) => typeof key !== "string")) return null;
84
+ const bundle = Object.getOwnPropertyDescriptor(value, "bundle");
85
+ const expectedHead = Object.getOwnPropertyDescriptor(value, "expectedHead");
86
+ if (bundle === undefined || expectedHead === undefined
87
+ || !bundle.enumerable || !expectedHead.enumerable
88
+ || bundle.get !== undefined || bundle.set !== undefined
89
+ || expectedHead.get !== undefined || expectedHead.set !== undefined) return null;
90
+ return { bundle: bundle.value, expectedHead: expectedHead.value };
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
33
96
  export class OhSqliteStorePortV1 implements OhStoreV1 {
34
97
  readonly binding: OhStoreBindingV1;
35
98
  readonly #authority: OhSqliteStore;
@@ -91,7 +154,7 @@ export class OhSqliteStorePortV1 implements OhStoreV1 {
91
154
  */
92
155
  export function createOhSqliteStoreAuthorityV1(
93
156
  options: OhSqliteStoreAuthorityOptionsV1 = {},
94
- ): OhStoreAuthorityV1 {
157
+ ): OhSqliteStoreAuthorityV1 {
95
158
  const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1);
96
159
  if (profile === null) throw new TypeError("Invalid SQLite store profile.");
97
160
  const spaceId = options.spaceId ?? "default";
@@ -104,7 +167,57 @@ export function createOhSqliteStoreAuthorityV1(
104
167
  });
105
168
  const store = new OhSqliteStorePortV1(authority, binding);
106
169
  let purge: OhSpacePurgeReceiptV1 | null = null;
107
- const host: OhStoreHostControlV1 = Object.freeze({
170
+ const replication: OhSqliteCanonicalReplicationV1 | null =
171
+ profile.capabilities.operationReplication
172
+ ? Object.freeze({
173
+ binding,
174
+ exportBundle: async (input: Readonly<{
175
+ after: OhHeadRefV1;
176
+ limit?: number;
177
+ through: OhHeadRefV1;
178
+ }>) => {
179
+ const page = authority.changesSince(input.after, {
180
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
181
+ through: input.through,
182
+ });
183
+ const bundle = createOhSyncBundleV1(binding.spaceId, page.operations, {
184
+ largestFittingPrefix: true,
185
+ });
186
+ const last = bundle.operations.at(-1);
187
+ return Object.freeze({
188
+ bundle,
189
+ from: page.from,
190
+ hasMore: page.hasMore || bundle.operations.length < page.operations.length,
191
+ through: page.through,
192
+ to: last === undefined ? page.from : {
193
+ operationSha256: last.operationSha256,
194
+ sequence: last.sequence,
195
+ },
196
+ v: 1 as const,
197
+ });
198
+ },
199
+ head: async () => authority.head(),
200
+ importBundle: async (input: Readonly<{
201
+ bundle: unknown;
202
+ expectedHead: OhHeadRefV1;
203
+ }>) => {
204
+ const request = exactReplicationImportInputV1(input);
205
+ const expectedHead = request === null ? null : parseOhSyncHeadRefV1(request.expectedHead);
206
+ if (request === null || expectedHead === null) {
207
+ throw new TypeError("Invalid canonical replication request.");
208
+ }
209
+ const bundle = parseOhSyncBundleV1(request.bundle);
210
+ if (bundle === null || bundle.spaceId !== binding.spaceId) {
211
+ throw new TypeError("Invalid canonical replication bundle.");
212
+ }
213
+ return authority.importOperations({
214
+ expectedHead,
215
+ operations: bundle.operations,
216
+ });
217
+ },
218
+ })
219
+ : null;
220
+ const host: OhSqliteStoreHostControlV1 = Object.freeze({
108
221
  binding,
109
222
  purgeWorkingSpace: async (input: Readonly<{ purgedAt?: string }>) => {
110
223
  if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) {
@@ -115,6 +228,7 @@ export function createOhSqliteStoreAuthorityV1(
115
228
  authority.close();
116
229
  return purge;
117
230
  },
231
+ replication,
118
232
  });
119
233
  return Object.freeze({ host, store });
120
234
  }
@@ -3,9 +3,12 @@ import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
 
6
+ import { canonicalJson, canonicalSha256 } from "../canonical";
6
7
  import { createKnowledgeGraphRecordV1 } from "../graph";
8
+ import { createOhOperationV1 } from "../operation";
7
9
  import { createOhStoreBindingV1, OH_WORKING_STORE_PROFILE_V1 } from "../store";
8
- import { OhConflictError, OhDependencyError, OhIntegrityError, OhSqliteStore } from "./store";
10
+ import { OhConflictError, OhDependencyError, OhIntegrityError, OhOperationSizeError,
11
+ OhSqliteStore } from "./store";
9
12
 
10
13
  const roots: string[] = [];
11
14
  afterEach(async () => {
@@ -64,6 +67,40 @@ describe("Oh SQLite authority", () => {
64
67
  store.close();
65
68
  });
66
69
 
70
+ test("uses a distinct precommit error for host-declared operation byte bounds", () => {
71
+ const store = new OhSqliteStore({ path: ":memory:" });
72
+ const empty = store.head();
73
+ const changes = [{ kind: "put" as const,
74
+ record: record("entity:bounded", "Bounded"), v: 1 as const }];
75
+ const first = store.commit({ actorId: "agent.test", changes, expectedHead: empty,
76
+ operationId: "op_bounded_first" });
77
+ expect(() => store.commit({ actorId: "agent.test", changes, expectedHead: empty,
78
+ maximumOperationBytes: 1, operationId: "op_bounded_first" }))
79
+ .toThrow(OhOperationSizeError);
80
+ expect(() => store.commit({ actorId: "agent.test", changes: [{ kind: "put",
81
+ record: record("entity:rejected", "Rejected"), v: 1 }], expectedHead: store.head(),
82
+ maximumOperationBytes: 1, operationId: "op_bounded_rejected" }))
83
+ .toThrow(OhOperationSizeError);
84
+ expect(new OhOperationSizeError(2, 1)).toBeInstanceOf(RangeError);
85
+ expect(store.head()).toMatchObject({ operationSha256: first.operationSha256, sequence: 1 });
86
+ expect(store.exportOperations().map(({ operationId }) => operationId)).toEqual(["op_bounded_first"]);
87
+ expect(store.snapshotRecords().map(({ key }) => key)).toEqual(["entity:bounded"]);
88
+ store.close();
89
+ });
90
+
91
+ test("rejects copied size-error fields without a native branded error", () => {
92
+ const copied = Object.create(RangeError.prototype) as Record<PropertyKey, unknown>;
93
+ Object.defineProperties(copied, {
94
+ [Symbol.for("@hraness/oh/OhOperationSizeError/v1")]: {
95
+ configurable: false, value: true, writable: false,
96
+ },
97
+ code: { configurable: false, value: "oh.operation-size.v1", writable: false },
98
+ maximumOperationBytes: { configurable: false, value: 1, writable: false },
99
+ operationBytes: { configurable: false, value: 2, writable: false },
100
+ });
101
+ expect(copied instanceof OhOperationSizeError).toBe(false);
102
+ });
103
+
67
104
  test("rejects orphaned and cross-space rows as idempotent operations", () => {
68
105
  const changes = [{ kind: "put" as const, record: record("entity:orphan", "Orphan"), v: 1 as const }];
69
106
  const source = new OhSqliteStore({ path: ":memory:", spaceId: "orphan-target" });
@@ -181,6 +218,74 @@ describe("Oh SQLite authority", () => {
181
218
  store.close();
182
219
  });
183
220
 
221
+ test("verifies foreign keys and exact search materializations", () => {
222
+ const store = new OhSqliteStore({ path: ":memory:", spaceId: "search-integrity" });
223
+ store.commit({ actorId: "agent.test", changes: [{ kind: "put",
224
+ record: record("entity:a", "A"), v: 1 }], expectedHead: store.head(), operationId: "op_search" });
225
+ const document = store.database.query<{ text: string }, [string, string]>(
226
+ "SELECT text FROM oh_search_documents WHERE space_id = ? AND record_key = ?",
227
+ ).get(store.spaceId, "entity:a");
228
+ if (document === null) throw new Error("Expected materialized search document.");
229
+ store.database.query("UPDATE oh_search_documents SET text = ? WHERE space_id = ? AND record_key = ?")
230
+ .run("tampered", store.spaceId, "entity:a");
231
+ expect(() => store.verifyReplay()).toThrow("Materialized search documents");
232
+ store.database.query("UPDATE oh_search_documents SET text = ? WHERE space_id = ? AND record_key = ?")
233
+ .run(document.text, store.spaceId, "entity:a");
234
+ store.database.query(`INSERT INTO oh_search_fts(space_id, record_key, text)
235
+ SELECT space_id, record_key, text FROM oh_search_fts WHERE space_id = ? AND record_key = ?`)
236
+ .run(store.spaceId, "entity:a");
237
+ expect(() => store.verifyReplay()).toThrow("Materialized full-text search rows");
238
+ store.database.query("DELETE FROM oh_search_fts WHERE rowid = (SELECT max(rowid) FROM oh_search_fts)").run();
239
+ store.database.exec("PRAGMA foreign_keys = OFF");
240
+ store.database.query(`INSERT INTO oh_sync_state(remote_id, space_id, pulled_sequence,
241
+ pushed_sequence, remote_head_sha256, updated_at) VALUES (?, ?, 0, 0, NULL, ?)`)
242
+ .run("remote.alien", "alien-space", "2026-09-06T12:00:00.000Z");
243
+ store.database.exec("PRAGMA foreign_keys = ON");
244
+ expect(() => store.verifyReplay()).toThrow("foreign_key_check");
245
+ store.close();
246
+ });
247
+
248
+ test("rejects an exact replay prefix disconnected from the current authority head", () => {
249
+ const store = new OhSqliteStore({ path: ":memory:", spaceId: "replay-tail-integrity" });
250
+ const operations = [1, 2, 3].map((index) => store.commit({ actorId: "agent.test",
251
+ changes: [{ kind: "put", record: record(`entity:${index}`, `Entity ${index}`), v: 1 }],
252
+ expectedHead: store.head(), instant: `2026-09-06T12:0${index}:00.000Z`,
253
+ operationId: `op_tail_${index}` }));
254
+ const third = operations[2]!;
255
+ const { operationSha256: _operationSha256, ...thirdPayload } = third;
256
+ const disconnected = createOhOperationV1({
257
+ ...thirdPayload,
258
+ parentOperationSha256: canonicalSha256("disconnected parent"),
259
+ });
260
+ store.database.query(`UPDATE oh_operations SET operation_json = ?
261
+ WHERE space_id = ? AND sequence = 3`).run(canonicalJson(disconnected), store.spaceId);
262
+ expect(() => store.importOperations({
263
+ expectedHead: { operationSha256: null, sequence: 0 },
264
+ operations: operations.slice(0, 2),
265
+ })).toThrow(OhIntegrityError);
266
+ store.database.query(`UPDATE oh_operations SET operation_json = ?
267
+ WHERE space_id = ? AND sequence = 3`).run(canonicalJson(third), store.spaceId);
268
+ store.database.exec("PRAGMA foreign_keys = OFF");
269
+ store.database.query(`UPDATE oh_operation_records SET operation_sha256 = ?
270
+ WHERE operation_sha256 = ?`).run(disconnected.operationSha256, third.operationSha256);
271
+ store.database.query(`UPDATE oh_records SET operation_sha256 = ?
272
+ WHERE operation_sha256 = ?`).run(disconnected.operationSha256, third.operationSha256);
273
+ store.database.query(`UPDATE oh_sync_outbox SET operation_sha256 = ?
274
+ WHERE operation_sha256 = ?`).run(disconnected.operationSha256, third.operationSha256);
275
+ store.database.query(`UPDATE oh_operations SET operation_sha256 = ?, parent_operation_sha256 = ?,
276
+ operation_json = ? WHERE space_id = ? AND sequence = 3`).run(disconnected.operationSha256,
277
+ disconnected.parentOperationSha256, canonicalJson(disconnected), store.spaceId);
278
+ store.database.query("UPDATE oh_spaces SET head_operation_sha256 = ? WHERE space_id = ?")
279
+ .run(disconnected.operationSha256, store.spaceId);
280
+ store.database.exec("PRAGMA foreign_keys = ON");
281
+ expect(store.database.query<Record<string, unknown>, []>("PRAGMA foreign_key_check").all()).toEqual([]);
282
+ expect(() => store.importOperations({
283
+ expectedHead: { operationSha256: null, sequence: 0 },
284
+ operations: operations.slice(0, 2),
285
+ })).toThrow(OhIntegrityError);
286
+ store.close();
287
+ });
288
+
184
289
  test("verifies duplicated operation columns against canonical envelopes", () => {
185
290
  const store = new OhSqliteStore({ path: ":memory:" });
186
291
  store.commit({ actorId: "agent.test", changes: [{ kind: "put",