@hraness/oh 0.4.1 → 0.4.2

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 (92) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +4 -1
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +15590 -1705
  6. package/dist/index.js +15209 -261
  7. package/dist/libsql-model.d.ts +75 -0
  8. package/dist/libsql-model.d.ts.map +1 -0
  9. package/dist/libsql-platform.d.ts +24 -0
  10. package/dist/libsql-platform.d.ts.map +1 -0
  11. package/dist/libsql-program.d.ts +64 -0
  12. package/dist/libsql-program.d.ts.map +1 -0
  13. package/dist/libsql-runtime.d.ts +13 -0
  14. package/dist/libsql-runtime.d.ts.map +1 -0
  15. package/dist/libsql.d.ts +4 -36
  16. package/dist/libsql.d.ts.map +1 -1
  17. package/dist/libsql.js +16583 -1426
  18. package/dist/memory-authority-platform.d.ts +210 -0
  19. package/dist/memory-authority-platform.d.ts.map +1 -0
  20. package/dist/memory-authority-program.d.ts +15 -0
  21. package/dist/memory-authority-program.d.ts.map +1 -0
  22. package/dist/memory-authority-runtime.d.ts +4 -0
  23. package/dist/memory-authority-runtime.d.ts.map +1 -0
  24. package/dist/memory-core.d.ts +522 -0
  25. package/dist/memory-core.d.ts.map +1 -0
  26. package/dist/memory.d.ts +5 -445
  27. package/dist/memory.d.ts.map +1 -1
  28. package/dist/memory.js +18232 -4143
  29. package/dist/sdk.js +15266 -318
  30. package/dist/semantic-cloud.js +1 -260
  31. package/dist/semantic-model.d.ts +75 -0
  32. package/dist/semantic-model.d.ts.map +1 -0
  33. package/dist/semantic-platform.d.ts +32 -0
  34. package/dist/semantic-platform.d.ts.map +1 -0
  35. package/dist/semantic-program.d.ts +18 -0
  36. package/dist/semantic-program.d.ts.map +1 -0
  37. package/dist/semantic-runtime.d.ts +6 -0
  38. package/dist/semantic-runtime.d.ts.map +1 -0
  39. package/dist/semantic.d.ts +6 -57
  40. package/dist/semantic.d.ts.map +1 -1
  41. package/dist/semantic.js +15541 -514
  42. package/dist/sqlite/index.js +1148 -1378
  43. package/dist/sqlite/port.d.ts +1 -1
  44. package/dist/sqlite/port.d.ts.map +1 -1
  45. package/dist/sync-libsql-program.d.ts +5 -0
  46. package/dist/sync-libsql-program.d.ts.map +1 -0
  47. package/dist/sync-model.d.ts +60 -0
  48. package/dist/sync-model.d.ts.map +1 -0
  49. package/dist/sync-platform.d.ts +41 -0
  50. package/dist/sync-platform.d.ts.map +1 -0
  51. package/dist/sync-program.d.ts +11 -0
  52. package/dist/sync-program.d.ts.map +1 -0
  53. package/dist/sync-runtime.d.ts +6 -0
  54. package/dist/sync-runtime.d.ts.map +1 -0
  55. package/dist/sync.d.ts +5 -55
  56. package/dist/sync.d.ts.map +1 -1
  57. package/dist/sync.js +15174 -226
  58. package/package.json +7 -5
  59. package/skills/oh/SKILL.md +2 -1
  60. package/spec/v1/store.md +14 -0
  61. package/src/cli.test.ts +35 -0
  62. package/src/cli.ts +4 -3
  63. package/src/cloudflare-embedding.ts +1 -1
  64. package/src/libsql-lifecycle.test.ts +76 -0
  65. package/src/libsql-model.ts +350 -0
  66. package/src/libsql-platform.ts +51 -0
  67. package/src/libsql-program.ts +1583 -0
  68. package/src/libsql-runtime.ts +89 -0
  69. package/src/libsql-semantic-v2.ts +1 -1
  70. package/src/libsql-semantic.ts +1 -1
  71. package/src/libsql.test.ts +245 -0
  72. package/src/libsql.ts +26 -1698
  73. package/src/memory-authority-platform.ts +133 -0
  74. package/src/memory-authority-program.ts +309 -0
  75. package/src/memory-authority-runtime.ts +40 -0
  76. package/src/memory-core.ts +2401 -0
  77. package/src/memory.test.ts +55 -0
  78. package/src/memory.ts +68 -2759
  79. package/src/semantic-model.ts +142 -0
  80. package/src/semantic-platform.ts +130 -0
  81. package/src/semantic-program.ts +113 -0
  82. package/src/semantic-runtime.ts +53 -0
  83. package/src/semantic.test.ts +115 -1
  84. package/src/semantic.ts +14 -324
  85. package/src/sqlite/port.ts +1 -1
  86. package/src/sync-libsql-program.ts +170 -0
  87. package/src/sync-lifecycle.test.ts +110 -0
  88. package/src/sync-model.ts +530 -0
  89. package/src/sync-platform.ts +87 -0
  90. package/src/sync-program.ts +117 -0
  91. package/src/sync-runtime.ts +33 -0
  92. package/src/sync.ts +10 -765
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/oh",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "open-source tools for agentic research",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -101,10 +101,10 @@
101
101
  },
