@hraness/oh 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +194 -127
  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-semantic-v2.d.ts +160 -0
  10. package/dist/libsql-semantic-v2.d.ts.map +1 -0
  11. package/dist/libsql.d.ts.map +1 -1
  12. package/dist/libsql.js +162 -35
  13. package/dist/memory-page.js +2 -2
  14. package/dist/memory.d.ts +87 -6
  15. package/dist/memory.d.ts.map +1 -1
  16. package/dist/memory.js +1106 -148
  17. package/dist/operation.d.ts +3 -1
  18. package/dist/operation.d.ts.map +1 -1
  19. package/dist/projection-public.js +2 -2
  20. package/dist/projection-suss.js +2 -2
  21. package/dist/sdk.js +780 -88
  22. package/dist/semantic-cloud.d.ts +1 -0
  23. package/dist/semantic-cloud.d.ts.map +1 -1
  24. package/dist/semantic-cloud.js +1545 -2
  25. package/dist/semantic.js +2 -2
  26. package/dist/sqlite/index.js +1251 -306
  27. package/dist/sqlite/port.d.ts +31 -3
  28. package/dist/sqlite/port.d.ts.map +1 -1
  29. package/dist/sqlite/store.d.ts +18 -2
  30. package/dist/sqlite/store.d.ts.map +1 -1
  31. package/dist/store.d.ts +3 -12
  32. package/dist/store.d.ts.map +1 -1
  33. package/dist/store.js +154 -32
  34. package/dist/sync.d.ts +7 -1
  35. package/dist/sync.d.ts.map +1 -1
  36. package/dist/sync.js +668 -35
  37. package/package.json +5 -1
  38. package/skills/oh/SKILL.md +42 -16
  39. package/spec/README.md +10 -3
  40. package/spec/manifest.json +5 -1
  41. package/spec/v1/libsql-semantic-cache-schema-v1.sql +114 -0
  42. package/spec/v1/libsql-semantic-digest-fixture-v1.json +13 -0
  43. package/spec/v1/memory.md +134 -16
  44. package/spec/v1/storage.md +8 -5
  45. package/spec/v1/store.md +20 -0
  46. package/spec/v1/sync.md +77 -8
  47. package/spec/v2/libsql-semantic-cache-schema-v2.sql +175 -0
  48. package/spec/v2/libsql-semantic-digest-fixture-v2.json +14 -0
  49. package/spec/v2/manifest.json +11 -0
  50. package/spec/v2/semantic-cloud.md +106 -0
  51. package/src/cli.test.ts +53 -1
  52. package/src/cli.ts +34 -8
  53. package/src/errors.test.ts +87 -0
  54. package/src/errors.ts +185 -0
  55. package/src/graph.ts +2 -2
  56. package/src/libsql-semantic-v2.test.ts +1114 -0
  57. package/src/libsql-semantic-v2.ts +1891 -0
  58. package/src/libsql-semantic.test.ts +50 -1
  59. package/src/libsql.test.ts +36 -0
  60. package/src/libsql.ts +26 -5
  61. package/src/memory.test.ts +1488 -18
  62. package/src/memory.ts +1199 -122
  63. package/src/operation.ts +13 -3
  64. package/src/semantic-cloud.ts +1 -0
  65. package/src/sqlite/port.test.ts +209 -0
  66. package/src/sqlite/port.ts +118 -4
  67. package/src/sqlite/store.test.ts +106 -1
  68. package/src/sqlite/store.ts +168 -30
  69. package/src/store.test.ts +12 -0
  70. package/src/store.ts +30 -20
  71. package/src/sync.test.ts +570 -2
  72. package/src/sync.ts +586 -36
