@hraness/oh 0.2.6 → 0.3.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 (55) hide show
  1. package/README.md +116 -12
  2. package/dist/canonical.d.ts.map +1 -1
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +91 -19
  5. package/dist/cloudflare-embedding.d.ts +104 -0
  6. package/dist/cloudflare-embedding.d.ts.map +1 -0
  7. package/dist/graph.d.ts.map +1 -1
  8. package/dist/index.js +90 -18
  9. package/dist/libsql-semantic.d.ts +111 -0
  10. package/dist/libsql-semantic.d.ts.map +1 -0
  11. package/dist/libsql.js +105 -18
  12. package/dist/memory-page.d.ts +2 -0
  13. package/dist/memory-page.d.ts.map +1 -0
  14. package/dist/memory-page.js +725 -0
  15. package/dist/memory-pages.d.ts +76 -0
  16. package/dist/memory-pages.d.ts.map +1 -0
  17. package/dist/memory.d.ts +1 -0
  18. package/dist/memory.d.ts.map +1 -1
  19. package/dist/memory.js +478 -18
  20. package/dist/projection-public.js +105 -18
  21. package/dist/projection-suss.js +105 -18
  22. package/dist/sdk.js +90 -18
  23. package/dist/semantic-cloud.d.ts +3 -0
  24. package/dist/semantic-cloud.d.ts.map +1 -0
  25. package/dist/semantic-cloud.js +1843 -0
  26. package/dist/semantic.d.ts.map +1 -1
  27. package/dist/semantic.js +104 -21
  28. package/dist/sqlite/index.js +90 -18
  29. package/dist/store.js +105 -18
  30. package/dist/sync.js +90 -18
  31. package/package.json +10 -2
  32. package/skills/oh/SKILL.md +28 -2
  33. package/spec/README.md +11 -3
  34. package/spec/manifest.json +9 -1
  35. package/spec/v1/cloudflare-embedding-profile.json +13 -0
  36. package/spec/v1/cloudflare-embedding-renderer.json +8 -0
  37. package/spec/v1/memory-page.md +153 -0
  38. package/spec/v1/memory-page.schema.json +154 -0
  39. package/spec/v1/memory.md +18 -0
  40. package/spec/v1/migration.md +13 -0
  41. package/spec/v1/semantic-cloud.md +87 -0
  42. package/src/canonical.ts +28 -13
  43. package/src/cli.ts +1 -1
  44. package/src/cloudflare-embedding.test.ts +306 -0
  45. package/src/cloudflare-embedding.ts +385 -0
  46. package/src/contracts.test.ts +20 -0
  47. package/src/graph.ts +63 -6
  48. package/src/libsql-semantic.test.ts +478 -0
  49. package/src/libsql-semantic.ts +1117 -0
  50. package/src/memory-page.ts +1 -0
  51. package/src/memory-pages.test.ts +277 -0
  52. package/src/memory-pages.ts +440 -0
  53. package/src/memory.ts +2 -0
  54. package/src/semantic-cloud.ts +2 -0
  55. package/src/semantic.ts +14 -3
package/README.md CHANGED
@@ -22,9 +22,9 @@ indexes derived and replaceable.
22
22
  - **Keep local state authoritative.** Records and operations live in one SQLite
23
23
  file you control. Sync is an explicit transport seam and accepts only
24
24
  fast-forward histories after an exact contract handshake.
25
- - **Treat search as a view.** FTS5 documents and optional local embeddings are
26
- derived from current record digests, so either index can be rebuilt without
27
- becoming graph authority.
25
+ - **Treat search as a view.** FTS5 documents, optional local embeddings, and a
26
+ separately scoped hosted cache are derived from current record digests, so
27
+ an index can be rebuilt without becoming graph authority.
28
28
  - **Derive without silently asserting.** Positive recursive rules run against
29
29
  one exact graph head and fact-pack digest. Their tuples and bounded proofs
30
30
  are deterministic, disposable output rather than accepted graph records.