102
102
  "scripts": {
103
103
  "build": "bun run build:js && bun run build:portable && bun run build:types",
104
- "build:js": "bun build ./src/index.ts ./src/sdk.ts ./src/sqlite/index.ts ./src/sync.ts ./src/semantic.ts ./src/cli.ts --outdir ./dist --target bun --format esm --external bun:sqlite",
105
- "build:portable": "bun build ./src/store.ts ./src/libsql.ts ./src/projection-public.ts ./src/projection-suss.ts ./src/memory.ts ./src/memory-page.ts ./src/semantic-cloud.ts --outdir ./dist --target node --format esm --external @suss/datalog",
104
+ "build:js": "bun build ./src/index.ts ./src/sdk.ts ./src/sqlite/index.ts ./src/sync.ts --outdir ./dist --target bun --format esm --external bun:sqlite && bun build ./src/cli.ts --outdir ./dist --target bun --format esm --external bun:sqlite && bun build ./src/semantic.ts --outdir ./dist --target bun --format esm --external bun:sqlite",
105
+ "build:portable": "bun build ./src/store.ts ./src/projection-public.ts ./src/projection-suss.ts ./src/memory-page.ts ./src/semantic-cloud.ts --outdir ./dist --target node --format esm --external @suss/datalog && bun build ./src/libsql.ts --outdir ./dist --target node --format esm --external @suss/datalog && bun build ./src/memory.ts --outdir ./dist --target node --format esm --external @suss/datalog",
106
106
  "build:types": "tsc -p tsconfig.build.json",
107
- "check": "bun run typecheck && bun run typecheck:scripts && bun run test && bun run build && bun run test:types:node && bun run test:node && bun run test:node-projection && bun run test:package",
107
+ "check": "bun run typecheck && bun run typecheck:scripts && bun run check:effect && bun run test && bun run build && bun run test:types:node && bun run test:node && bun run test:node-projection && bun run test:package",
108
108
  "test:node": "node ./tests/node-portable.mjs",
109
109
  "test:types:node": "tsc -p tsconfig.node-portable.json --noEmit",
110
110
  "test:package": "bun run ./scripts/package-smoke.ts",
@@ -112,11 +112,13 @@
112
112
  "test": "bun test ./src ./tests ./site/tests/source.test.ts",
113
113
  "test:node-projection": "node --test ./scripts/projection-node.test.mjs",
114
114
  "typecheck": "tsc -p tsconfig.json --noEmit",
115
- "typecheck:scripts": "tsc -p tsconfig.scripts.json --noEmit"
115
+ "typecheck:scripts": "tsc -p tsconfig.scripts.json --noEmit",
116
+ "check:effect": "bun run ./scripts/check-effect-architecture.ts && bun test ./scripts/effect-architecture.test.ts"
116
117
  },
117
118
  "devDependencies": {
118
119
  "@suss/datalog": "0.20.0",
119
120
  "@types/bun": "1.3.14",
121
+ "effect": "3.22.1",
120
122
  "sigstore": "4.1.1",
121
123
  "typescript": "5.9.3"
122
124
  },
@@ -30,7 +30,8 @@ oh --help
30
30
  oh version