@@ -147,7 +147,11 @@ function document(
147
147
  async function bootstrapped(): Promise<SqliteCompatibleLibSqlClient> {
148
148
  const client = new SqliteCompatibleLibSqlClient();
149
149
  expect(await bootstrapOhLibSqlSemanticCacheV1(client, { appliedAt: instant1 }))
150
- .toMatchObject({ schemaVersion: 1, v: 1 });
150
+ .toEqual({
151
+ schemaSha256: "d9903735e900e08a8c49c9ad36ea8f277b1689117ef88b9a2c6b906456dfbace" as Sha256Hex,
152
+ schemaVersion: 1,
153
+ v: 1,
154
+ });
151
155
  return client;
152
156
  }
153
157
 
@@ -167,6 +171,51 @@ function authority(
167
171
  }
168
172
 
169
173
  describe("libSQL derived semantic cache", () => {
174
+ test("reproduces the fixed released V1 digest fixture", async () => {
175
+ const fixture = await Bun.file(new URL(
176
+ "../spec/v1/libsql-semantic-digest-fixture-v1.json",
177
+ import.meta.url,
178
+ )).json() as Readonly<{
179
+ authorityId: string;
180
+ authoritySha256: Sha256Hex;
181
+ content: string;
182
+ generation: number;
183
+ generationSha256: Sha256Hex;
184
+ inputSha256: Sha256Hex;
185
+ membershipSha256: Sha256Hex;
186
+ recordKey: string;
187
+ recordSha256: Sha256Hex;
188
+ title: string;
189
+ v: 1;
190
+ }>;
191
+ const client = await bootstrapped();
192
+ const cache = await openOhLibSqlSemanticCacheV1(client);
193
+ const calls: string[][] = [];
194
+ const staged = await cache.stage({
195
+ authorityId: fixture.authorityId,
196
+ authoritySha256: fixture.authoritySha256,
197
+ createdAt: instant1,
198
+ documents: [{
199
+ content: fixture.content,
200
+ key: fixture.recordKey,
201
+ recordSha256: fixture.recordSha256,
202
+ title: fixture.title,
203
+ v: fixture.v,
204
+ }],
205
+ embeddingClient: embeddingClient(calls),
206
+ generation: fixture.generation,
207
+ });
208
+ expect(staged).toMatchObject({
209
+ generationSha256: fixture.generationSha256,
210
+ membershipSha256: fixture.membershipSha256,
211
+ v: 1,
212
+ });
213
+ expect(calls).toHaveLength(1);
214
+ expect(sha256Hex(calls[0]?.[0] ?? "")).toBe(fixture.inputSha256);
215
+ await cache.close();
216
+ client.close();
217
+ });
218
+
170
219
  test("requires explicit bootstrap and refuses a partial or drifted schema", async () => {
171
220
  const empty = new SqliteCompatibleLibSqlClient();
172
221
  await expect(openOhLibSqlSemanticCacheV1(empty)).rejects.toMatchObject({
@@ -18,6 +18,7 @@ import {
18
18
  OH_WORKING_STORE_PROFILE_V1,
19
19
  OhConflictError,
20
20
  OhIntegrityError,
21
+ OhOperationSizeError,
21
22
  OhProfileError,
22
23
  OhPurgedSpaceError,
23
24
  } from "./store";
@@ -341,6 +342,41 @@ describe("direct libSQL Oh authority", () => {
341
342
  client.close();
342
343
  });
343
344
 
345
+ test("honors the host-declared operation byte bound before replay or persistence", async () => {
346
+ const client = await bootstrappedClient();
347
+ const authority = await createOhLibSqlStoreAuthorityV1(client, {
348
+ profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:operation-bound", spaceId: "operation-bound",
349
+ });
350
+ const empty = await authority.store.head();
351
+ const firstChanges = [{ kind: "put" as const,
352
+ record: entity("entity:bounded", "Bounded"), v: 1 as const }];
353
+ const first = await authority.store.commit({ actorId: "agent.bound", changes: firstChanges,
354
+ expectedHead: empty, operationId: "op_bounded_first" });
355
+
356
+ const replayError: unknown = await authority.store.commit({ actorId: "agent.bound", changes: firstChanges,
357
+ expectedHead: empty, maximumOperationBytes: 1,
358
+ operationId: "op_bounded_first" }).catch((error: unknown) => error);
359
+ const commitError: unknown = await authority.store.commit({ actorId: "agent.bound", changes: [{ kind: "put",
360
+ record: entity("entity:rejected", "Rejected"), v: 1 }], expectedHead: await authority.store.head(),
361
+ maximumOperationBytes: 1, operationId: "op_bounded_rejected" }).catch((error: unknown) => error);
362
+ for (const error of [replayError, commitError]) {
363
+ expect(error).toBeInstanceOf(OhOperationSizeError);
364
+ expect(error).toBeInstanceOf(RangeError);
365
+ expect(error).toMatchObject({ maximumOperationBytes: 1,
366
+ operationBytes: expect.any(Number) });
367
+ }
368
+
369
+ expect(await authority.store.head()).toMatchObject({
370
+ operationSha256: first.operationSha256,
371
+ sequence: 1,
372
+ });
373
+ expect(client.database.query<{ count: number }, [string]>(`SELECT count(*) AS count
374
+ FROM oh_authority_operations WHERE space_id = ?`).get("operation-bound")?.count).toBe(1);
375
+ expect((await authority.store.snapshot()).records.map(({ key }) => key)).toEqual(["entity:bounded"]);
376
+ await authority.store.close();
377
+ client.close();
378
+ });
379
+
344
380
  test("uses compare-and-swap guards to leave no partial remote mutation", async () => {
345
381
  const client = await bootstrappedClient();
346
382
  const first = await createOhLibSqlStoreAuthorityV1(client, {
package/src/libsql.ts CHANGED
@@ -11,7 +11,11 @@ import { OH_CONTRACT_MANIFEST_V1 } from "./contract";
11
11
  import { canonicalKnowledgeGraphChangesV1, knowledgeGraphRecordRefV1, OH_GRAPH_LIMITS_V1,
12
12
  parseKnowledgeGraphRecordV1,
13
13
  type KnowledgeGraphRecordV1 } from "./graph";
14
- import { parseOhOperationV1, type OhOperationV1 } from "./operation";
14
+ import {
15
+ OH_OPERATION_MAX_BYTES_V1,
16
+ parseOhOperationV1,
17
+ type OhOperationV1,
18
+ } from "./operation";
15
19
  import {
16
20
  createOhDependencyClosureV1,
17
21
  createOhSpacePurgeReceiptV1,
@@ -21,6 +25,7 @@ import {
21
25
  OH_WORKING_STORE_PROFILE_V1,
22
26
  OhConflictError,
23
27
  OhIntegrityError,
28
+ OhOperationSizeError,
24
29
  OhProfileError,
25
30
  OhPurgedSpaceError,
26
31
  parseOhHeadRefV1,
@@ -1261,8 +1266,14 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
1261
1266
  this.#assertOpen();
1262
1267
  const actorId = safeCode(input.actorId);
1263
1268
  const operationId = safeCode(input.operationId);
1269
+ const maximumOperationBytes = input.maximumOperationBytes ?? OH_OPERATION_MAX_BYTES_V1;
1264
1270
  const changes = canonicalKnowledgeGraphChangesV1(input.changes);
1265
- if (actorId === null || operationId === null || changes.length === 0) throw new TypeError("Invalid Oh commit input.");
1271
+ if (actorId === null || operationId === null || changes.length === 0
1272
+ || !Number.isSafeInteger(maximumOperationBytes)
1273
+ || maximumOperationBytes < 1
1274
+ || maximumOperationBytes > OH_OPERATION_MAX_BYTES_V1) {
1275
+ throw new TypeError("Invalid Oh commit input.");
1276
+ }
1266
1277
  if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) {
1267
1278
  throw new RangeError("A direct libSQL commit exceeds its change-count bound.");
1268
1279
  }
@@ -1277,6 +1288,10 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
1277
1288
  if (duplicate.actorId !== actorId || canonicalJson(duplicate.changes) !== canonicalJson(changes)) {
1278
1289
  throw new OhConflictError("The operation ID is already bound to different content.");
1279
1290
  }
1291
+ const operationBytes = utf8ByteLength(canonicalJson(duplicate));
1292
+ if (operationBytes > maximumOperationBytes) {
1293
+ throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
1294
+ }
1280
1295
  return duplicate;
1281
1296
  }
1282
1297
  if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0
@@ -1286,9 +1301,15 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
1286
1301
  }
1287
1302
  const snapshot = await this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot);
1288
1303
  const transition = transitionOhSnapshotV1({ actorId, changes,
1289
- instant: input.instant ?? canonicalNow(), operationId, snapshot, spaceId: this.binding.spaceId });
1304
+ instant: input.instant ?? canonicalNow(), maximumOperationBytes,
1305
+ operationId, snapshot, spaceId: this.binding.spaceId });
1290
1306
  const operation = transition.operation;
1291
- if (utf8ByteLength(canonicalJson(operation)) > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) {
1307
+ const operationJson = canonicalJson(operation);
1308
+ const operationBytes = utf8ByteLength(operationJson);
1309
+ if (operationBytes > maximumOperationBytes) {
1310
+ throw new OhOperationSizeError(operationBytes, maximumOperationBytes);
1311
+ }
1312
+ if (operationBytes > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) {
1292
1313
  throw new RangeError("A direct libSQL operation exceeds its canonical byte bound.");
1293
1314
  }
1294
1315
  const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)";
@@ -1301,7 +1322,7 @@ class OhLibSqlStoreV1 implements OhStoreV1 {
1301
1322
  WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`,
1302
1323
  args: [operation.operationSha256, this.binding.spaceId, operation.sequence, operation.operationId,
1303
1324
  operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256,
1304
- canonicalJson(operation), operation.instant, this.binding.spaceId, current.generation,
1325
+ operationJson, operation.instant, this.binding.spaceId, current.generation,
1305
1326
  current.operationSha256],
1306
1327
  }];
1307
1328
  for (const [ordinal, change] of operation.changes.entries()) {