@hraness/oh 0.2.3

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +598 -0
  3. package/dist/canonical.d.ts +32 -0
  4. package/dist/canonical.d.ts.map +1 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +3419 -0
  8. package/dist/contract.d.ts +33 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/graph.d.ts +67 -0
  11. package/dist/graph.d.ts.map +1 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +1988 -0
  15. package/dist/libsql.d.ts +57 -0
  16. package/dist/libsql.d.ts.map +1 -0
  17. package/dist/libsql.js +2662 -0
  18. package/dist/memory.d.ts +366 -0
  19. package/dist/memory.d.ts.map +1 -0
  20. package/dist/memory.js +3650 -0
  21. package/dist/ontology.d.ts +242 -0
  22. package/dist/ontology.d.ts.map +1 -0
  23. package/dist/operation.d.ts +24 -0
  24. package/dist/operation.d.ts.map +1 -0
  25. package/dist/projection-public.d.ts +59 -0
  26. package/dist/projection-public.d.ts.map +1 -0
  27. package/dist/projection-public.js +1682 -0
  28. package/dist/projection-suss.d.ts +17 -0
  29. package/dist/projection-suss.d.ts.map +1 -0
  30. package/dist/projection-suss.js +1721 -0
  31. package/dist/projection.d.ts +315 -0
  32. package/dist/projection.d.ts.map +1 -0
  33. package/dist/schema.d.ts +45 -0
  34. package/dist/schema.d.ts.map +1 -0
  35. package/dist/sdk.d.ts +51 -0
  36. package/dist/sdk.d.ts.map +1 -0
  37. package/dist/sdk.js +3072 -0
  38. package/dist/search.d.ts +34 -0
  39. package/dist/search.d.ts.map +1 -0
  40. package/dist/semantic.d.ts +83 -0
  41. package/dist/semantic.d.ts.map +1 -0
  42. package/dist/semantic.js +706 -0
  43. package/dist/sqlite/driver.d.ts +6 -0
  44. package/dist/sqlite/driver.d.ts.map +1 -0
  45. package/dist/sqlite/index.d.ts +5 -0
  46. package/dist/sqlite/index.d.ts.map +1 -0
  47. package/dist/sqlite/index.js +2840 -0
  48. package/dist/sqlite/migrations.d.ts +10 -0
  49. package/dist/sqlite/migrations.d.ts.map +1 -0
  50. package/dist/sqlite/port.d.ts +40 -0
  51. package/dist/sqlite/port.d.ts.map +1 -0
  52. package/dist/sqlite/runtime.d.ts +23 -0
  53. package/dist/sqlite/runtime.d.ts.map +1 -0
  54. package/dist/sqlite/store.d.ts +85 -0
  55. package/dist/sqlite/store.d.ts.map +1 -0
  56. package/dist/store.d.ts +236 -0
  57. package/dist/store.d.ts.map +1 -0
  58. package/dist/store.js +996 -0
  59. package/dist/sync.d.ts +57 -0
  60. package/dist/sync.d.ts.map +1 -0
  61. package/dist/sync.js +1328 -0
  62. package/package.json +121 -0
  63. package/skills/oh/SKILL.md +206 -0
  64. package/skills/oh/agents/openai.yaml +4 -0
  65. package/spec/README.md +74 -0
  66. package/spec/manifest.json +41 -0
  67. package/spec/v1/canonical-json.md +59 -0
  68. package/spec/v1/contract.json +28 -0
  69. package/spec/v1/contract.schema.json +58 -0
  70. package/spec/v1/embedding-profile.json +11 -0
  71. package/spec/v1/embedding.md +56 -0
  72. package/spec/v1/graph.md +87 -0
  73. package/spec/v1/memory.md +193 -0
  74. package/spec/v1/migration.md +92 -0
  75. package/spec/v1/ontology.json +55 -0
  76. package/spec/v1/ontology.md +80 -0
  77. package/spec/v1/operation.schema.json +138 -0
  78. package/spec/v1/projection-identity.schema.json +58 -0
  79. package/spec/v1/projection-query.schema.json +60 -0
  80. package/spec/v1/projection-result.schema.json +452 -0
  81. package/spec/v1/projection-rule-pack.schema.json +182 -0
  82. package/spec/v1/projection.md +165 -0
  83. package/spec/v1/record.schema.json +95 -0
  84. package/spec/v1/schema-evolution.md +51 -0
  85. package/spec/v1/schema-revision.schema.json +178 -0
  86. package/spec/v1/storage.md +88 -0
  87. package/spec/v1/store.md +131 -0
  88. package/spec/v1/sync-bundle.schema.json +51 -0
  89. package/spec/v1/sync.md +67 -0
  90. package/src/canonical.test.ts +46 -0
  91. package/src/canonical.ts +203 -0
  92. package/src/cli.test.ts +103 -0
  93. package/src/cli.ts +308 -0
  94. package/src/contract.ts +87 -0
  95. package/src/contracts.test.ts +147 -0
  96. package/src/graph.ts +248 -0
  97. package/src/index.ts +8 -0
  98. package/src/libsql.test.ts +657 -0
  99. package/src/libsql.ts +1687 -0
  100. package/src/memory.test.ts +783 -0
  101. package/src/memory.ts +1684 -0
  102. package/src/ontology.ts +573 -0
  103. package/src/operation.ts +80 -0
  104. package/src/projection-public.ts +53 -0
  105. package/src/projection-suss.ts +129 -0
  106. package/src/projection.test.ts +418 -0
  107. package/src/projection.ts +1457 -0
  108. package/src/schema.ts +156 -0
  109. package/src/sdk.ts +96 -0
  110. package/src/search.ts +66 -0
  111. package/src/semantic.test.ts +480 -0
  112. package/src/semantic.ts +333 -0
  113. package/src/sqlite/driver.ts +47 -0
  114. package/src/sqlite/index.ts +4 -0
  115. package/src/sqlite/migrations.test.ts +44 -0
  116. package/src/sqlite/migrations.ts +178 -0
  117. package/src/sqlite/port.test.ts +127 -0
  118. package/src/sqlite/port.ts +120 -0
  119. package/src/sqlite/runtime.test.ts +68 -0
  120. package/src/sqlite/runtime.ts +53 -0
  121. package/src/sqlite/store.test.ts +295 -0
  122. package/src/sqlite/store.ts +988 -0
  123. package/src/store.test.ts +121 -0
  124. package/src/store.ts +701 -0
  125. package/src/sync.test.ts +117 -0
  126. package/src/sync.ts +227 -0
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Database, type SQLQueryBindings } from "bun:sqlite";
3
+
4
+ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
5
+ import { createKnowledgeGraphRecordV1 } from "./graph";
6
+ import { OhSqliteStore } from "./sqlite/store";
7
+ import { createLibSqlOperationSyncTransportV1, createOhSyncBundleV1, parseOhSyncBundleV1,
8
+ synchronizeOhStoreV1, type LibSqlClientV1, type LibSqlStatementV1,
9
+ type LibSqlResultV1,
10
+ type OhOperationSyncTransportV1, type OhSyncBundleV1, type OhSyncHeadV1 } from "./sync";
11
+
12
+ const record = (key: string, name: string) => createKnowledgeGraphRecordV1({
13
+ dependencies: [], key, kind: "entity", v: 1, value: { name },
14
+ });
15
+
16
+ class MemoryTransport implements OhOperationSyncTransportV1 {
17
+ readonly store = new OhSqliteStore({ path: ":memory:" });
18
+ handshakes = 0;
19
+
20
+ async handshake(manifest: typeof OH_CONTRACT_MANIFEST_V1): Promise<void> {
21
+ if (manifest.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) throw new Error("contract mismatch");
22
+ this.handshakes += 1;
23
+ }
24
+ async head(spaceId: string): Promise<OhSyncHeadV1> {
25
+ if (spaceId !== this.store.spaceId) throw new Error("space mismatch");
26
+ const head = this.store.head();
27
+ return { operationSha256: head.operationSha256, sequence: head.sequence, v: 1 };
28
+ }
29
+ async pull(spaceId: string, afterSequence: number, limit: number): Promise<OhSyncBundleV1> {
30
+ return createOhSyncBundleV1(spaceId, this.store.exportOperations(afterSequence, limit));
31
+ }
32
+ async push(bundle: OhSyncBundleV1): Promise<OhSyncHeadV1> {
33
+ for (const operation of bundle.operations) this.store.importOperation(operation);
34
+ return await this.head(bundle.spaceId);
35
+ }
36
+ close(): void { this.store.close(); }
37
+ }
38
+
39
+ function put(store: OhSqliteStore, key: string, name: string, operationId: string): void {
40
+ store.commit({ actorId: "agent.test", changes: [{ kind: "put", record: record(key, name), v: 1 }],
41
+ expectedHead: store.head(), operationId });
42
+ }
43
+
44
+ describe("operation sync", () => {
45
+ test("round-trips fast-forward logs and settles idempotently", async () => {
46
+ const remote = new MemoryTransport();
47
+ const first = new OhSqliteStore({ path: ":memory:" });
48
+ const second = new OhSqliteStore({ path: ":memory:" });
49
+ put(first, "entity:a", "A", "op_a");
50
+ put(first, "entity:b", "B", "op_b");
51
+ expect(await synchronizeOhStoreV1(first, remote)).toMatchObject({ pulled: 0, pushed: 2 });
52
+ expect(await synchronizeOhStoreV1(second, remote)).toMatchObject({ pulled: 2, pushed: 0 });
53
+ expect(second.head().operationSha256).toBe(first.head().operationSha256);
54
+ put(second, "entity:c", "C", "op_c");
55
+ expect(await synchronizeOhStoreV1(second, remote)).toMatchObject({ pushed: 1 });
56
+ expect(await synchronizeOhStoreV1(first, remote)).toMatchObject({ pulled: 1 });
57
+ expect(await synchronizeOhStoreV1(first, remote)).toMatchObject({ pulled: 0, pushed: 0 });
58
+ expect(remote.handshakes).toBeGreaterThanOrEqual(5);
59
+ first.close(); second.close(); remote.close();
60
+ });
61
+
62
+ test("rejects diverged heads without modifying either log", async () => {
63
+ const remote = new MemoryTransport();
64
+ const local = new OhSqliteStore({ path: ":memory:" });
65
+ put(local, "entity:base", "Base", "op_base");
66
+ await synchronizeOhStoreV1(local, remote);
67
+ put(local, "entity:local", "Local", "op_local");
68
+ put(remote.store, "entity:remote", "Remote", "op_remote");
69
+ await expect(synchronizeOhStoreV1(local, remote)).rejects.toThrow("different heads");
70
+ expect(local.get("entity:local")).not.toBeNull();
71
+ expect(remote.store.get("entity:remote")).not.toBeNull();
72
+ local.close(); remote.close();
73
+ });
74
+
75
+ test("binds every bundle byte to its digest", () => {
76
+ const source = new OhSqliteStore({ path: ":memory:" });
77
+ put(source, "entity:a", "A", "op_a");
78
+ const bundle = createOhSyncBundleV1(source.spaceId, source.exportOperations());
79
+ expect(parseOhSyncBundleV1(bundle)).toEqual(bundle);
80
+ expect(parseOhSyncBundleV1({ ...bundle, spaceId: "other" })).toBeNull();
81
+ expect(parseOhSyncBundleV1({ ...bundle, contractSha256: "a".repeat(64) })).toBeNull();
82
+ source.close();
83
+ });
84
+
85
+ test("runs the libSQL/Turso seam against SQLite-compatible statements", async () => {
86
+ const database = new Database(":memory:", { strict: true });
87
+ const execute = (statement: LibSqlStatementV1 | string): LibSqlResultV1 => {
88
+ const sql = typeof statement === "string" ? statement : statement.sql;
89
+ const args = typeof statement === "string" ? [] : statement.args ?? [];
90
+ const bindings: SQLQueryBindings[] = args.map((value) => value instanceof Date
91
+ ? value.toISOString() : value instanceof ArrayBuffer ? new Uint8Array(value) : value);
92
+ if (/^\s*SELECT\b/iu.test(sql)) {
93
+ return { rows: database.query<Record<string, unknown>, SQLQueryBindings[]>(sql).all(...bindings) };
94
+ }
95
+ database.query<never, SQLQueryBindings[]>(sql).run(...bindings);
96
+ return { rows: [] };
97
+ };
98
+ const client: LibSqlClientV1 = {
99
+ execute: async (statement) => execute(statement),
100
+ batch: async (statements) => database.transaction((items: LibSqlStatementV1[]) =>
101
+ items.map((statement) => execute(statement)))(statements),
102
+ };
103
+ const transport = createLibSqlOperationSyncTransportV1(client);
104
+ const source = new OhSqliteStore({ path: ":memory:" });
105
+ put(source, "entity:a", "A", "op_a");
106
+ await expect(transport.handshake({ ...OH_CONTRACT_MANIFEST_V1,
107
+ ontologyVersion: "9.9.9" } as unknown as typeof OH_CONTRACT_MANIFEST_V1))
108
+ .rejects.toThrow("Unsupported contract manifest");
109
+ await transport.handshake(OH_CONTRACT_MANIFEST_V1);
110
+ const pushed = await transport.push(createOhSyncBundleV1(source.spaceId, source.exportOperations()));
111
+ expect(pushed).toEqual({ operationSha256: source.head().operationSha256, sequence: 1, v: 1 });
112
+ expect((await transport.pull(source.spaceId, 0, 100)).operations).toEqual(source.exportOperations());
113
+ expect(await transport.push(createOhSyncBundleV1(source.spaceId, source.exportOperations()))).toEqual(pushed);
114
+ source.close();
115
+ database.close();
116
+ });
117
+ });
package/src/sync.ts ADDED
@@ -0,0 +1,227 @@
1
+ import {
2
+ canonicalJson,
3
+ canonicalSha256,
4
+ hasExactKeys,
5
+ isPlainRecord,
6
+ parseSha256Hex,
7
+ safeCode,
8
+ type Sha256Hex,
9
+ } from "./canonical";
10
+ import { OH_CONTRACT_MANIFEST_V1, parseOhContractManifestV1, type OhContractManifestV1 } from "./contract";
11
+ import { parseOhOperationV1, type OhOperationV1 } from "./operation";
12
+ import type { OhSqliteStore } from "./sqlite/store";
13
+
14
+ export const OH_SYNC_PROTOCOL_V1 = "oh.sync.v1" as const;
15
+
16
+ export type OhSyncHeadV1 = Readonly<{
17
+ operationSha256: Sha256Hex | null;
18
+ sequence: number;
19
+ v: 1;
20
+ }>;
21
+
22
+ export type OhSyncBundleV1 = Readonly<{
23
+ bundleSha256: Sha256Hex;
24
+ contractSha256: Sha256Hex;
25
+ operations: readonly OhOperationV1[];
26
+ protocol: typeof OH_SYNC_PROTOCOL_V1;
27
+ spaceId: string;
28
+ v: 1;
29
+ }>;
30
+
31
+ export function createOhSyncBundleV1(spaceId: string, operations: readonly OhOperationV1[]): OhSyncBundleV1 {
32
+ const parsedSpaceId = safeCode(spaceId);
33
+ if (parsedSpaceId === null || operations.length > 1000) throw new TypeError("Invalid sync bundle.");
34
+ let priorSequence: number | null = null;
35
+ let priorSha256: Sha256Hex | null = null;
36
+ const parsed: OhOperationV1[] = [];
37
+ for (const candidate of operations) {
38
+ const operation = parseOhOperationV1(candidate);
39
+ if (operation === null || operation.spaceId !== parsedSpaceId
40
+ || (priorSequence !== null && operation.sequence !== priorSequence + 1)
41
+ || (priorSequence !== null && operation.parentOperationSha256 !== priorSha256)) {
42
+ throw new TypeError("Sync operations must form one ordered chain.");
43
+ }
44
+ parsed.push(operation);
45
+ priorSequence = operation.sequence;
46
+ priorSha256 = operation.operationSha256;
47
+ }
48
+ const payload = { contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256,
49
+ operations: parsed, protocol: OH_SYNC_PROTOCOL_V1, spaceId: parsedSpaceId, v: 1 as const };
50
+ return { ...payload, bundleSha256: canonicalSha256(payload) };
51
+ }
52
+
53
+ export function parseOhSyncBundleV1(value: unknown): OhSyncBundleV1 | null {
54
+ if (!isPlainRecord(value) || !hasExactKeys(value, ["bundleSha256", "contractSha256", "operations", "protocol", "spaceId", "v"])
55
+ || value.protocol !== OH_SYNC_PROTOCOL_V1 || value.v !== 1 || !Array.isArray(value.operations)) return null;
56
+ const bundleSha256 = parseSha256Hex(value.bundleSha256);
57
+ const contractSha256 = parseSha256Hex(value.contractSha256);
58
+ if (bundleSha256 === null || contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) return null;
59
+ try {
60
+ const created = createOhSyncBundleV1(value.spaceId as string, value.operations as OhOperationV1[]);
61
+ return created.bundleSha256 === bundleSha256 ? { ...created, bundleSha256 } : null;
62
+ } catch { return null; }
63
+ }
64
+
65
+ export interface OhOperationSyncTransportV1 {
66
+ handshake(manifest: OhContractManifestV1): Promise<void>;
67
+ head(spaceId: string): Promise<OhSyncHeadV1>;
68
+ pull(spaceId: string, afterSequence: number, limit: number): Promise<OhSyncBundleV1>;
69
+ push(bundle: OhSyncBundleV1): Promise<OhSyncHeadV1>;
70
+ }
71
+
72
+ export type OhSyncResultV1 = Readonly<{
73
+ head: OhSyncHeadV1;
74
+ pulled: number;
75
+ pushed: number;
76
+ rounds: number;
77
+ v: 1;
78
+ }>;
79
+
80
+ /**
81
+ * Reconciles only fast-forward histories. Concurrent heads fail closed and leave
82
+ * both logs intact for an explicit merge operation.
83
+ */
84
+ export async function synchronizeOhStoreV1(
85
+ store: OhSqliteStore,
86
+ transport: OhOperationSyncTransportV1,
87
+ options: Readonly<{ batchSize?: number; maximumRounds?: number; remoteId?: string }> = {},
88
+ ): Promise<OhSyncResultV1> {
89
+ const batchSize = options.batchSize ?? 100;
90
+ const maximumRounds = options.maximumRounds ?? 100;
91
+ const remoteId = safeCode(options.remoteId ?? "default");
92
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1000
93
+ || !Number.isSafeInteger(maximumRounds) || maximumRounds < 1 || maximumRounds > 10_000
94
+ || remoteId === null) throw new TypeError("Invalid sync options.");
95
+ await transport.handshake(OH_CONTRACT_MANIFEST_V1);
96
+ let pulled = 0;
97
+ let pushed = 0;
98
+ for (let round = 1; round <= maximumRounds; round += 1) {
99
+ const local = store.head();
100
+ const remote = await transport.head(store.spaceId);
101
+ if (local.sequence === remote.sequence) {
102
+ if (local.operationSha256 !== remote.operationSha256) {
103
+ throw new Error("Sync conflict: equal sequence numbers have different heads.");
104
+ }
105
+ store.updateSyncState(remoteId, { pulledSequence: local.sequence,
106
+ pushedSequence: local.sequence, remoteHeadSha256: remote.operationSha256 });
107
+ return { head: remote, pulled, pushed, rounds: round, v: 1 };
108
+ }
109
+ if (local.sequence < remote.sequence) {
110
+ const bundle = parseOhSyncBundleV1(await transport.pull(store.spaceId, local.sequence, batchSize));
111
+ if (bundle === null || bundle.operations.length === 0
112
+ || bundle.operations[0]?.sequence !== local.sequence + 1
113
+ || bundle.operations[0]?.parentOperationSha256 !== local.operationSha256) {
114
+ throw new Error("Sync conflict: remote history does not extend the local head.");
115
+ }
116
+ for (const operation of bundle.operations) {
117
+ store.importOperation(operation);
118
+ pulled += 1;
119
+ }
120
+ } else {
121
+ const operations = store.exportOperations(remote.sequence, batchSize);
122
+ if (operations.length === 0 || operations[0]?.sequence !== remote.sequence + 1
123
+ || operations[0]?.parentOperationSha256 !== remote.operationSha256) {
124
+ throw new Error("Sync conflict: local history does not extend the remote head.");
125
+ }
126
+ const head = await transport.push(createOhSyncBundleV1(store.spaceId, operations));
127
+ if (head.sequence !== operations.at(-1)?.sequence
128
+ || head.operationSha256 !== operations.at(-1)?.operationSha256) {
129
+ throw new Error("The sync transport acknowledged a different head.");
130
+ }
131
+ pushed += operations.length;
132
+ }
133
+ }
134
+ throw new Error("Sync did not settle within maximumRounds.");
135
+ }
136
+
137
+ export type LibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
138
+ export type LibSqlStatementV1 = { args?: LibSqlValueV1[]; sql: string };
139
+ export type LibSqlResultV1 = Readonly<{ rows: readonly (Readonly<Record<string, unknown>> | readonly unknown[])[] }>;
140
+ export interface LibSqlClientV1 {
141
+ execute(statement: LibSqlStatementV1 | string): Promise<LibSqlResultV1>;
142
+ batch(statements: LibSqlStatementV1[], mode?: "deferred" | "read" | "write"): Promise<readonly LibSqlResultV1[]>;
143
+ }
144
+
145
+ function rowValue(row: Readonly<Record<string, unknown>> | readonly unknown[], key: string, index: number): unknown {
146
+ return Array.isArray(row) ? row[index] : (row as Readonly<Record<string, unknown>>)[key];
147
+ }
148
+
149
+ /** A zero-dependency adapter for clients implementing @libsql/client's execute/batch shape. */
150
+ export function createLibSqlOperationSyncTransportV1(client: LibSqlClientV1): OhOperationSyncTransportV1 {
151
+ let ready: Promise<void> | null = null;
152
+ const setup = async (manifest: OhContractManifestV1): Promise<void> => {
153
+ if (parseOhContractManifestV1(manifest) === null) throw new Error("Unsupported contract manifest.");
154
+ await client.batch([
155
+ { sql: `CREATE TABLE IF NOT EXISTS oh_sync_contracts (
156
+ contract_id TEXT PRIMARY KEY, contract_sha256 TEXT NOT NULL, manifest_json TEXT NOT NULL
157
+ ) STRICT` },
158
+ { sql: `CREATE TABLE IF NOT EXISTS oh_sync_operations (
159
+ space_id TEXT NOT NULL, sequence INTEGER NOT NULL, operation_sha256 TEXT NOT NULL UNIQUE,
160
+ operation_json TEXT NOT NULL, PRIMARY KEY(space_id, sequence)
161
+ ) STRICT` },
162
+ { sql: "INSERT INTO oh_sync_contracts(contract_id, contract_sha256, manifest_json) VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING",
163
+ args: [manifest.contractId, manifest.contractSha256, canonicalJson(manifest)] },
164
+ ], "write");
165
+ const result = await client.execute({ sql: "SELECT contract_sha256, manifest_json FROM oh_sync_contracts WHERE contract_id = ?",
166
+ args: [manifest.contractId] });
167
+ const row = result.rows[0];
168
+ if (row === undefined || rowValue(row, "contract_sha256", 0) !== manifest.contractSha256
169
+ || rowValue(row, "manifest_json", 1) !== canonicalJson(manifest)) {
170
+ throw new Error("Remote contract manifest mismatch.");
171
+ }
172
+ };
173
+ const ensure = (manifest = OH_CONTRACT_MANIFEST_V1): Promise<void> => {
174
+ ready ??= setup(manifest).catch((error) => { ready = null; throw error; });
175
+ return ready;
176
+ };
177
+ const head = async (spaceId: string): Promise<OhSyncHeadV1> => {
178
+ await ensure();
179
+ const result = await client.execute({ sql: `SELECT sequence, operation_sha256 FROM oh_sync_operations
180
+ WHERE space_id = ? ORDER BY sequence DESC LIMIT 1`, args: [spaceId] });
181
+ const row = result.rows[0];
182
+ if (row === undefined) return { operationSha256: null, sequence: 0, v: 1 };
183
+ const sequence = Number(rowValue(row, "sequence", 0));
184
+ const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 1));
185
+ if (!Number.isSafeInteger(sequence) || sequence < 1 || operationSha256 === null) throw new Error("Invalid remote head.");
186
+ return { operationSha256, sequence, v: 1 };
187
+ };
188
+ return {
189
+ handshake: async (manifest) => {
190
+ const parsed = parseOhContractManifestV1(manifest);
191
+ if (parsed === null) throw new Error("Unsupported contract manifest.");
192
+ await ensure(parsed);
193
+ },
194
+ head,
195
+ pull: async (spaceId, afterSequence, limit) => {
196
+ await ensure();
197
+ const result = await client.execute({ sql: `SELECT operation_json FROM oh_sync_operations
198
+ WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`, args: [spaceId, afterSequence, limit] });
199
+ const operations = result.rows.map((row) => {
200
+ const json = rowValue(row, "operation_json", 0);
201
+ if (typeof json !== "string") throw new Error("Invalid remote operation JSON.");
202
+ const operation = parseOhOperationV1(JSON.parse(json));
203
+ if (operation === null || canonicalJson(operation) !== json) throw new Error("Invalid remote operation.");
204
+ return operation;
205
+ });
206
+ return createOhSyncBundleV1(spaceId, operations);
207
+ },
208
+ push: async (value) => {
209
+ await ensure();
210
+ const bundle = parseOhSyncBundleV1(value);
211
+ if (bundle === null) throw new Error("Invalid outgoing sync bundle.");
212
+ if (bundle.operations.length === 0) return head(bundle.spaceId);
213
+ const remote = await head(bundle.spaceId);
214
+ const first = bundle.operations[0] as OhOperationV1;
215
+ const last = bundle.operations.at(-1) as OhOperationV1;
216
+ if (remote.sequence === last.sequence && remote.operationSha256 === last.operationSha256) return remote;
217
+ if (first.sequence !== remote.sequence + 1 || first.parentOperationSha256 !== remote.operationSha256) {
218
+ throw new Error("Sync conflict: pushed history does not extend the remote head.");
219
+ }
220
+ await client.batch(bundle.operations.map((operation) => ({
221
+ sql: "INSERT INTO oh_sync_operations(space_id, sequence, operation_sha256, operation_json) VALUES (?, ?, ?, ?)",
222
+ args: [bundle.spaceId, operation.sequence, operation.operationSha256, canonicalJson(operation)],
223
+ })), "write");
224
+ return { operationSha256: last.operationSha256, sequence: last.sequence, v: 1 };
225
+ },
226
+ };
227
+ }