@hraness/oh 0.3.2 → 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 (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
@@ -0,0 +1,87 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ isOhConflictError,
5
+ isOhDependencyError,
6
+ isOhIntegrityError,
7
+ isOhOperationSizeError,
8
+ isOhProfileError,
9
+ OhConflictError,
10
+ OhDependencyError,
11
+ OhIntegrityError,
12
+ OhOperationSizeError,
13
+ OhProfileError,
14
+ } from "./errors";
15
+
16
+ const CORE_ERRORS = [
17
+ {
18
+ brand: "@hraness/oh/OhConflictError/v1",
19
+ ErrorClass: OhConflictError,
20
+ guard: isOhConflictError,
21
+ name: "OhConflictError",
22
+ },
23
+ {
24
+ brand: "@hraness/oh/OhIntegrityError/v1",
25
+ ErrorClass: OhIntegrityError,
26
+ guard: isOhIntegrityError,
27
+ name: "OhIntegrityError",
28
+ },
29
+ {
30
+ brand: "@hraness/oh/OhDependencyError/v1",
31
+ ErrorClass: OhDependencyError,
32
+ guard: isOhDependencyError,
33
+ name: "OhDependencyError",
34
+ },
35
+ {
36
+ brand: "@hraness/oh/OhProfileError/v1",
37
+ ErrorClass: OhProfileError,
38
+ guard: isOhProfileError,
39
+ name: "OhProfileError",
40
+ },
41
+ ] as const;
42
+
43
+ describe("Oh public error identity", () => {
44
+ for (const { brand, ErrorClass, guard, name } of CORE_ERRORS) {
45
+ test(`brands ${name} without changing its public constructor`, () => {
46
+ const error = new ErrorClass("preserved message");
47
+ expect(Error.isError(error)).toBe(true);
48
+ expect(error).toBeInstanceOf(Error);
49
+ expect(error).toBeInstanceOf(ErrorClass);
50
+ expect(guard(error)).toBe(true);
51
+ expect(error).toMatchObject({ message: "preserved message", name });
52
+ expect(Object.keys(error)).toEqual(["name"]);
53
+
54
+ const copied = Object.create(Error.prototype) as Record<PropertyKey, unknown>;
55
+ Object.defineProperty(copied, Symbol.for(brand), {
56
+ configurable: false,
57
+ value: true,
58
+ writable: false,
59
+ });
60
+ expect(Error.isError(copied)).toBe(false);
61
+ expect(guard(copied)).toBe(false);
62
+ expect(copied instanceof ErrorClass).toBe(false);
63
+ });
64
+ }
65
+
66
+ test("preserves native subclass identity while exposing the branded base", () => {
67
+ class NarrowConflictError extends OhConflictError {}
68
+
69
+ const base = new OhConflictError("base");
70
+ const narrow = new NarrowConflictError("narrow");
71
+ expect(base).not.toBeInstanceOf(NarrowConflictError);
72
+ expect(narrow).toBeInstanceOf(NarrowConflictError);
73
+ expect(narrow).toBeInstanceOf(OhConflictError);
74
+ expect(isOhConflictError(narrow)).toBe(true);
75
+ });
76
+
77
+ test("does not make every operation-size error an instance of a subclass", () => {
78
+ class NarrowSizeError extends OhOperationSizeError {}
79
+
80
+ const base = new OhOperationSizeError(2, 1);
81
+ const narrow = new NarrowSizeError(2, 1);
82
+ expect(base).not.toBeInstanceOf(NarrowSizeError);
83
+ expect(narrow).toBeInstanceOf(NarrowSizeError);
84
+ expect(narrow).toBeInstanceOf(OhOperationSizeError);
85
+ expect(isOhOperationSizeError(narrow)).toBe(true);
86
+ });
87
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,185 @@
1
+ export const OH_OPERATION_SIZE_ERROR_CODE_V1 = "oh.operation-size.v1" as const;
2
+
3
+ const OH_CONFLICT_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhConflictError/v1");
4
+ const OH_DEPENDENCY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhDependencyError/v1");
5
+ const OH_INTEGRITY_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhIntegrityError/v1");
6
+ const OH_OPERATION_SIZE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhOperationSizeError/v1");
7
+ const OH_PROFILE_ERROR_BRAND_V1 = Symbol.for("@hraness/oh/OhProfileError/v1");
8
+
9
+ function immutableOwnValue(value: object, key: PropertyKey): unknown {
10
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
11
+ return descriptor !== undefined
12
+ && descriptor.get === undefined
13
+ && descriptor.set === undefined
14
+ && descriptor.configurable === false
15
+ && descriptor.writable === false
16
+ ? descriptor.value
17
+ : undefined;
18
+ }
19
+
20
+ function brandNativeError(value: Error, brand: symbol): void {
21
+ Object.defineProperty(value, brand, {
22
+ configurable: false,
23
+ enumerable: false,
24
+ value: true,
25
+ writable: false,
26
+ });
27
+ }
28
+
29
+ function hasNativeErrorBrand(value: unknown, brand: symbol): boolean {
30
+ try {
31
+ return Error.isError(value) && immutableOwnValue(value, brand) === true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function hasNativeSubclassInstance(constructor: Function, value: unknown): boolean {
38
+ return Function.prototype[Symbol.hasInstance].call(constructor, value);
39
+ }
40
+
41
+ /** Recognizes a compare-and-swap conflict across separately bundled Oh entrypoints. */
42
+ export function isOhConflictError(value: unknown): value is OhConflictError {
43
+ return hasNativeErrorBrand(value, OH_CONFLICT_ERROR_BRAND_V1);
44
+ }
45
+
46
+ export class OhConflictError extends Error {
47
+ static override [Symbol.hasInstance](value: unknown): boolean {
48
+ return this === OhConflictError
49
+ ? isOhConflictError(value)
50
+ : hasNativeSubclassInstance(this, value);
51
+ }
52
+
53
+ constructor(message: string) {
54
+ super(message);
55
+ this.name = "OhConflictError";
56
+ brandNativeError(this, OH_CONFLICT_ERROR_BRAND_V1);
57
+ }
58
+ }
59
+
60
+ /** Recognizes an authority-integrity failure across separately bundled Oh entrypoints. */
61
+ export function isOhIntegrityError(value: unknown): value is OhIntegrityError {
62
+ return hasNativeErrorBrand(value, OH_INTEGRITY_ERROR_BRAND_V1);
63
+ }
64
+
65
+ export class OhIntegrityError extends Error {
66
+ static override [Symbol.hasInstance](value: unknown): boolean {
67
+ return this === OhIntegrityError
68
+ ? isOhIntegrityError(value)
69
+ : hasNativeSubclassInstance(this, value);
70
+ }
71
+
72
+ constructor(message: string) {
73
+ super(message);
74
+ this.name = "OhIntegrityError";
75
+ brandNativeError(this, OH_INTEGRITY_ERROR_BRAND_V1);
76
+ }
77
+ }
78
+
79
+ /** Recognizes a missing-dependency failure across separately bundled Oh entrypoints. */
80
+ export function isOhDependencyError(value: unknown): value is OhDependencyError {
81
+ return hasNativeErrorBrand(value, OH_DEPENDENCY_ERROR_BRAND_V1);
82
+ }
83
+
84
+ export class OhDependencyError extends Error {
85
+ static override [Symbol.hasInstance](value: unknown): boolean {
86
+ return this === OhDependencyError
87
+ ? isOhDependencyError(value)
88
+ : hasNativeSubclassInstance(this, value);
89
+ }
90
+
91
+ constructor(message: string) {
92
+ super(message);
93
+ this.name = "OhDependencyError";
94
+ brandNativeError(this, OH_DEPENDENCY_ERROR_BRAND_V1);
95
+ }
96
+ }
97
+
98
+ /** Recognizes a store-profile refusal across separately bundled Oh entrypoints. */
99
+ export function isOhProfileError(value: unknown): value is OhProfileError {
100
+ return hasNativeErrorBrand(value, OH_PROFILE_ERROR_BRAND_V1);
101
+ }
102
+
103
+ export class OhProfileError extends Error {
104
+ static override [Symbol.hasInstance](value: unknown): boolean {
105
+ return this === OhProfileError
106
+ ? isOhProfileError(value)
107
+ : hasNativeSubclassInstance(this, value);
108
+ }
109
+
110
+ constructor(message: string) {
111
+ super(message);
112
+ this.name = "OhProfileError";
113
+ brandNativeError(this, OH_PROFILE_ERROR_BRAND_V1);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Recognizes this precommit refusal across separately bundled Oh entrypoints.
119
+ * Native Error identity plus immutable branded fields excludes copied plain
120
+ * objects while preserving one stable discriminator for package consumers.
121
+ */
122
+ export function isOhOperationSizeError(value: unknown): value is OhOperationSizeError {
123
+ try {
124
+ if (!Error.isError(value) || !(value instanceof RangeError)) return false;
125
+ const operationBytes = immutableOwnValue(value, "operationBytes");
126
+ const maximumOperationBytes = immutableOwnValue(value, "maximumOperationBytes");
127
+ return immutableOwnValue(value, OH_OPERATION_SIZE_ERROR_BRAND_V1) === true
128
+ && immutableOwnValue(value, "code") === OH_OPERATION_SIZE_ERROR_CODE_V1
129
+ && Number.isSafeInteger(operationBytes)
130
+ && (operationBytes as number) > 0
131
+ && Number.isSafeInteger(maximumOperationBytes)
132
+ && (maximumOperationBytes as number) > 0
133
+ && (operationBytes as number) > (maximumOperationBytes as number);
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ export class OhOperationSizeError extends RangeError {
140
+ declare readonly code: typeof OH_OPERATION_SIZE_ERROR_CODE_V1;
141
+ declare readonly maximumOperationBytes: number;
142
+ declare readonly operationBytes: number;
143
+
144
+ static override [Symbol.hasInstance](value: unknown): boolean {
145
+ return this === OhOperationSizeError
146
+ ? isOhOperationSizeError(value)
147
+ : hasNativeSubclassInstance(this, value);
148
+ }
149
+
150
+ constructor(operationBytes: number, maximumOperationBytes: number) {
151
+ if (!Number.isSafeInteger(operationBytes) || operationBytes < 1
152
+ || !Number.isSafeInteger(maximumOperationBytes) || maximumOperationBytes < 1
153
+ || operationBytes <= maximumOperationBytes) {
154
+ throw new TypeError("Invalid Oh operation size refusal.");
155
+ }
156
+ super(`The ${operationBytes}-byte operation exceeds the host-declared ${maximumOperationBytes}-byte canonical bound.`);
157
+ this.name = "OhOperationSizeError";
158
+ Object.defineProperties(this, {
159
+ [OH_OPERATION_SIZE_ERROR_BRAND_V1]: {
160
+ configurable: false,
161
+ enumerable: false,
162
+ value: true,
163
+ writable: false,
164
+ },
165
+ code: {
166
+ configurable: false,
167
+ enumerable: true,
168
+ value: OH_OPERATION_SIZE_ERROR_CODE_V1,
169
+ writable: false,
170
+ },
171
+ maximumOperationBytes: {
172
+ configurable: false,
173
+ enumerable: true,
174
+ value: maximumOperationBytes,
175
+ writable: false,
176
+ },
177
+ operationBytes: {
178
+ configurable: false,
179
+ enumerable: true,
180
+ value: operationBytes,
181
+ writable: false,
182
+ },
183
+ });
184
+ }
185
+ }
package/src/graph.ts CHANGED
@@ -19,12 +19,12 @@ export const OH_GRAPH_LIMITS_V1 = Object.freeze({
19
19
  recordsPerSnapshot: 65_536,
20
20
  });
21
21
 
22
- export const OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
22
+ export const OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = Object.freeze([
23
23
  "activity", "assertion", "context", "dependency-manifest", "edition", "entity",
24
24
  "evidence", "identity-operation", "inquiry", "inquiry-event", "review-decision",
25
25
  "rights-decision", "schema", "shape", "statement", "type-membership", "view",
26
26
  "vocabulary",
27
- ] as const;
27
+ ] as const);
28
28
  export type KnowledgeGraphRecordKindV1 = (typeof OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1)[number];
29
29
 
30
30
  export type KnowledgeGraphRecordV1 = Readonly<{
@@ -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()) {