31
31
  ```
32
32
 
33
- The supported CLI is the exact npm release `@hraness/oh@0.4.1`. Its identical
33
+ This source tree prepares version `0.4.2`. Use the verified public CLI
34
+ `@hraness/oh@0.4.1` until the next release completes publication. Its identical
34
35
  tarball and checksum are mirrored by the immutable GitHub Release `v0.4.1`.
35
36
  It requires Bun 1.3.14 or newer. The versioned contract is published at
36
37
  <https://oh.computer/spec/>.
package/spec/v1/store.md CHANGED
@@ -149,3 +149,17 @@ Operation-record deletion resolves ownership through the canonical operation;
149
149
  the purge postcondition also rejects any global orphan or cross-space owner
150
150
  mismatch. Purge receipts are immutable and intentionally retain only binding,
151
151
  prior-head, and purge-event evidence.
152
+
153
+ ### Direct libSQL shutdown
154
+
155
+ Calling an authority store's `close()` immediately fences new calls on both its
156
+ store and host handles. Already admitted operations finish their native calls,
157
+ transaction result validation and reconciliation before close resolves. Concurrent
158
+ commits remain concurrent and are arbitrated by SQL compare-and-swap.
159
+
160
+ The client is borrowed unless `closeClient: true` was selected when opening the
161
+ authority. An owned client is closed once after the drain; repeated `close()` calls
162
+ observe that same result. A failed operation and a failed authority close reject
163
+ their respective Promises with their original values. The standalone
164
+ `purgeOhLibSqlWorkingSpaceV1` helper retains its `finally` contract: when both purge
165
+ and its requested client close fail, the close failure is its rejection value.
package/src/cli.test.ts CHANGED
@@ -24,6 +24,41 @@ async function run(arguments_: readonly string[], cwd = REPOSITORY_ROOT): Promis
24
24
  }
25
25
 
26
26
  describe("oh CLI", () => {
27
+ test("loads the SDK only after a store command passes validation", async () => {
28
+ const root = await mkdtemp(join(tmpdir(), "oh-cli-lazy-test-"));
29
+ roots.push(root);
30
+ const probe = `
31
+ import { createRequire } from "node:module";
32
+ import { dirname, sep } from "node:path";
33
+ const require = createRequire(${JSON.stringify(CLI_PATH)});
34
+ const effectRoot = dirname(require.resolve("effect/package.json")) + sep;
35
+ const loaded = () => Object.keys(require.cache).some((path) => path.startsWith(effectRoot));
36
+ const { runOhCli } = await import(${JSON.stringify(CLI_PATH)});
37
+ if (loaded()) throw new Error("CLI import loaded Effect");
38
+ for (const args of [["help"], ["version"], ["contract"]]) {
39
+ if (await runOhCli(args) !== 0) throw new Error("Static command failed");
40
+ if (loaded()) throw new Error("Static command loaded Effect");
41
+ }
42
+ let rejected = false;
43
+ try { await runOhCli(["sync", "export", "--limit", "0"]); }
44
+ catch (error) { if (!(error instanceof TypeError)) throw error; rejected = true; }
45
+ if (!rejected || loaded()) throw new Error("Invalid command crossed the SDK boundary");
46
+ // A real store command is also the positive control for the cache observer.
47
+ if (await runOhCli(["init"]) !== 0 || !loaded()) {
48
+ throw new Error("Store command did not load the runtime");
49
+ }
50
+ `;
51
+ const child = Bun.spawn([process.execPath, "--eval", probe], {
52
+ cwd: root, stderr: "pipe", stdout: "pipe",
53
+ });
54
+ const [code, stderr, stdout] = await Promise.all([
55
+ child.exited, new Response(child.stderr).text(), new Response(child.stdout).text(),
56
+ ]);
57
+ expect({ code, stderr }).toEqual({ code: 0, stderr: "" });
58
+ expect(stdout).toContain('"spaceId":"default"');
59
+ expect(existsSync(join(root, ".oh", "oh.sqlite"))).toBe(true);
60
+ });
61
+
27
62
  test("initializes, writes, searches, and verifies one local database", async () => {
28
63
  const root = await mkdtemp(join(tmpdir(), "oh-cli-test-"));
29
64
  roots.push(root);
package/src/cli.ts CHANGED
@@ -5,11 +5,10 @@ import { canonicalJson, opaqueId, safeCode, type JsonValue } from "./canonical";
5
5
  import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
6
6
  import { OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, createKnowledgeGraphRecordV1,
7
7
  type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "./graph";
8
- import { Oh } from "./sdk";
9
8
  import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations";
10
- import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1, parseOhSyncBundleV1 } from "./sync";
9
+ import { createOhSyncBundleV1, OH_SYNC_BUNDLE_MAX_BYTES_V1, parseOhSyncBundleV1 } from "./sync-model";
11
10
 
12
- export const OH_PACKAGE_VERSION = "0.4.1" as const;
11
+ export const OH_PACKAGE_VERSION = "0.4.2" as const;
13
12
 
14
13
  type ParsedArguments = { options: Map<string, string[]>; positionals: string[] };
15
14
  type ValidatedInvocation = Readonly<{
@@ -244,6 +243,8 @@ export async function runOhCli(arguments_: readonly string[]): Promise<number> {
244
243
  print({ manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION, v: 1 });
245
244
  return 0;
246
245
  }
246
+ // Static commands and invalid input need no store or sync runtime.
247
+ const { Oh } = await import("./sdk");
247
248
  const oh = Oh.open({ databasePath: validated.databasePath, spaceId: validated.spaceId });
248
249
  try {
249
250
  if (command === "init") { print({ head: oh.head(), spaceId: oh.store.spaceId, v: 1 }); return 0; }
@@ -6,7 +6,7 @@ import {
6
6
  utf8ByteLength,
7
7
  type Sha256Hex,
8
8
  } from "./canonical";
9
- import { normalizeOhEmbeddingV1 } from "./semantic";
9
+ import { normalizeOhEmbeddingV1 } from "./semantic-model";
10
10
 
11
11
  const cloudflareEmbeddingProfilePayload = Object.freeze({
12
12
  dimensions: 768,
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "bun:test";
2
+ import { Cause, Effect, Exit, Fiber } from "effect";
3
+ import { LibSqlStoreProgram, makeLibSqlOwner } from "./libsql-program";
4
+ import { LibSqlAuthorityClient, libSqlAuthorityClientLive } from "./libsql-platform";
5
+ import { createOhStoreBindingV1, emptyOhHeadV1, OH_CANONICAL_STORE_PROFILE_V1 } from "./store";
6
+
7
+ test("interrupting an admitted operation cannot complete drain before the native transaction settles", async () => {
8
+ let entered!: () => void;
9
+ const started = new Promise<void>(resolve => { entered = resolve; });
10
+ let settle!: () => void;
11
+ const held = new Promise<void>(resolve => { settle = resolve; });
12
+ const events: string[] = [];
13
+ const owner = Effect.runSync(makeLibSqlOwner);
14
+ expect(Effect.runSync(owner.admit)).toBe(true);
15
+ const operation = Effect.gen(function* () {
16
+ const client = yield* LibSqlAuthorityClient;
17
+ yield* client.batch([{ sql: "native transaction" }], "write");
18
+ yield* Effect.sync(() => { events.push("reconciled"); });
19
+ }).pipe(Effect.provide(libSqlAuthorityClientLive({
20
+ execute: async () => ({ rows: [] }),
21
+ batch: async () => {
22
+ entered();
23
+ await held;
24
+ events.push("native-settled");
25
+ return [{ rows: [] }];
26
+ },
27
+ })));
28
+ const fiber = Effect.runFork(owner.complete(operation));
29
+ await started;
30
+ Effect.runSync(owner.beginClose);
31
+ expect(Effect.runSync(owner.admit)).toBe(false);
32
+ let drained = false;
33
+ const drain = Effect.runPromise(owner.drained).then(() => { drained = true; });
34
+ const interruption = Effect.runPromise(Fiber.interrupt(fiber));
35
+ await Promise.resolve();
36
+ expect(drained).toBe(false);
37
+ expect(events).toEqual([]);
38
+ settle();
39
+ const exit = await interruption;
40
+ await drain;
41
+ expect(Exit.isFailure(exit) && Cause.isInterrupted(exit.cause)).toBe(true);
42
+ expect(events).toEqual(["native-settled", "reconciled"]);
43
+ expect(drained).toBe(true);
44
+ });
45
+
46
+
47
+ test("expected row and input failures stay typed while an unrelated response defect remains a defect", async () => {
48
+ const binding = createOhStoreBindingV1({ profile: OH_CANONICAL_STORE_PROFILE_V1,
49
+ realmId: "realm:typed-failures", spaceId: "typed-failures", v: 1 });
50
+ const program = new LibSqlStoreProgram({
51
+ execute: () => Effect.succeed({ rows: [{}] }),
52
+ batch: () => Effect.succeed([]), close: Effect.void, currentInstant: Effect.succeed("2026-09-06T12:00:00.000Z"),
53
+ }, binding);
54
+ for (const [operation, tag] of [
55
+ [program.head().pipe(Effect.asVoid), "LibSqlIntegrity"],
56
+ [program.commit({ actorId: "", operationId: "", changes: [], expectedHead: emptyOhHeadV1() }).pipe(Effect.asVoid), "LibSqlValidation"],
57
+ ] as const) {
58
+ const exit = await Effect.runPromiseExit(operation);
59
+ expect(Exit.isFailure(exit)).toBe(true);
60
+ if (Exit.isFailure(exit)) {
61
+ expect(Array.from(Cause.defects(exit.cause))).toEqual([]);
62
+ expect(Array.from(Cause.failures(exit.cause)).map(failure => failure._tag)).toEqual([tag]);
63
+ }
64
+ }
65
+ const defect = Object.freeze({ kind: "unexpected-provider-getter" });
66
+ const defective = new LibSqlStoreProgram({
67
+ execute: () => Effect.succeed({ get rows(): [] { throw defect; } }),
68
+ batch: () => Effect.succeed([]), close: Effect.void, currentInstant: Effect.succeed("2026-09-06T12:00:00.000Z"),
69
+ }, binding);
70
+ const exit = await Effect.runPromiseExit(defective.head());
71
+ expect(Exit.isFailure(exit)).toBe(true);
72
+ if (Exit.isFailure(exit)) {
73
+ expect(Array.from(Cause.failures(exit.cause))).toEqual([]);
74
+ expect(Array.from(Cause.defects(exit.cause))).toEqual([defect]);
75
+ }
76
+ });
@@ -0,0 +1,350 @@
1
+ import { canonicalJson, canonicalSha256, parseSha256Hex, type Sha256Hex } from "./canonical";
2
+
3
+ import { parseOhOperationV1, type OhOperationV1 } from "./operation";
4
+ import {
5
+ OhIntegrityError,
6
+ parseOhSpacePurgeReceiptV1,
7
+ parseOhStoreBindingV1,
8
+ type OhHeadV1,
9
+ type OhSpacePurgeReceiptV1,
10
+ type OhStoreBindingV1,
11
+ type OhStoreProfileV1,
12
+ } from "./store";
13
+
14
+ export type OhLibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string;
15
+ export type OhLibSqlStatementV1 = Readonly<{ args?: readonly OhLibSqlValueV1[]; sql: string }>;
16
+ export type OhLibSqlResultV1 = Readonly<{
17
+ rows: readonly (Readonly<Record<string, unknown>> | readonly unknown[])[];
18
+ rowsAffected?: number;
19
+ }>;
20
+
21
+ /** Structural subset implemented by `@libsql/client` clients. */
22
+ export interface OhLibSqlClientV1 {
23
+ batch(
24
+ statements: readonly OhLibSqlStatementV1[],
25
+ mode?: "deferred" | "read" | "write",
26
+ ): Promise<readonly OhLibSqlResultV1[]>;
27
+ close?(): void;
28
+ execute(statement: OhLibSqlStatementV1 | string): Promise<OhLibSqlResultV1>;
29
+ }
30
+
31
+ export type OhLibSqlStoreAuthorityOptionsV1 = Readonly<{
32
+ closeClient?: boolean;
33
+ profile?: OhStoreProfileV1;
34
+ realmId?: string;
35
+ spaceId?: string;
36
+ }>;
37
+
38
+ export const OH_LIBSQL_STORE_LIMITS_V1 = Object.freeze({
39
+ changesPerCommit: 64,
40
+ changeFeedLimit: 7,
41
+ dependenciesPerCommit: 512,
42
+ historyBytes: 4 * 1024 * 1024,
43
+ historyOperations: 16_384,
44
+ operationBytes: 512 * 1024,
45
+ providerResponseBytes: 9_000_000,
46
+ snapshotComponentBytes: 6 * 1024 * 1024,
47
+ });
48
+
49
+ export const AUTHORITY_SCHEMA_NAME = "oh.libsql-authority.v1";
50
+ export const AUTHORITY_SCHEMA_VERSION = 1;
51
+ export const EMPTY_RECORDS_SHA256 = canonicalSha256([]);
52
+ export const PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256,
53
+ prior_sequence, purged_at, receipt_sha256, receipt_json
54
+ FROM oh_authority_purges WHERE space_id = ?`;
55
+ export const BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind,
56
+ profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`;
57
+ export const SPACE_PURGE_PROOF_SELECT = `SELECT generation, graph_revision_sha256, head_operation_sha256,
58
+ records_sha256, sequence, contract_id FROM oh_authority_spaces WHERE space_id = ?`;
59
+ export const OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id,
60
+ parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`;
61
+ // libSQL serializes text again in its JSON response. Twice the UTF-8 text plus all
62
+ // duplicated columns and a fixed row reserve is a conservative upper bound for
63
+ // canonical JSON, whose own escapes can only be escaped once more by transport.
64
+ export const OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB))
65
+ + 2 * (length(operation.operation_sha256) + length(operation.space_id)
66
+ + length(operation.operation_id) + coalesce(length(operation.parent_operation_sha256), 0)
67
+ + length(operation.graph_revision_sha256) + length(operation.records_sha256)
68
+ + length(operation.instant)) + 512`;
69
+ export const RECORD_RESPONSE_BYTES = `2 * length(CAST(record.record_json AS BLOB))
70
+ + 2 * (length(record.record_key) + length(record.kind) + length(record.record_sha256)
71
+ + length(record.operation_sha256)) + 384`;
72
+ export const DEPENDENCY_RESPONSE_BYTES = `2 * (length(dependency.record_key)
73
+ + length(dependency.dependency_key)) + 192`;
74
+ export const OPERATION_RECORD_RESPONSE_BYTES = `2 * (length(materialized.space_id)
75
+ + length(materialized.operation_sha256) + length(materialized.record_key)
76
+ + length(materialized.change_kind) + length(materialized.record_sha256)) + 320`;
77
+
78
+ export const AUTHORITY_SCHEMA_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS oh_authority_schemas (
79
+ version INTEGER PRIMARY KEY,
80
+ name TEXT NOT NULL UNIQUE,
81
+ schema_sha256 TEXT NOT NULL,
82
+ applied_at TEXT NOT NULL
83
+ ) STRICT`;
84
+
85
+ export const AUTHORITY_SCHEMA_STATEMENTS = Object.freeze([
86
+ `CREATE TABLE IF NOT EXISTS oh_authority_contracts (
87
+ contract_id TEXT PRIMARY KEY,
88
+ contract_sha256 TEXT NOT NULL,
89
+ manifest_json TEXT NOT NULL
90
+ ) STRICT`,
91
+ `CREATE TABLE IF NOT EXISTS oh_authority_spaces (
92
+ space_id TEXT PRIMARY KEY,
93
+ contract_id TEXT NOT NULL,
94
+ generation INTEGER NOT NULL CHECK(generation >= 0),
95
+ head_operation_sha256 TEXT,
96
+ graph_revision_sha256 TEXT,
97
+ records_sha256 TEXT NOT NULL,
98
+ sequence INTEGER NOT NULL CHECK(sequence >= 0),
99
+ created_at TEXT NOT NULL,
100
+ updated_at TEXT NOT NULL,
101
+ CHECK(generation = sequence)
102
+ ) STRICT`,
103
+ `CREATE TABLE IF NOT EXISTS oh_authority_operations (
104
+ operation_sha256 TEXT PRIMARY KEY,
105
+ space_id TEXT NOT NULL,
106
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
107
+ operation_id TEXT NOT NULL,
108
+ parent_operation_sha256 TEXT,
109
+ graph_revision_sha256 TEXT NOT NULL,
110
+ records_sha256 TEXT NOT NULL,
111
+ operation_json TEXT NOT NULL,
112
+ instant TEXT NOT NULL,
113
+ UNIQUE(space_id, sequence),
114
+ UNIQUE(space_id, operation_id)
115
+ ) STRICT`,
116
+ `CREATE TABLE IF NOT EXISTS oh_authority_operation_records (
117
+ space_id TEXT NOT NULL,
118
+ operation_sha256 TEXT NOT NULL,
119
+ ordinal INTEGER NOT NULL CHECK(ordinal >= 0),
120
+ record_key TEXT NOT NULL,
121
+ change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')),
122
+ record_sha256 TEXT NOT NULL,
123
+ PRIMARY KEY(operation_sha256, ordinal),
124
+ UNIQUE(operation_sha256, record_key)
125
+ ) STRICT`,
126
+ `CREATE TABLE IF NOT EXISTS oh_authority_records (
127
+ space_id TEXT NOT NULL,
128
+ record_key TEXT NOT NULL,
129
+ kind TEXT NOT NULL,
130
+ record_sha256 TEXT NOT NULL,
131
+ record_json TEXT NOT NULL,
132
+ operation_sha256 TEXT NOT NULL,
133
+ sequence INTEGER NOT NULL CHECK(sequence > 0),
134
+ PRIMARY KEY(space_id, record_key)
135
+ ) STRICT`,
136
+ `CREATE TABLE IF NOT EXISTS oh_authority_dependencies (
137
+ space_id TEXT NOT NULL,
138
+ record_key TEXT NOT NULL,
139
+ dependency_key TEXT NOT NULL,
140
+ PRIMARY KEY(space_id, record_key, dependency_key)
141
+ ) STRICT`,
142
+ `CREATE TABLE IF NOT EXISTS oh_authority_bindings (
143
+ space_id TEXT PRIMARY KEY,
144
+ realm_id TEXT NOT NULL,
145
+ profile_id TEXT NOT NULL,
146
+ profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')),
147
+ profile_sha256 TEXT NOT NULL,
148
+ binding_sha256 TEXT NOT NULL UNIQUE,
149
+ binding_json TEXT NOT NULL,
150
+ created_at TEXT NOT NULL
151
+ ) STRICT`,
152
+ `CREATE TABLE IF NOT EXISTS oh_authority_purges (
153
+ space_id TEXT PRIMARY KEY,
154
+ binding_sha256 TEXT NOT NULL,
155
+ prior_operation_sha256 TEXT,
156
+ prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0),
157
+ purged_at TEXT NOT NULL,
158
+ receipt_sha256 TEXT NOT NULL UNIQUE,
159
+ receipt_json TEXT NOT NULL
160
+ ) STRICT`,
161
+ `CREATE TABLE IF NOT EXISTS oh_authority_commit_guards (
162
+ value TEXT NOT NULL CHECK(value = 'ok')
163
+ ) STRICT`,
164
+ "CREATE INDEX IF NOT EXISTS oh_authority_operations_space_sequence ON oh_authority_operations(space_id, sequence)",
165
+ "CREATE INDEX IF NOT EXISTS oh_authority_records_space_kind ON oh_authority_records(space_id, kind, record_key)",
166
+ "CREATE INDEX IF NOT EXISTS oh_authority_dependencies_dependency ON oh_authority_dependencies(space_id, dependency_key)",
167
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_no_update
168
+ BEFORE UPDATE ON oh_authority_operations
169
+ BEGIN SELECT RAISE(ABORT, 'Oh authority operations are immutable'); END`,
170
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_guard_delete
171
+ BEFORE DELETE ON oh_authority_operations
172
+ WHEN NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = OLD.space_id)
173
+ BEGIN SELECT RAISE(ABORT, 'Oh authority operations require a purge receipt'); END`,
174
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_no_update
175
+ BEFORE UPDATE ON oh_authority_operation_records
176
+ BEGIN SELECT RAISE(ABORT, 'Oh authority operation records are immutable'); END`,
177
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_insert
178
+ BEFORE INSERT ON oh_authority_operation_records
179
+ WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations
180
+ WHERE operation_sha256 = NEW.operation_sha256 AND space_id = NEW.space_id)
181
+ BEGIN SELECT RAISE(ABORT, 'Oh authority operation record has no owning operation'); END`,
182
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_delete
183
+ BEFORE DELETE ON oh_authority_operation_records
184
+ WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations AS operation
185
+ JOIN oh_authority_purges AS purge ON purge.space_id = operation.space_id
186
+ WHERE operation.operation_sha256 = OLD.operation_sha256)
187
+ BEGIN SELECT RAISE(ABORT, 'Oh authority operation records require a purge receipt'); END`,
188
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_update
189
+ BEFORE UPDATE ON oh_authority_purges
190
+ BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`,
191
+ `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_delete
192
+ BEFORE DELETE ON oh_authority_purges
193
+ BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`,
194
+ ]);
195
+
196
+ export function normalizedSchemaSql(sql: string): string {
197
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim();
198
+ }
199
+
200
+ export function expectedSchemaObject(statement: string): Readonly<{
201
+ name: string;
202
+ sql: string;
203
+ tableName: string;
204
+ type: "index" | "table" | "trigger";
205
+ }> {
206
+ const match = /^CREATE\s+(TABLE|INDEX|TRIGGER)(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-z0-9_]+)/iu.exec(statement.trim());
207
+ if (match === null) throw new Error("Invalid compiled authority schema statement.");
208
+ const declaredType = match[1]?.toLowerCase();
209
+ const type = declaredType === "index" ? "index" as const
210
+ : declaredType === "trigger" ? "trigger" as const : "table" as const;
211
+ const name = match[2] as string;
212
+ const tableMatch = type === "index" || type === "trigger" ? /\bON\s+([a-z0-9_]+)/iu.exec(statement) : null;
213
+ const tableName = type === "table" ? name : tableMatch?.[1];
214
+ if (tableName === undefined) throw new Error("Invalid compiled authority index statement.");
215
+ return { name, sql: normalizedSchemaSql(statement), tableName, type };
216
+ }
217
+
218
+ export const AUTHORITY_SCHEMA_OBJECTS = Object.freeze(
219
+ [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS]
220
+ .map(expectedSchemaObject)
221
+ .sort((left, right) => canonicalJson([left.type, left.name])
222
+ .localeCompare(canonicalJson([right.type, right.name]))),
223
+ );
224
+ export const AUTHORITY_SCHEMA_SHA256 = canonicalSha256(AUTHORITY_SCHEMA_OBJECTS);
225
+
226
+ export function rowValue(
227
+ row: Readonly<Record<string, unknown>> | readonly unknown[],
228
+ key: string,
229
+ index: number,
230
+ ): unknown {
231
+ return Array.isArray(row) ? row[index] : (row as Readonly<Record<string, unknown>>)[key];
232
+ }
233
+
234
+ export function integer(value: unknown): number | null {
235
+ if (typeof value === "number") return Number.isSafeInteger(value) ? value : null;
236
+ if (typeof value === "bigint") {
237
+ const parsed = Number(value);
238
+ return Number.isSafeInteger(parsed) ? parsed : null;
239
+ }
240
+ return null;
241
+ }
242
+
243
+ export function normalizeLimit(value: number | undefined, fallback = 100, maximum = 1000): number {
244
+ const limit = value ?? fallback;
245
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) {
246
+ throw new RangeError(`limit must be an integer from 1 through ${maximum}.`);
247
+ }
248
+ return limit;
249
+ }
250
+
251
+ export function parseOperationJson(value: unknown): OhOperationV1 {
252
+ if (typeof value !== "string") throw new OhIntegrityError("A stored operation is not JSON text.");
253
+ let parsedValue: unknown;
254
+ try { parsedValue = JSON.parse(value); } catch { throw new OhIntegrityError("A stored operation is not JSON."); }
255
+ const operation = parseOhOperationV1(parsedValue);
256
+ if (operation === null || canonicalJson(operation) !== value) {
257
+ throw new OhIntegrityError("A stored operation is invalid.");
258
+ }
259
+ return operation;
260
+ }
261
+
262
+ export function parseOperationRow(
263
+ row: Readonly<Record<string, unknown>> | readonly unknown[],
264
+ expected: Readonly<{ operationId?: string; operationSha256?: string; spaceId?: string }> = {},
265
+ ): OhOperationV1 {
266
+ const operation = parseOperationJson(rowValue(row, "operation_json", 7));
267
+ if (rowValue(row, "operation_sha256", 0) !== operation.operationSha256
268
+ || rowValue(row, "space_id", 1) !== operation.spaceId
269
+ || integer(rowValue(row, "sequence", 2)) !== operation.sequence
270
+ || rowValue(row, "operation_id", 3) !== operation.operationId
271
+ || rowValue(row, "parent_operation_sha256", 4) !== operation.parentOperationSha256
272
+ || rowValue(row, "graph_revision_sha256", 5) !== operation.graphRevisionSha256
273
+ || rowValue(row, "records_sha256", 6) !== operation.recordsSha256
274
+ || rowValue(row, "instant", 8) !== operation.instant
275
+ || (expected.spaceId !== undefined && operation.spaceId !== expected.spaceId)
276
+ || (expected.operationId !== undefined && operation.operationId !== expected.operationId)
277
+ || (expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256)) {
278
+ throw new OhIntegrityError("Remote operation columns do not match their canonical envelope.");
279
+ }
280
+ return operation;
281
+ }
282
+
283
+ export function parseBindingRow(
284
+ row: Readonly<Record<string, unknown>> | readonly unknown[],
285
+ expectedSpaceId: string,
286
+ ): OhStoreBindingV1 {
287
+ const json = rowValue(row, "binding_json", 6);
288
+ if (typeof json !== "string") throw new OhIntegrityError("A remote store binding is not JSON text.");
289
+ let value: unknown;
290
+ try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote store binding is not JSON."); }
291
+ const binding = parseOhStoreBindingV1(value);
292
+ if (binding === null || canonicalJson(binding) !== json || binding.spaceId !== expectedSpaceId
293
+ || rowValue(row, "space_id", 0) !== binding.spaceId
294
+ || rowValue(row, "realm_id", 1) !== binding.realmId
295
+ || rowValue(row, "profile_id", 2) !== binding.profile.profileId
296
+ || rowValue(row, "profile_kind", 3) !== binding.profile.profileKind
297
+ || rowValue(row, "profile_sha256", 4) !== binding.profile.profileSha256
298
+ || rowValue(row, "binding_sha256", 5) !== binding.bindingSha256) {
299
+ throw new OhIntegrityError("Remote binding columns do not match their canonical envelope.");
300
+ }
301
+ return binding;
302
+ }
303
+
304
+ export function parsePurgeReceiptRow(
305
+ row: Readonly<Record<string, unknown>> | readonly unknown[],
306
+ expectedSpaceId: string,
307
+ expectedBindingSha256?: Sha256Hex,
308
+ ): OhSpacePurgeReceiptV1 {
309
+ const json = rowValue(row, "receipt_json", 6);
310
+ if (typeof json !== "string") throw new OhIntegrityError("A remote purge receipt is invalid.");
311
+ let value: unknown;
312
+ try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote purge receipt is invalid."); }
313
+ const receipt = parseOhSpacePurgeReceiptV1(value);
314
+ if (receipt === null || canonicalJson(receipt) !== json) {
315
+ throw new OhIntegrityError("A remote purge receipt is invalid.");
316
+ }
317
+ if (receipt.spaceId !== expectedSpaceId
318
+ || (expectedBindingSha256 !== undefined && receipt.bindingSha256 !== expectedBindingSha256)
319
+ || rowValue(row, "space_id", 0) !== receipt.spaceId
320
+ || rowValue(row, "binding_sha256", 1) !== receipt.bindingSha256
321
+ || rowValue(row, "prior_operation_sha256", 2) !== receipt.priorHead.operationSha256
322
+ || integer(rowValue(row, "prior_sequence", 3)) !== receipt.priorHead.sequence
323
+ || rowValue(row, "purged_at", 4) !== receipt.purgedAt
324
+ || rowValue(row, "receipt_sha256", 5) !== receipt.receiptSha256) {
325
+ throw new OhIntegrityError("Remote purge columns do not match their canonical receipt.");
326
+ }
327
+ return receipt;
328
+ }
329
+
330
+ export function parseHeadRow(row: Readonly<Record<string, unknown>> | readonly unknown[]): OhHeadV1 {
331
+ const generation = integer(rowValue(row, "generation", 0));
332
+ const graphValue = rowValue(row, "graph_revision_sha256", 1);
333
+ const operationValue = rowValue(row, "head_operation_sha256", 2);
334
+ const recordsSha256 = parseSha256Hex(rowValue(row, "records_sha256", 3));
335
+ const sequence = integer(rowValue(row, "sequence", 4));
336
+ const graphRevisionSha256 = graphValue === null ? null : parseSha256Hex(graphValue);
337
+ const operationSha256 = operationValue === null ? null : parseSha256Hex(operationValue);
338
+ if (generation === null || sequence === null || generation !== sequence || recordsSha256 === null
339
+ || (graphValue !== null && graphRevisionSha256 === null)
340
+ || (operationValue !== null && operationSha256 === null)
341
+ || ((sequence === 0) !== (operationSha256 === null))
342
+ || ((sequence === 0) !== (graphRevisionSha256 === null))) {
343
+ throw new OhIntegrityError("The remote authority contains an invalid head.");
344
+ }
345
+ return { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 };
346
+ }
347
+
348
+ export const PURGE_PAYLOAD_TABLES = ["oh_authority_spaces", "oh_authority_bindings",
349
+ "oh_authority_operations", "oh_authority_operation_records", "oh_authority_records",
350
+ "oh_authority_dependencies"] as const;
@@ -0,0 +1,51 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { canonicalNow } from "./canonical";
3
+ import { OhConflictError, OhDependencyError, OhIntegrityError, OhOperationSizeError, OhProfileError, OhPurgedSpaceError } from "./store";
4
+ import type { OhLibSqlClientV1, OhLibSqlResultV1, OhLibSqlStatementV1 } from "./libsql-model";
5
+
6
+ export type LibSqlFailure = Readonly<{
7
+ _tag: "LibSqlConflict" | "LibSqlDependency" | "LibSqlIntegrity" | "LibSqlProfile" | "LibSqlPurged" | "LibSqlClosed"
8
+ | "LibSqlCapacity" | "LibSqlValidation" | "LibSqlForeign";
9
+ /** Kept locally to reproduce the existing public rejection value. */
10
+ cause: unknown;
11
+ }>;
12
+
13
+ export function libSqlFailure(cause: unknown): LibSqlFailure {
14
+ return { _tag: cause instanceof OhConflictError ? "LibSqlConflict"
15
+ : cause instanceof OhDependencyError ? "LibSqlDependency"
16
+ : cause instanceof OhIntegrityError ? "LibSqlIntegrity"
17
+ : cause instanceof OhProfileError ? "LibSqlProfile"
18
+ : cause instanceof OhPurgedSpaceError ? "LibSqlPurged"
19
+ : cause instanceof OhOperationSizeError || cause instanceof RangeError ? "LibSqlCapacity"
20
+ : cause instanceof TypeError ? "LibSqlValidation" : "LibSqlForeign", cause };
21
+ }
22
+
23
+ /** Existing canonical validators may throw; unrelated generator defects remain defects. */
24
+ export function libSqlValue<A>(evaluate: () => A): Effect.Effect<A, LibSqlFailure> {
25
+ return Effect.try({ try: evaluate, catch: libSqlFailure });
26
+ }
27
+
28
+ export interface LibSqlAuthorityClientService {
29
+ execute(statement: OhLibSqlStatementV1 | string): Effect.Effect<OhLibSqlResultV1, LibSqlFailure>;
30
+ batch(statements: readonly OhLibSqlStatementV1[], mode?: "deferred" | "read" | "write"):
31
+ Effect.Effect<readonly OhLibSqlResultV1[], LibSqlFailure>;
32
+ readonly close: Effect.Effect<void, LibSqlFailure>;
33
+ readonly currentInstant: Effect.Effect<string, LibSqlFailure>;
34
+ }
35
+
36
+ export class LibSqlAuthorityClient extends Context.Tag("@hraness/oh/LibSqlAuthorityClient")<
37
+ LibSqlAuthorityClient, LibSqlAuthorityClientService
38
+ >() {}
39
+
40
+ /** The structural client owns transaction atomicity. Never split a batch or
41
+ * abandon its foreign Promise when a fiber is interrupted; no retry is added. */
42
+ export function libSqlAuthorityClientLive(client: OhLibSqlClientV1): Layer.Layer<LibSqlAuthorityClient> {
43
+ const settle = <A>(call: () => Promise<A>): Effect.Effect<A, LibSqlFailure> =>
44
+ Effect.uninterruptible(Effect.tryPromise({ try: call, catch: libSqlFailure }));
45
+ return Layer.succeed(LibSqlAuthorityClient, {
46
+ execute: statement => settle(() => client.execute(statement)),
47
+ batch: (statements, mode) => settle(() => client.batch(statements, mode)),
48
+ close: libSqlValue(() => { client.close?.(); }),
49
+ currentInstant: libSqlValue(canonicalNow),
50
+ });
51
+ }