@@ -40,16 +40,16 @@ direct libSQL authority also support Node 24 serverless runtimes. Install the
40
40
  exact current release from npm:
41
41
 
42
42
  ```sh
43
- bun add --global @hraness/oh@0.2.6
43
+ bun add --global @hraness/oh@0.3.0
44
44
  oh --help
45
45
  ```
46
46
 
47
47
  The identical package bytes and their checksum are available from the
48
- [immutable GitHub Release](https://github.com/hraness/oh/releases/tag/v0.2.6),
48
+ [immutable GitHub Release](https://github.com/hraness/oh/releases/tag/v0.3.0),
49
49
  including the mirrored
50
- [`hraness-oh-0.2.6.tgz`](https://github.com/hraness/oh/releases/download/v0.2.6/hraness-oh-0.2.6.tgz)
50
+ [`hraness-oh-0.3.0.tgz`](https://github.com/hraness/oh/releases/download/v0.3.0/hraness-oh-0.3.0.tgz)
51
51
  and
52
- [`SHA256SUMS`](https://github.com/hraness/oh/releases/download/v0.2.6/SHA256SUMS).
52
+ [`SHA256SUMS`](https://github.com/hraness/oh/releases/download/v0.3.0/SHA256SUMS).
53
53
 
54
54
  Oh writes to `.oh/oh.sqlite` and the `default` space unless you select another
55
55
  path or space. Keep `.oh/` out of source control.
@@ -107,7 +107,7 @@ For a project dependency, pin the same immutable release in `package.json`:
107
107
  ```json
108
108
  {
109
109
  "dependencies": {
110
- "@hraness/oh": "0.2.6"
110
+ "@hraness/oh": "0.3.0"
111
111
  }
112
112
  }
113
113
  ```
@@ -151,7 +151,10 @@ promise interface, `@hraness/oh/libsql` for a direct Node 24 or serverless
151
151
  authority, `@hraness/oh/sqlite` for the local Bun store, `@hraness/oh/sdk` for
152
152
  the local `Oh` facade, `@hraness/oh/sync` for transport seams,
153
153
  `@hraness/oh/projection` for recursive derived views, and
154
- `@hraness/oh/semantic` for the optional local embedding backend. The
154
+ `@hraness/oh/semantic` for the optional local embedding backend, and
155
+ `@hraness/oh/semantic-cloud` for the Cloudflare EmbeddingGemma plus direct
156
+ libSQL derived-cache adapter. Use the narrow stable `@hraness/oh/memory-page`
157
+ entrypoint for model-neutral page records and `.oh.md` interchange. The
155
158
  `@hraness/oh/experimental/memory` subpath composes host-bound working and
156
159
  canonical stores behind a smaller agent-facing surface.
157
160
 
@@ -345,6 +348,51 @@ proposal; it never promotes it. Read the
345
348
  [experimental memory specification](spec/v1/memory.md) for the complete
346
349
  authority and lifecycle boundary.
347
350
 
351
+ ## Use a memory page or `.oh.md` file
352
+
353
+ A memory page is an ordinary `edition` record with bounded Markdown, explicit
354
+ source observations, and a host-attestation receipt reference. It is useful
355
+ for readable summaries and long-running-agent notes without introducing a
356
+ second memory ontology. Its canonical `.oh.md` form contains the complete
357
+ record key, dependencies, digest, metadata, and body:
358
+
359
+ ```ts
360
+ import {
361
+ createOhMemoryPageRecordV1,
362
+ renderOhMemoryPageMarkdownV1,
363
+ } from "@hraness/oh/memory-page";
364
+
365
+ const page = createOhMemoryPageRecordV1({
366
+ dependencies: ["activity:memory-attestation"],
367
+ key: "edition:session-summary",
368
+ value: {
369
+ body: "## Current state\n\nThe provider rollout is paused before activation.",
370
+ createdAt: "2026-08-31T12:00:00.000Z",
371
+ format: "oh.memory-page.v1",
372
+ language: "en",
373
+ provenance: {
374
+ actorId: "host.memory",
375
+ attestationSha256: receiptSha256,
376
+ attestedAt: "2026-08-31T12:00:00.000Z",
377
+ kind: "host-attested",
378
+ v: 1,
379
+ },
380
+ sources: [],
381
+ summary: "The exact resumable session frontier.",
382
+ title: "Session frontier",
383
+ updatedAt: "2026-08-31T12:00:00.000Z",
384
+ v: 1,
385
+ },
386
+ });
387
+
388
+ const portable = renderOhMemoryPageMarkdownV1(page);
389
+ ```
390
+
391
+ The page never stores vectors, model IDs, scores, or provider configuration.
392
+ Treat its Markdown and source titles as untrusted data. See the
393
+ [memory-page specification](spec/v1/memory-page.md) for the exact format and
394
+ round-trip rules.
395
+
348
396
  ## Derive an exact projection
349
397
 
350
398
  The projection subpath is pure TypeScript and runs in Node 24 serverless
@@ -478,6 +526,62 @@ The exact V1 profile is documented in
478
526
  all inference stay local. Keyword mode remains available when QMD or the model
479
527
  is absent.
480
528
 
529
+ ## Add a hosted semantic cache
530
+
531
+ The hosted adapter uses the same source-record principle with a distinct,
532
+ profile-bound cache. It sends bounded inputs to Cloudflare Workers AI's
533
+ EmbeddingGemma model and stores only float32 vectors, input digests, record
534
+ digests, immutable generation membership, and a published pointer in direct
535
+ libSQL. It stores no title, body, record JSON, or query text.
536
+
537
+ ```ts
538
+ import { createClient } from "@libsql/client";
539
+ import {
540
+ OhCloudflareEmbeddingClientV1,
541
+ bootstrapOhLibSqlSemanticCacheV1,
542
+ openOhLibSqlSemanticCacheV1,
543
+ } from "@hraness/oh/semantic-cloud";
544
+
545
+ // Deploy once with a short-lived schema credential.
546
+ const schemaClient = createClient({
547
+ authToken: process.env.OH_SEMANTIC_SCHEMA_TOKEN!,
548
+ url: process.env.OH_SEMANTIC_DATABASE_URL!,
549
+ });
550
+ await bootstrapOhLibSqlSemanticCacheV1(schemaClient);
551
+ schemaClient.close();
552
+
553
+ const client = createClient({
554
+ authToken: process.env.OH_SEMANTIC_RUNTIME_TOKEN!,
555
+ url: process.env.OH_SEMANTIC_DATABASE_URL!,
556
+ });
557
+ const cache = await openOhLibSqlSemanticCacheV1(client, { closeClient: true });
558
+ const embedder = new OhCloudflareEmbeddingClientV1({
559
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
560
+ apiToken: process.env.CLOUDFLARE_WORKERS_AI_TOKEN!,
561
+ });
562
+
563
+ const staged = await cache.stage({
564
+ authorityId: "thread:research/epoch:1",
565
+ authoritySha256: snapshot.head.recordsSha256,
566
+ documents,
567
+ embeddingClient: embedder,
568
+ generation: snapshot.head.generation,
569
+ });
570
+ await cache.publish({
571
+ authorityId: "thread:research/epoch:1",
572
+ expectedPublishedGeneration: null,
573
+ generation: staged.generation,
574
+ });
575
+ ```
576
+
577
+ Search requires the exact current authority generation and record digests, and
578
+ returns nothing from a stale or concurrently replaced head. Purge writes a
579
+ permanent authority tombstone before deleting memberships and unused vectors.
580
+ The same authority ID cannot be resurrected; allocate a new epoch for a new
581
+ lifetime. Hosted failure is a missing convenience lane, never permission to
582
+ weaken exact graph or Datalog operations. Read the
583
+ [hosted semantic-cache specification](spec/v1/semantic-cloud.md).
584
+
481
585
  ## Sync through libSQL or Turso
482
586
 
483
587
  `createLibSqlOperationSyncTransportV1` accepts the `execute` and `batch` shape
@@ -545,9 +649,9 @@ keep remote sync explicit.
545
649
  You can also give an agent this prompt:
546
650
 
547
651
  ```text
548
- Install @hraness/oh@0.2.6 from npm and use its packaged Oh Agent Skill. The
549
- exact npm tarball and SHA256SUMS are mirrored by the immutable v0.2.6 Release at
550
- https://github.com/hraness/oh/releases/tag/v0.2.6. Verify the CLI with
652
+ Install @hraness/oh@0.3.0 from npm and use its packaged Oh Agent Skill. The
653
+ exact npm tarball and SHA256SUMS are mirrored by the immutable v0.3.0 Release at
654
+ https://github.com/hraness/oh/releases/tag/v0.3.0. Verify the CLI with
551
655
  `oh --help` and `oh version`.
552
656
  Do not create or modify an Oh database until I name its path and ask you to.
553
657
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,SAAS,EAAE,GAAG,UAAU,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAEhE,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,MAAM,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAE5E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAMxD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAG7F;AAgFD,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,SAAmB,GAAG,SAAS,CAc3F;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE/D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAEzD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAI/D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOrE;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,SAAM,GAAG,MAAM,GAAG,IAAI,CAK3E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,SAAY,GAAG,MAAM,GAAG,IAAI,CAUnF;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,OAAO,CAEzF;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,EAAE,CAU3F"}
1
+ {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,SAAS,EAAE,GAAG,UAAU,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,CAAC;AAEhE,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,MAAM,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAE5E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAMxD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAG7F;AA+FD,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,SAAmB,GAAG,SAAS,CAc3F;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE/D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAEzD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAI/D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOrE;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,SAAM,GAAG,MAAM,GAAG,IAAI,CAK3E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,SAAY,GAAG,MAAM,GAAG,IAAI,CAUnF;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,OAAO,CAEzF;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,EAAE,CAU3F"}
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- export declare const OH_PACKAGE_VERSION: "0.2.6";
2
+ export declare const OH_PACKAGE_VERSION: "0.3.0";
3
3
  export declare function runOhCli(arguments_: readonly string[]): Promise<number>;
4
4
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -63,17 +63,27 @@ function encodeCanonical(value, path, ancestors) {
63
63
  ancestors.add(value);
64
64
  try {
65
65
  if (Array.isArray(value)) {
66
- const encoded = [];
67
- for (let index = 0;index < value.length; index += 1) {
68
- if (!Object.hasOwn(value, index)) {
69
- throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
70
- }
71
- encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
66
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
67
+ const length = lengthDescriptor?.value;
68
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0) {
69
+ throw new OhValidationError("non-json-property", path, "array has an invalid length descriptor");
72
70
  }
73
- const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
74
- if (extraKeys.length > 0) {
71
+ const ownKeys2 = Reflect.ownKeys(value);
72
+ if (!ownKeys2.includes("length") || ownKeys2.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
75
73
  throw new OhValidationError("non-json-property", path, "array has non-index properties");
76
74
  }
75
+ const elements = [];
76
+ for (let index = 0;index < length; index += 1) {
77
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
78
+ if (descriptor === undefined) {
79
+ throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
80
+ }
81
+ if (!descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
82
+ throw new OhValidationError("non-json-property", `${path}[${index}]`, "must be an enumerable data property");
83
+ }
84
+ elements.push(descriptor.value);
85
+ }
86
+ const encoded = elements.map((element, index) => encodeCanonical(element, `${path}[${index}]`, ancestors));
77
87
  return `[${encoded.join(",")}]`;
78
88
  }
79
89
  if (!isPlainRecord(value)) {
@@ -83,19 +93,21 @@ function encodeCanonical(value, path, ancestors) {
83
93
  if (ownKeys.some((key) => typeof key !== "string")) {
84
94
  throw new OhValidationError("non-json-property", path, "object has a symbol property");
85
95
  }
96
+ const entries = [];
86
97
  const keys = ownKeys;
87
98
  for (const key of keys) {
88
99
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
89
100
  if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
90
101
  throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
91
102
  }
103
+ entries.push([key, descriptor.value]);
92
104
  }
93
- keys.sort();
94
- const entries = keys.map((key) => {
105
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
106
+ const encodedEntries = entries.map(([key, entryValue]) => {
95
107
  assertUnicodeScalarString(key, `${path}.<key>`);
96
- return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
108
+ return `${JSON.stringify(key)}:${encodeCanonical(entryValue, `${path}.${key}`, ancestors)}`;
97
109
  });
98
- return `{${entries.join(",")}}`;
110
+ return `{${encodedEntries.join(",")}}`;
99
111
  } finally {
100
112
  ancestors.delete(value);
101
113
  }
@@ -207,17 +219,70 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
207
219
  "view",
208
220
  "vocabulary"
209
221
  ];
222
+ var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
223
+ "dependencies",
224
+ "key",
225
+ "kind",
226
+ "recordSha256",
227
+ "v",
228
+ "value"
229
+ ];
230
+ function exactKnowledgeGraphRecordEnvelopeV1(value) {
231
+ try {
232
+ if (!isPlainRecord(value))
233
+ return null;
234
+ const ownKeys = Reflect.ownKeys(value);
235
+ if (ownKeys.length !== KNOWLEDGE_GRAPH_RECORD_KEYS_V1.length || ownKeys.some((key) => typeof key !== "string") || KNOWLEDGE_GRAPH_RECORD_KEYS_V1.some((key) => !ownKeys.includes(key)))
236
+ return null;
237
+ const detached = {};
238
+ for (const key of KNOWLEDGE_GRAPH_RECORD_KEYS_V1) {
239
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
240
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
241
+ return null;
242
+ detached[key] = descriptor.value;
243
+ }
244
+ return detached;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ function exactGraphDependenciesV1(value) {
250
+ try {
251
+ if (!Array.isArray(value))
252
+ return null;
253
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
254
+ const length = lengthDescriptor?.value;
255
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
256
+ return null;
257
+ const ownKeys = Reflect.ownKeys(value);
258
+ if (ownKeys.length !== length + 1 || !ownKeys.includes("length") || ownKeys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length)))
259
+ return null;
260
+ const detached = [];
261
+ for (let index = 0;index < length; index += 1) {
262
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
263
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
264
+ return null;
265
+ detached.push(descriptor.value);
266
+ }
267
+ return detached;
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
210
272
  function recordKey(value) {
211
273
  return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
212
274
  }
213
275
  function createKnowledgeGraphRecordV1(input) {
214
- if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
276
+ if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1)
215
277
  throw new TypeError("Invalid graph record input.");
278
+ const dependencyInput = exactGraphDependenciesV1(input.dependencies);
279
+ if (dependencyInput === null)
280
+ throw new TypeError("Invalid graph record dependencies.");
216
281
  const key = recordKey(input.key);
217
282
  const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
218
- if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
283
+ if (key === null || kind === undefined)
219
284
  throw new TypeError("Invalid graph record identity.");
220
- const dependencies = input.dependencies.map(recordKey);
285
+ const dependencies = dependencyInput.map(recordKey);
221
286
  if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
222
287
  throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
223
288
  }
@@ -229,10 +294,17 @@ function createKnowledgeGraphRecordV1(input) {
229
294
  return { ...payload, recordSha256: canonicalSha256(payload) };
230
295
  }
231
296
  function parseKnowledgeGraphRecordV1(value) {
232
- if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
297
+ const envelope = exactKnowledgeGraphRecordEnvelopeV1(value);
298
+ if (envelope === null)
233
299
  return null;
234
- const recordSha256 = parseSha256Hex(value.recordSha256);
235
- const { recordSha256: _digest, ...input } = value;
300
+ const recordSha256 = parseSha256Hex(envelope.recordSha256);
301
+ const input = {
302
+ dependencies: envelope.dependencies,
303
+ key: envelope.key,
304
+ kind: envelope.kind,
305
+ v: envelope.v,
306
+ value: envelope.value
307
+ };
236
308
  try {
237
309
  const created = createKnowledgeGraphRecordV1(input);
238
310
  return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;
@@ -3072,7 +3144,7 @@ class Oh {
3072
3144
 
3073
3145
  // src/cli.ts
3074
3146
  import { readFile } from "fs/promises";
3075
- var OH_PACKAGE_VERSION = "0.2.6";
3147
+ var OH_PACKAGE_VERSION = "0.3.0";
3076
3148
  var KNOWN_OPTIONS = new Set([
3077
3149
  "actor",
3078
3150
  "after",
@@ -0,0 +1,104 @@
1
+ import { type Sha256Hex } from "./canonical";
2
+ /** A hosted embedding space. It is intentionally distinct from the local QMD profile. */
3
+ export declare const OH_CLOUDFLARE_EMBEDDING_PROFILE_V1: Readonly<{
4
+ profileSha256: Sha256Hex;
5
+ dimensions: 768;
6
+ distance: "cosine";
7
+ documentFormat: "title: {title} | text: {content}";
8
+ inputUtf8Bytes: 448;
9
+ model: "@cf/google/embeddinggemma-300m";
10
+ normalization: "l2";
11
+ profileId: "oh.cloudflare.embeddinggemma.v1";
12
+ provider: "cloudflare.workers-ai";
13
+ queryFormat: "task: search result | query: {query}";
14
+ v: 1;
15
+ }>;
16
+ export declare const OH_SEMANTIC_RENDERER_V1: Readonly<{
17
+ rendererSha256: Sha256Hex;
18
+ documentFormat: "title: {title} | text: {content}";
19
+ inputUtf8Bytes: 448;
20
+ rendererId: "oh.embedding-input.utf8-chunks.v1";
21
+ split: "unicode-scalar-greedy";
22
+ v: 1;
23
+ }>;
24
+ export declare const OH_CLOUDFLARE_EMBEDDING_LIMITS_V1: Readonly<{
25
+ batchInputs: 32;
26
+ deadlineMs: 30000;
27
+ documentBytes: number;
28
+ inputUtf8Bytes: 448;
29
+ responseBytes: number;
30
+ renderedChunks: 256;
31
+ titleBytes: number;
32
+ }>;
33
+ export type OhRenderedEmbeddingInputV1 = Readonly<{
34
+ input: string;
35
+ inputSha256: Sha256Hex;
36
+ kind: "document" | "query";
37
+ utf8Bytes: number;
38
+ v: 1;
39
+ }>;
40
+ export type OhRenderedDocumentChunkV1 = Readonly<{
41
+ content: string;
42
+ input: OhRenderedEmbeddingInputV1;
43
+ ordinal: number;
44
+ title: string;
45
+ v: 1;
46
+ }>;
47
+ export type OhRenderedDocumentV1 = Readonly<{
48
+ chunks: readonly OhRenderedDocumentChunkV1[];
49
+ diagnostic: null | Readonly<{
50
+ code: "oversize-prefix" | "partial";
51
+ maximumChunks: number;
52
+ omittedUtf8Bytes: number;
53
+ v: 1;
54
+ }>;
55
+ sourceUtf8Bytes: number;
56
+ status: "complete" | "oversize" | "partial";
57
+ v: 1;
58
+ }>;
59
+ export declare class OhCloudflareEmbeddingError extends Error {
60
+ readonly code: "aborted" | "invalid-input" | "invalid-response" | "provider-unavailable";
61
+ readonly status: number | null;
62
+ constructor(code: OhCloudflareEmbeddingError["code"], message: string, status?: number | null);
63
+ }
64
+ export declare function renderOhCloudflareEmbeddingQueryV1(query: string): OhRenderedEmbeddingInputV1;
65
+ /**
66
+ * Renders every source scalar in order. A caller-selected chunk limit is made
67
+ * visible as `partial`; a title that leaves no content capacity is `oversize`.
68
+ */
69
+ export declare function renderOhCloudflareEmbeddingDocumentV1(input: Readonly<{
70
+ content: string;
71
+ maximumChunks?: number;
72
+ title: string;
73
+ }>): OhRenderedDocumentV1;
74
+ export type OhEmbeddingFetchV1 = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
75
+ export type OhCloudflareEmbeddingClientOptionsV1 = Readonly<{
76
+ accountId: string;
77
+ apiToken: string;
78
+ deadlineMs?: number;
79
+ fetch?: OhEmbeddingFetchV1;
80
+ maximumBatchInputs?: number;
81
+ maximumResponseBytes?: number;
82
+ }>;
83
+ /** Fixed Workers AI adapter; credentials are never included in surfaced errors. */
84
+ export declare class OhCloudflareEmbeddingClientV1 {
85
+ #private;
86
+ readonly profile: Readonly<{
87
+ profileSha256: Sha256Hex;
88
+ dimensions: 768;
89
+ distance: "cosine";
90
+ documentFormat: "title: {title} | text: {content}";
91
+ inputUtf8Bytes: 448;
92
+ model: "@cf/google/embeddinggemma-300m";
93
+ normalization: "l2";
94
+ profileId: "oh.cloudflare.embeddinggemma.v1";
95
+ provider: "cloudflare.workers-ai";
96
+ queryFormat: "task: search result | query: {query}";
97
+ v: 1;
98
+ }>;
99
+ constructor(options: OhCloudflareEmbeddingClientOptionsV1);
100
+ embed(inputs: readonly OhRenderedEmbeddingInputV1[], options?: Readonly<{
101
+ signal?: AbortSignal;
102
+ }>): Promise<readonly (readonly number[])[]>;
103
+ }
104
+ //# sourceMappingURL=cloudflare-embedding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-embedding.d.ts","sourceRoot":"","sources":["../src/cloudflare-embedding.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -61,17 +61,27 @@ function encodeCanonical(value, path, ancestors) {
61
61
  ancestors.add(value);
62
62
  try {
63
63
  if (Array.isArray(value)) {
64
- const encoded = [];
65
- for (let index = 0;index < value.length; index += 1) {
66
- if (!Object.hasOwn(value, index)) {
67
- throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
68
- }
69
- encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors));
64
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
65
+ const length = lengthDescriptor?.value;
66
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0) {
67
+ throw new OhValidationError("non-json-property", path, "array has an invalid length descriptor");
70
68
  }
71
- const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length));
72
- if (extraKeys.length > 0) {
69
+ const ownKeys2 = Reflect.ownKeys(value);
70
+ if (!ownKeys2.includes("length") || ownKeys2.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length))) {
73
71
  throw new OhValidationError("non-json-property", path, "array has non-index properties");
74
72
  }
73
+ const elements = [];
74
+ for (let index = 0;index < length; index += 1) {
75
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
76
+ if (descriptor === undefined) {
77
+ throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes");
78
+ }
79
+ if (!descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
80
+ throw new OhValidationError("non-json-property", `${path}[${index}]`, "must be an enumerable data property");
81
+ }
82
+ elements.push(descriptor.value);
83
+ }
84
+ const encoded = elements.map((element, index) => encodeCanonical(element, `${path}[${index}]`, ancestors));
75
85
  return `[${encoded.join(",")}]`;
76
86
  }
77
87
  if (!isPlainRecord(value)) {
@@ -81,19 +91,21 @@ function encodeCanonical(value, path, ancestors) {
81
91
  if (ownKeys.some((key) => typeof key !== "string")) {
82
92
  throw new OhValidationError("non-json-property", path, "object has a symbol property");
83
93
  }
94
+ const entries = [];
84
95
  const keys = ownKeys;
85
96
  for (const key of keys) {
86
97
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
87
98
  if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) {
88
99
  throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property");
89
100
  }
101
+ entries.push([key, descriptor.value]);
90
102
  }
91
- keys.sort();
92
- const entries = keys.map((key) => {
103
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
104
+ const encodedEntries = entries.map(([key, entryValue]) => {
93
105
  assertUnicodeScalarString(key, `${path}.<key>`);
94
- return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`;
106
+ return `${JSON.stringify(key)}:${encodeCanonical(entryValue, `${path}.${key}`, ancestors)}`;
95
107
  });
96
- return `{${entries.join(",")}}`;
108
+ return `{${encodedEntries.join(",")}}`;
97
109
  } finally {
98
110
  ancestors.delete(value);
99
111
  }
@@ -205,17 +217,70 @@ var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [
205
217
  "view",
206
218
  "vocabulary"
207
219
  ];
220
+ var KNOWLEDGE_GRAPH_RECORD_KEYS_V1 = [
221
+ "dependencies",
222
+ "key",
223
+ "kind",
224
+ "recordSha256",
225
+ "v",
226
+ "value"
227
+ ];
228
+ function exactKnowledgeGraphRecordEnvelopeV1(value) {
229
+ try {
230
+ if (!isPlainRecord(value))
231
+ return null;
232
+ const ownKeys = Reflect.ownKeys(value);
233
+ if (ownKeys.length !== KNOWLEDGE_GRAPH_RECORD_KEYS_V1.length || ownKeys.some((key) => typeof key !== "string") || KNOWLEDGE_GRAPH_RECORD_KEYS_V1.some((key) => !ownKeys.includes(key)))
234
+ return null;
235
+ const detached = {};
236
+ for (const key of KNOWLEDGE_GRAPH_RECORD_KEYS_V1) {
237
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
238
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
239
+ return null;
240
+ detached[key] = descriptor.value;
241
+ }
242
+ return detached;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ function exactGraphDependenciesV1(value) {
248
+ try {
249
+ if (!Array.isArray(value))
250
+ return null;
251
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
252
+ const length = lengthDescriptor?.value;
253
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
254
+ return null;
255
+ const ownKeys = Reflect.ownKeys(value);
256
+ if (ownKeys.length !== length + 1 || !ownKeys.includes("length") || ownKeys.some((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= length)))
257
+ return null;
258
+ const detached = [];
259
+ for (let index = 0;index < length; index += 1) {
260
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
261
+ if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined)
262
+ return null;
263
+ detached.push(descriptor.value);
264
+ }
265
+ return detached;
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
208
270
  function recordKey(value) {
209
271
  return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null;
210
272
  }
211
273
  function createKnowledgeGraphRecordV1(input) {
212
- if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies))
274
+ if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1)
213
275
  throw new TypeError("Invalid graph record input.");
276
+ const dependencyInput = exactGraphDependenciesV1(input.dependencies);
277
+ if (dependencyInput === null)
278
+ throw new TypeError("Invalid graph record dependencies.");
214
279
  const key = recordKey(input.key);
215
280
  const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind);
216
- if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord)
281
+ if (key === null || kind === undefined)
217
282
  throw new TypeError("Invalid graph record identity.");
218
- const dependencies = input.dependencies.map(recordKey);
283
+ const dependencies = dependencyInput.map(recordKey);
219
284
  if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) {
220
285
  throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive.");
221
286
  }
@@ -227,10 +292,17 @@ function createKnowledgeGraphRecordV1(input) {
227
292
  return { ...payload, recordSha256: canonicalSha256(payload) };
228
293
  }
229
294
  function parseKnowledgeGraphRecordV1(value) {
230
- if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256"))
295
+ const envelope = exactKnowledgeGraphRecordEnvelopeV1(value);
296
+ if (envelope === null)
231
297
  return null;
232
- const recordSha256 = parseSha256Hex(value.recordSha256);
233
- const { recordSha256: _digest, ...input } = value;
298
+ const recordSha256 = parseSha256Hex(envelope.recordSha256);
299
+ const input = {
300
+ dependencies: envelope.dependencies,
301
+ key: envelope.key,
302
+ kind: envelope.kind,
303
+ v: envelope.v,
304
+ value: envelope.value
305
+ };
234
306
  try {
235
307
  const created = createKnowledgeGraphRecordV1(input);
236
308
  return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null;