diff --git a/README.md b/README.md index db0ef27..1a61270 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,22 @@ indexes derived and replaceable. - **Treat search as a view.** FTS5 documents and optional local embeddings are derived from current record digests, so either index can be rebuilt without becoming graph authority. +- **Derive without silently asserting.** Positive recursive rules run against + one exact graph head and fact-pack digest. Their tuples and bounded proofs + are deterministic, disposable output rather than accepted graph records. +- **Remember without conflating authority.** An experimental facade composes a + purgeable working authority with one pinned canonical head while preserving + lane, conflict, record, and proof provenance. ## Install and first run -[Bun 1.3.14 or newer](https://bun.sh/docs/installation) is required. Install -the current immutable release directly from GitHub: +[Bun 1.3.14 or newer](https://bun.sh/docs/installation) is required for the +CLI, local SDK, and SQLite authority. The runtime-neutral store contracts and +direct libSQL authority also support Node 24 serverless runtimes. Install the +current immutable release directly from GitHub: ```sh -bun add --global github:hraness/oh#v0.1.1 +bun add --global github:hraness/oh#v0.2.0 oh --help ``` @@ -92,7 +100,7 @@ For a project dependency, pin the same immutable release in `package.json`: ```json { "dependencies": { - "@hraness/oh": "github:hraness/oh#v0.1.1" + "@hraness/oh": "github:hraness/oh#v0.2.0" } } ``` @@ -131,9 +139,216 @@ retry `OhConflictError` blindly. Read the new head and records, reconcile the intended change, then submit a new operation. The root entrypoint exports canonical JSON, ontology, schema, graph, operation, -and sync contracts. Use `@hraness/oh/sqlite` for the local store, -`@hraness/oh/sdk` for the `Oh` facade, `@hraness/oh/sync` for transport seams, -and `@hraness/oh/semantic` for the optional local embedding backend. +store, and sync contracts. Use `@hraness/oh/store` for the runtime-neutral +promise interface, `@hraness/oh/libsql` for a direct Node 24 or serverless +authority, `@hraness/oh/sqlite` for the local Bun store, `@hraness/oh/sdk` for +the local `Oh` facade, `@hraness/oh/sync` for transport seams, +`@hraness/oh/projection` for recursive derived views, and +`@hraness/oh/semantic` for the optional local embedding backend. The +`@hraness/oh/experimental/memory` subpath composes host-bound working and +canonical stores behind a smaller agent-facing surface. + +## Open a scoped working store + +Working memory uses the same V1 graph and operation bytes under a different +storage lifecycle. The host chooses and retains the realm binding. Application +code receives the promise-based store and keeps the host object that can purge +a working space out of agent tools. A model-facing adapter should expose strict +semantic ingress and bounded query methods, not generic commit or change-feed +access. + +```ts +import { createClient } from "@libsql/client"; +import { + bootstrapOhLibSqlAuthorityV1, + createOhLibSqlStoreAuthorityV1, +} from "@hraness/oh/libsql"; +import { OH_WORKING_STORE_PROFILE_V1 } from "@hraness/oh/store"; + +// Run once during deployment with a short-lived schema credential. +const schemaClient = createClient({ + authToken: process.env.OH_SCHEMA_TOKEN!, + url: process.env.OH_DATABASE_URL!, +}); +await bootstrapOhLibSqlAuthorityV1(schemaClient); +schemaClient.close(); + +// Runtime opens verify the schema and execute no DDL. +const runtimeClient = createClient({ + authToken: process.env.OH_RUNTIME_TOKEN!, + url: process.env.OH_DATABASE_URL!, +}); +const authority = await createOhLibSqlStoreAuthorityV1(runtimeClient, { + profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "tenant:example/thread:research", + spaceId: "thread:research", +}); + +const store = authority.store; +console.log(await store.head()); +``` + +The working profile disables operation replication. Dependency-closure export +remains available for explicit reviewed adoption. `purgeWorkingSpace` exists +only on `authority.host`; do not expose that object or raw database credentials +through a model tool. Read the [store-port specification](spec/v1/store.md) for +exact snapshot, change-feed, codec ingress, closure, and purge behavior. + +## Compose working and canonical memory + +The experimental memory facade uses the same Oh kernel twice, not a separate +memory database model. Trusted host code supplies two distinct physical store +handles, their expected binding digests, one exact canonical head, sealed +working codecs, digest-identified fact extractors, and a closed registry of +named projection programs: + +```ts +import { createOhMemoryAgentV1 } from "@hraness/oh/experimental/memory"; + +const memory = await createOhMemoryAgentV1({ + actorId: "research.memory-agent", + canonical: { + authorityId: "project-reviewed", + expectedBindingSha256: canonical.store.binding.bindingSha256, + expectedHead: await canonical.store.head(), + store: canonical.store, + }, + nominationRoutes: [{ + destinationPurpose: "kb.review", + nominationId: "knowledge-review", + }], + programs: [{ + programId: "project.dependencies", + purpose: "answer.research", + query, + rulePack, + }], + working: { + authorityId: "thread-working", + codecs, + expectedBindingSha256: working.store.binding.bindingSha256, + store: working.store, + }, +}); + +const result = await memory.query({ + programId: "project.dependencies", + v: 1, +}); +``` + +The returned object has only `remember`, `query`, `explain`, and `nominate`. +The host fixes the working actor, each program purpose, and every nomination +destination before exposing those methods. `remember` accepts an idempotency +request plus semantic changes and returns a locator-free working-lane receipt; +it does not accept caller-supplied actor or time claims. The object cannot +select a store, install a rule, write canonical knowledge, sync, or purge. +Query identity binds both exact physical lanes and all projection policy; +conflicting same-key records remain visible. Explanation requires a bounded, +short-lived opaque capability bound to the exact result. Nomination chooses +only a host-registered route and creates a verified working dependency-closure +proposal; it never promotes it. Read the +[experimental memory specification](spec/v1/memory.md) for the complete +authority and lifecycle boundary. + +## Derive an exact projection + +The projection subpath is pure TypeScript and runs in Node 24 serverless +functions without loading SQLite. A snapshot binds the current space head and +complete record-reference set. A fact pack binds the deterministic extractor +that translated those records into relations. Rules and queries are typed data, +not strings or executable callbacks. + +```ts +import { + OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1, + createOhProjectionDatasetV1, + createOhProjectionLiteralV1, + createOhProjectionQueryV1, + createOhProjectionRecordFactsV1, + createOhProjectionRulePackV1, + createOhProjectionRuleV1, + createOhProjectionSnapshotV1, + evaluateOhProjectionV1, + ohProjectionVariableV1 as variable, +} from "@hraness/oh/projection"; + +const records = oh.store.snapshotRecords(); +const snapshot = createOhProjectionSnapshotV1({ + head: oh.head(), + records, + spaceId: oh.store.spaceId, +}); +const dataset = createOhProjectionDatasetV1({ + extractorSha256: OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1.extractorSha256, + factPackId: OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1.factPackId, + factPackRevision: OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1.factPackRevision, + facts: createOhProjectionRecordFactsV1(records), + snapshot, +}); + +const x = variable("x"); +const y = variable("y"); +const z = variable("z"); +const literal = (relation: string, ...terms: ReturnType[]) => + createOhProjectionLiteralV1({ relation, terms }); +const rulePack = createOhProjectionRulePackV1({ + rulePackId: "example.dependencies", + rulePackRevision: 1, + rules: [ + createOhProjectionRuleV1({ + body: [literal("oh.dependency", x, y)], + head: literal("depends", x, y), + ruleId: "depends.direct", + }), + createOhProjectionRuleV1({ + body: [literal("depends", x, y), literal("oh.dependency", y, z)], + head: literal("depends", x, z), + ruleId: "depends.transitive", + }), + ], +}); +const query = createOhProjectionQueryV1({ + find: ["x", "z"], + queryId: "all.dependencies", + where: [literal("depends", x, z)], +}); + +const result = evaluateOhProjectionV1({ dataset, query, rulePack, snapshot }); +console.log(result.rows); +``` + +`result.authority` is always `derived`. Oh does not commit a result, elevate an +agent assertion, or make a proof authoritative. Changing the snapshot, +extracted fact set, rule pack, or query produces a new identity and requires a +full rebuild. + +The reference evaluator favors bounded, transparent correctness. It supports +positive recursion and set semantics; it does not yet support negation, +aggregation, arithmetic, or incremental invalidation. An optional compatibility +lane evaluates the same rules with exactly `@suss/datalog@0.20.0` and returns a +result only after every relation agrees with the reference evaluator: + +```sh +bun add @suss/datalog@0.20.0 +``` + +```ts +import { evaluateOhProjectionWithSussV1 } from "@hraness/oh/experimental/projection-suss"; + +const checked = evaluateOhProjectionWithSussV1({ + dataset, + query, + rulePack, + snapshot, +}); +``` + +Suss does not expose an execution-budget hook. Its adapter therefore applies a +conservative finite-domain admission bound and refuses programs it cannot prove +will stay inside the requested tuple ceiling. The built-in evaluator remains +available for those programs. The compatibility lane deliberately runs both +engines; it is an equivalence check, not a performance backend. ## Add local semantic search @@ -219,6 +434,9 @@ For offline transfer, `oh sync export` writes a bounded bundle to stdout and control, tenant isolation, backup, retry, and remote availability. - Divergent histories do not merge automatically. Oh returns an explicit conflict and leaves reconciliation policy to the consumer. +- Projection tuples and proofs are derived cache output. Persisting or + publishing them as knowledge requires an explicit application-level review + and a new authoritative graph operation. Read [SECURITY.md](SECURITY.md) for the complete public threat model. @@ -233,7 +451,7 @@ keep remote sync explicit. You can also give an agent this prompt: ```text -Install hraness/oh and its Oh Agent Skill from the immutable v0.1.1 tag at +Install hraness/oh and its Oh Agent Skill from the immutable v0.2.0 tag at https://github.com/hraness/oh. Verify the CLI with `oh --help` and `oh version`. Do not create or modify an Oh database until I name its path and ask you to. ``` @@ -243,7 +461,7 @@ Do not create or modify an Oh database until I name its path and ask you to. - **Install and prove the local path:** follow [Install and first run](#install-and-first-run). - **Embed Oh in a tool:** use [the SDK](#use-the-sdk), then select the narrow - package subpath for SQLite, sync, or optional semantics. + package subpath for SQLite, sync, projection, or optional semantics. - **Give Oh to an agent:** install the [Oh Agent Skill](skills/oh/SKILL.md) and keep its database, space, sync target, and mutation authority explicit. - **Implement or change a contract:** begin with the @@ -263,8 +481,10 @@ document. The current contract is V1: - [Schema evolution](spec/v1/schema-evolution.md) - [Graph and operations](spec/v1/graph.md) - [SQLite storage](spec/v1/storage.md) +- [Store ports, profiles, and direct libSQL authority](spec/v1/store.md) - [Sync protocol](spec/v1/sync.md) - [Local embedding profile](spec/v1/embedding.md) +- [Derived projections](spec/v1/projection.md) - [Compatibility and migration](spec/v1/migration.md) The JSON Schemas describe exchange envelopes. Runtime parsers additionally diff --git a/SECURITY.md b/SECURITY.md index d37b1c7..3e0aba2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,8 +29,42 @@ problem that has already been fixed there. - Contract, operation, record, and bundle digests detect accidental or hostile mutation. They do not encrypt data or authenticate an actor. - The libSQL sync seam validates contract bytes and fast-forward history. The - client application remains responsible for transport security, credentials, - access control, tenant isolation, backups, and service configuration. + direct libSQL authority additionally enforces compare-and-swap batches and + exact realm/profile bindings. The client application remains responsible for + transport security, credentials, access control, tenant isolation, backups, + and service configuration. +- Direct libSQL schema creation is an explicit bootstrap API. Use a short-lived + schema credential for that step and a narrower data credential for runtime + opens, commits, reads, and host-controlled purge. +- A working-store purge removes rows reachable through the supported authority + and leaves a content-free receipt. It does not erase provider backups, + replicas outside that authority, exported dependency closures, logs created + by the host, or bytes copied by a process that already held raw credentials. + Match retention claims to the complete custody and backup system. +- Store profiles are host control metadata. V1 operation digests do not attest + to that profile, and callers with raw database or filesystem access remain + outside the profile API boundary. +- The experimental memory facade is an in-process capability boundary, not a + tenant authenticator or sandbox. The host must bind its canonical and working + store handles, enforce tenant and session authorization before every call, + keep raw credentials and purge handles out of model tools, and never reuse a + facade across authorization domains. +- Memory fact extractors are trusted host code. They receive complete record + values, and their declared digest identifies policy but does not sandbox, + authenticate, or attest to a JavaScript function. Review extractors as code, + make them deterministic, and give them no ambient authority they do not need. + Invocation count, returned fact count, and retained bytes are bounded, but a + synchronous extractor's own execution time and temporary allocation are + outside the evaluator's resource guarantees. +- Explanation tokens are short-lived, process-local bearer capabilities bound + to one exact result. They can explain multiple rows until expiry or eviction; + the process keeps a bounded aggregate evidence cache. Do not log, persist, + share across tenants, or treat them as evidence that a caller may read either + underlying store. +- Composite query rows remain derived even when all visible premises are + canonical. A prepared nomination is content-addressed transport for a later + destination-owned review; it is not approval, synchronization, or permission + to mutate canonical knowledge. - Oh does not redact record values. Do not write secrets or sensitive research into a space unless the database, filesystem, backups, and sync destination have the required protection. diff --git a/bun.lock b/bun.lock index ad731ca..fe45df0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,18 +5,25 @@ "": { "name": "@hraness/oh", "devDependencies": { + "@suss/datalog": "0.20.0", "@types/bun": "1.3.14", "typescript": "5.9.3", }, "peerDependencies": { + "@libsql/client": ">=0.17.4 <1", + "@suss/datalog": "0.20.0", "@tobilu/qmd": "2.5.3", }, "optionalPeers": [ + "@libsql/client", + "@suss/datalog", "@tobilu/qmd", ], }, }, "packages": { + "@suss/datalog": ["@suss/datalog@0.20.0", "", {}, "sha512-a9NM+MwvnMqGkeV1EJlaJ8I68kcNcdVZsZJinCd05weLD2Flr6ck1Awwmi9uoznfapVwFVQq4iCTBLDPWPtIWQ=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], diff --git a/dist/cli.d.ts b/dist/cli.d.ts index 0e51c80..dd13ae4 100644 --- a/dist/cli.d.ts +++ b/dist/cli.d.ts @@ -1,4 +1,4 @@ #!/usr/bin/env bun -export declare const OH_PACKAGE_VERSION: "0.1.1"; +export declare const OH_PACKAGE_VERSION: "0.2.0"; export declare function runOhCli(arguments_: readonly string[]): Promise; //# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js index d55e94d..5193eba 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -1026,10 +1026,13 @@ function parseOhContractManifestV1(value) { class OhRecordCodecRegistry { #codecs = new Map; + #sealed = false; register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } parse(kind, value) { @@ -1046,6 +1049,27 @@ class OhRecordCodecRegistry { has(kind) { return this.#codecs.has(kind); } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } } // src/operation.ts @@ -1348,6 +1372,573 @@ async function searchOhV1(input) { import { mkdirSync } from "fs"; import { dirname } from "path"; +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} + // src/sqlite/driver.ts import { existsSync } from "fs"; import { Database } from "bun:sqlite"; @@ -1409,9 +2000,22 @@ function withImmediateTransaction(database, work) { throw error; } } +function withReadTransaction(database, work) { + database.exec("BEGIN"); + try { + const result = work(); + database.exec("COMMIT"); + return result; + } catch (error) { + try { + database.exec("ROLLBACK"); + } catch {} + throw error; + } +} // src/sqlite/migrations.ts -var OH_SQLITE_SCHEMA_VERSION = 1; +var OH_SQLITE_SCHEMA_VERSION = 2; var OH_SQLITE_MIGRATIONS = Object.freeze([ Object.freeze({ name: "0001_oh_core", @@ -1520,6 +2124,32 @@ CREATE VIRTUAL TABLE oh_search_fts USING fts5( CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence); CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key); CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key); +` + }), + Object.freeze({ + name: "0002_store_realms", + version: 2, + sql: ` +CREATE TABLE oh_space_bindings ( + space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id), + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64), + binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64), + binding_json TEXT NOT NULL CHECK(json_valid(binding_json)), + created_at TEXT NOT NULL +) STRICT; + +CREATE TABLE oh_space_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64), + prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64), + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64), + receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json)) +) STRICT; ` }) ]); @@ -1556,27 +2186,51 @@ function applyOhSqliteMigrations(database) { } // src/sqlite/store.ts -var EMPTY_RECORDS_SHA256 = canonicalSha256([]); - -class OhConflictError extends Error { - constructor(message) { - super(message); - this.name = "OhConflictError"; +var EMPTY_RECORDS_SHA2562 = canonicalSha256([]); +var OPERATION_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +var BINDING_COLUMNS = `space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json`; +var PURGE_COLUMNS = `space_id, binding_sha256, prior_operation_sha256, prior_sequence, + purged_at, receipt_sha256, receipt_json`; +function parseStoredOperationRow(row, expected = {}) { + let value; + try { + value = JSON.parse(row.operation_json); + } catch { + throw new OhIntegrityError("A stored operation is not JSON."); + } + const operation = parseOhOperationV1(value); + if (operation === null || canonicalJson(operation) !== row.operation_json || row.operation_sha256 !== operation.operationSha256 || row.space_id !== operation.spaceId || row.sequence !== operation.sequence || row.operation_id !== operation.operationId || row.parent_operation_sha256 !== operation.parentOperationSha256 || row.graph_revision_sha256 !== operation.graphRevisionSha256 || row.records_sha256 !== operation.recordsSha256 || row.instant !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) { + throw new OhIntegrityError("Stored operation columns do not match their canonical envelope."); } + return operation; } - -class OhIntegrityError extends Error { - constructor(message) { - super(message); - this.name = "OhIntegrityError"; +function parseStoredBindingRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.binding_json); + } catch { + throw new OhIntegrityError("A store binding is not JSON."); + } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== row.binding_json || binding.spaceId !== expectedSpaceId || row.space_id !== binding.spaceId || row.realm_id !== binding.realmId || row.profile_id !== binding.profile.profileId || row.profile_kind !== binding.profile.profileKind || row.profile_sha256 !== binding.profile.profileSha256 || row.binding_sha256 !== binding.bindingSha256) { + throw new OhIntegrityError("Stored binding columns do not match their canonical envelope."); } + return binding; } - -class OhDependencyError extends Error { - constructor(message) { - super(message); - this.name = "OhDependencyError"; +function parseStoredPurgeRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.receipt_json); + } catch { + throw new OhIntegrityError("A purge receipt is not JSON."); + } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== row.receipt_json || receipt.spaceId !== expectedSpaceId || row.space_id !== receipt.spaceId || row.binding_sha256 !== receipt.bindingSha256 || row.prior_operation_sha256 !== receipt.priorHead.operationSha256 || row.prior_sequence !== receipt.priorHead.sequence || row.purged_at !== receipt.purgedAt || row.receipt_sha256 !== receipt.receiptSha256) { + throw new OhIntegrityError("Stored purge columns do not match their canonical receipt."); } + return receipt; } function parseHead(row) { const operationSha256 = row.head_operation_sha256 === null ? null : parseSha256Hex(row.head_operation_sha256); @@ -1665,6 +2319,12 @@ class OhSqliteStore { if (this.#closed) throw new Error("The Oh store is closed."); } + #assertOperationReplication() { + const binding = this.binding(); + if (binding !== null && !binding.profile.capabilities.operationReplication) { + throw new OhProfileError("This bound store profile forbids operation replication."); + } + } #registerContract() { const manifestJson = canonicalJson(OH_CONTRACT_MANIFEST_V1); this.database.query(`INSERT INTO oh_contracts(contract_id, contract_sha256, manifest_json, created_at) @@ -1676,12 +2336,45 @@ class OhSqliteStore { } ensureSpace() { this.#assertOpen(); - const now = canonicalNow(); - this.database.query(`INSERT INTO oh_spaces( - space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, - records_sha256, sequence, created_at, updated_at - ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA256, now, now); - return this.head(); + return withImmediateTransaction(this.database, () => { + const purged = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (purged !== null) { + throw new OhPurgedSpaceError(parseStoredPurgeRow(purged, this.spaceId)); + } + const now = canonicalNow(); + this.database.query(`INSERT INTO oh_spaces( + space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, + records_sha256, sequence, created_at, updated_at + ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA2562, now, now); + return this.head(); + }); + } + bind(bindingValue) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId) { + throw new OhProfileError("The store binding does not identify this space."); + } + const bindingJson = canonicalJson(binding); + this.database.query(`INSERT INTO oh_space_bindings( + space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, binding.profile.profileSha256, binding.bindingSha256, bindingJson, canonicalNow()); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + throw new OhIntegrityError("The persisted store binding disappeared."); + const persisted = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persisted) !== bindingJson) { + throw new OhProfileError("The space is already bound to a different realm or profile."); + } + return binding; + } + binding() { + this.#assertOpen(); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + return null; + return parseStoredBindingRow(row, this.spaceId); } head() { this.#assertOpen(); @@ -1743,6 +2436,48 @@ class OhSqliteStore { }); return { graphRevisionSha256, records, recordsSha256 }; } + #assertCurrentHeadAuthority(head) { + const summary = this.database.query(`SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_operations WHERE space_id = ?`).get(this.spaceId); + if (summary === null || summary.count !== head.sequence || head.sequence === 0 && (summary.minimum !== null || summary.maximum !== null) || head.sequence > 0 && (summary.minimum !== 1 || summary.maximum !== head.sequence)) { + throw new OhIntegrityError("The operation history does not exactly cover the current space head."); + } + if (head.sequence === 0) + return; + if (head.operationSha256 === null) + throw new OhIntegrityError("A nonempty space head has no operation digest."); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, head.sequence); + if (row === null) + throw new OhIntegrityError("The current space head operation is missing."); + const operation = parseStoredOperationRow(row, { + spaceId: this.spaceId, + operationSha256: head.operationSha256 + }); + if (operation.sequence !== head.sequence || operation.graphRevisionSha256 !== head.graphRevisionSha256 || operation.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The current space head differs from its canonical operation."); + } + } + #assertOperationReachable(operation, head) { + if (operation.sequence < 1 || operation.sequence > head.sequence) { + throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head."); + } + const rows = this.database.query(`SELECT operation_sha256, parent_operation_sha256, sequence + FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence); + if (rows.length !== head.sequence - operation.sequence + 1) { + throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head."); + } + let priorSha256 = operation.parentOperationSha256; + for (let index = 0;index < rows.length; index += 1) { + const row = rows[index]; + if (row === undefined || row.sequence !== operation.sequence + index || row.parent_operation_sha256 !== priorSha256 || index === 0 && row.operation_sha256 !== operation.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain."); + } + priorSha256 = row.operation_sha256; + } + if (priorSha256 !== head.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest."); + } + } #persist(operation) { this.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, @@ -1804,17 +2539,17 @@ class OhSqliteStore { if (changes.length === 0 || changes.length > 8192) throw new TypeError("A commit needs 1 through 8192 changes."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND operation_id = ?").get(this.spaceId, operationId); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operationId); if (duplicate !== null) { - const existing = parseOhOperationV1(JSON.parse(duplicate.operation_json)); - if (existing === null) - throw new OhIntegrityError("The stored idempotent operation is invalid."); + const existing = parseStoredOperationRow(duplicate, { operationId, spaceId: this.spaceId }); + this.#assertOperationReachable(existing, head); if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) { throw new OhConflictError("The operation ID is already bound to different content."); } return existing; } - const head = this.head(); if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) { throw new OhConflictError("The expected head does not match the current space head."); } @@ -1838,18 +2573,25 @@ class OhSqliteStore { } importOperation(value) { this.#assertOpen(); + this.#assertOperationReplication(); const operation = parseOhOperationV1(value); if (operation === null || operation.spaceId !== this.spaceId) throw new OhIntegrityError("Invalid imported operation."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE operation_sha256 = ?").get(operation.operationSha256); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`).get(operation.operationSha256); if (duplicate !== null) { - if (canonicalJson(JSON.parse(duplicate.operation_json)) !== canonicalJson(operation)) { + const existing = parseStoredOperationRow(duplicate, { + operationSha256: operation.operationSha256, + spaceId: this.spaceId + }); + this.#assertOperationReachable(existing, head); + if (canonicalJson(existing) !== canonicalJson(operation)) { throw new OhIntegrityError("An operation digest is bound to different bytes."); } return { imported: false, operation }; } - const head = this.head(); if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) { throw new OhConflictError("The imported operation does not extend the local head."); } @@ -1863,15 +2605,117 @@ class OhSqliteStore { } exportOperations(afterSequence = 0, limit = 1000) { this.#assertOpen(); + this.#assertOperationReplication(); if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) throw new RangeError("afterSequence must be nonnegative."); const boundedLimit = normalizeLimit(limit); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?").all(this.spaceId, afterSequence, boundedLimit); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null || canonicalJson(operation) !== row.operation_json) - throw new OhIntegrityError("A stored operation is invalid."); - return operation; + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`).all(this.spaceId, afterSequence, boundedLimit); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + } + #headAt(reference) { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) + throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) + return emptyOhHeadV1(); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, parsed.sequence); + if (row === null) + throw new OhConflictError("The requested head is not present in this space."); + const operation = parseStoredOperationRow(row, { spaceId: this.spaceId }); + if (operation.spaceId !== this.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + snapshotAtHead(options = {}) { + this.#assertOpen(); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + return withReadTransaction(this.database, () => { + const current = this.head(); + const target = options.head === undefined ? current : this.#headAt(options.head); + if (target.sequence > current.sequence) + throw new OhConflictError("The requested head is ahead of this space."); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, target.sequence); + const operations = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + const snapshot = replayOhOperationsV1(this.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Operation replay does not reproduce the requested head."); + } + return snapshot; + }); + } + changesSince(fromValue, options = {}) { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) + throw new TypeError("Invalid change-feed cursor."); + const limit = normalizeLimit(options.limit, 100, 1000); + return withReadTransaction(this.database, () => { + const current = this.head(); + const fromHead = this.#headAt(from); + const through = options.through === undefined ? current : this.#headAt(options.through); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one local history prefix."); + } + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? AND sequence <= ? + ORDER BY sequence LIMIT ?`).all(this.spaceId, fromHead.sequence, through.sequence, limit + 1); + const parsed = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (parsed.length > limit + 1) + throw new OhIntegrityError("The change feed exceeded its requested page bound."); + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior = fromHead; + for (const operation of parsed) { + if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { + from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, + operations, + through, + to, + v: 1 + }; + }); + } + exportDependencyClosure(input) { + const binding = parseOhStoreBindingV1(input.binding); + if (binding === null || binding.spaceId !== this.spaceId || canonicalJson(this.binding()) !== canonicalJson(binding)) { + throw new OhProfileError("Dependency closure export requires the exact persisted store binding."); + } + if (!binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This store profile does not permit dependency-closure export."); + } + const snapshot = this.snapshotAtHead({ + ...input.head === undefined ? {} : { head: input.head }, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords } + }); + return createOhDependencyClosureV1({ + binding, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }, + roots: input.roots, + snapshot }); } get(key) { @@ -1917,13 +2761,9 @@ class OhSqliteStore { } log(limit = 50) { this.#assertOpen(); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence DESC LIMIT ?").all(this.spaceId, normalizeLimit(limit)); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null) - throw new OhIntegrityError("The stored operation is invalid."); - return operation; - }); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? ORDER BY sequence DESC LIMIT ?`).all(this.spaceId, normalizeLimit(limit)); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); } searchKeyword(query, limit = 20) { this.#assertOpen(); @@ -1979,36 +2819,27 @@ class OhSqliteStore { if (integrity?.integrity_check !== "ok") throw new OhIntegrityError("SQLite integrity_check failed."); const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0; - const operations = storedCount <= 1000 ? this.exportOperations(0, 1000) : this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence").all(this.spaceId).map((row) => { - let value; - try { - value = JSON.parse(row.operation_json); - } catch { - throw new OhIntegrityError("A stored operation is not JSON."); - } - if (canonicalJson(value) !== row.operation_json) - throw new OhIntegrityError("A stored operation is not canonical JSON."); - const parsed = parseOhOperationV1(value); - if (parsed === null) - throw new OhIntegrityError("A stored operation is invalid."); - return parsed; - }); + const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (operations.length !== storedCount) + throw new OhIntegrityError("Operation count changed during verification."); return this.#verifyOperations(operations); } #verifyOperations(operations) { const records = new Map; const materializedBy = new Map; + const operationIds = new Set; let head = { generation: 0, graphRevisionSha256: null, operationSha256: null, - recordsSha256: EMPTY_RECORDS_SHA256, + recordsSha256: EMPTY_RECORDS_SHA2562, sequence: 0, v: 1 }; for (const operation of operations) { - if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) + if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) throw new OhIntegrityError("Operation replay chain is broken."); + operationIds.add(operation.operationId); for (const change of operation.changes) { if (change.kind === "put") { records.set(change.record.key, change.record); @@ -2085,6 +2916,66 @@ class OhSqliteStore { contract() { return { manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION }; } + purgeWorkingSpace(bindingValue, purgedAt = canonicalNow()) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + return withImmediateTransaction(this.database, () => { + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const persistedBinding = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persistedBinding) !== canonicalJson(binding)) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const receipt = createOhSpacePurgeReceiptV1({ binding, priorHead: this.head(), purgedAt }); + this.database.query(`INSERT INTO oh_space_purges(space_id, binding_sha256, + prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + VALUES (?, ?, ?, ?, ?, ?, ?)`).run(this.spaceId, binding.bindingSha256, receipt.priorHead.operationSha256, receipt.priorHead.sequence, receipt.purgedAt, receipt.receiptSha256, canonicalJson(receipt)); + this.database.query("DELETE FROM oh_search_fts WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_search_documents WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_dependencies WHERE space_id = ?").run(this.spaceId); + this.database.query(`DELETE FROM oh_operation_records WHERE operation_sha256 IN + (SELECT operation_sha256 FROM oh_operations WHERE space_id = ?)`).run(this.spaceId); + this.database.query("DELETE FROM oh_sync_outbox WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_sync_state WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_records WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_operations WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_space_bindings WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_spaces WHERE space_id = ?").run(this.spaceId); + const directTables = [ + "oh_spaces", + "oh_space_bindings", + "oh_operations", + "oh_records", + "oh_dependencies", + "oh_search_documents", + "oh_sync_outbox", + "oh_sync_state" + ]; + for (const table of directTables) { + const count = this.database.query(`SELECT count(*) AS count FROM ${table} WHERE space_id = ?`).get(this.spaceId)?.count; + if (count !== 0) + throw new OhIntegrityError(`Space purge left rows in ${table}.`); + } + const operationRecords = this.database.query(`SELECT count(*) AS count + FROM oh_operation_records AS materialized JOIN oh_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ?`).get(this.spaceId)?.count; + const searchRows = this.database.query("SELECT count(*) AS count FROM oh_search_fts WHERE space_id = ?").get(this.spaceId)?.count; + if (operationRecords !== 0 || searchRows !== 0) { + throw new OhIntegrityError("Space purge left derived private payload rows."); + } + const receiptRow = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (receiptRow === null || canonicalJson(parseStoredPurgeRow(receiptRow, this.spaceId)) !== canonicalJson(receipt)) { + throw new OhIntegrityError("The stored purge receipt differs from the requested purge."); + } + return receipt; + }); + } close() { if (this.#closed) return; @@ -2181,7 +3072,7 @@ class Oh { // src/cli.ts import { readFile } from "fs/promises"; -var OH_PACKAGE_VERSION = "0.1.1"; +var OH_PACKAGE_VERSION = "0.2.0"; var KNOWN_OPTIONS = new Set([ "actor", "after", diff --git a/dist/contract.d.ts b/dist/contract.d.ts index 5417639..60661be 100644 --- a/dist/contract.d.ts +++ b/dist/contract.d.ts @@ -24,5 +24,10 @@ export declare class OhRecordCodecRegistry { register(codec: OhRecordCodec): this; parse(kind: KnowledgeGraphRecordKindV1, value: unknown): JsonValue | null; has(kind: KnowledgeGraphRecordKindV1): boolean; + /** Parses only through an explicitly registered codec. */ + parseRequired(kind: KnowledgeGraphRecordKindV1, value: unknown): JsonValue | null; + /** Prevents the validation policy from changing after an ingress is created. */ + seal(): this; + get sealed(): boolean; } //# sourceMappingURL=contract.d.ts.map \ No newline at end of file diff --git a/dist/contract.d.ts.map b/dist/contract.d.ts.map index e7fb257..8b7ecea 100644 --- a/dist/contract.d.ts.map +++ b/dist/contract.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EACjC,KAAK,0BAA0B,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,UAAU,EAAE,OAAO,iBAAiB,CAAC;IACrC,cAAc,EAAE,SAAS,CAAC;IAC1B,kBAAkB,EAAE,OAAO,0BAA0B,CAAC;IACtD,eAAe,EAAE,OAAO,sBAAsB,CAAC;IAC/C,WAAW,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,mBAAmB,EAAE,OAAO,2BAA2B,CAAC;IACxD,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAWH,eAAO,MAAM,uBAAuB,EAAE,oBAGpC,CAAC;AAEH,uEAAuE;AACvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAKrF;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,IAAI,QAAQ,CAAC;IACpE,IAAI,EAAE,0BAA0B,CAAC;IACjC,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC;CACjC,CAAC,CAAC;AAEH,uFAAuF;AACvF,qBAAa,qBAAqB;;IAGhC,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAMpC,KAAK,CAAC,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI;IAMzE,GAAG,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO;CAG/C"} \ No newline at end of file +{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EACjC,KAAK,0BAA0B,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,UAAU,EAAE,OAAO,iBAAiB,CAAC;IACrC,cAAc,EAAE,SAAS,CAAC;IAC1B,kBAAkB,EAAE,OAAO,0BAA0B,CAAC;IACtD,eAAe,EAAE,OAAO,sBAAsB,CAAC;IAC/C,WAAW,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,mBAAmB,EAAE,OAAO,2BAA2B,CAAC;IACxD,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAWH,eAAO,MAAM,uBAAuB,EAAE,oBAGpC,CAAC;AAEH,uEAAuE;AACvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAKrF;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,IAAI,QAAQ,CAAC;IACpE,IAAI,EAAE,0BAA0B,CAAC;IACjC,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC;CACjC,CAAC,CAAC;AAEH,uFAAuF;AACvF,qBAAa,qBAAqB;;IAIhC,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAOpC,KAAK,CAAC,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI;IAMzE,GAAG,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO;IAI9C,0DAA0D;IAC1D,aAAa,CAAC,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI;IAWjF,gFAAgF;IAChF,IAAI,IAAI,IAAI;IAKZ,IAAI,MAAM,IAAI,OAAO,CAEpB;CACF"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 2a67ea0..a74a3fe 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -4,5 +4,6 @@ export * from "./graph"; export * from "./ontology"; export * from "./operation"; export * from "./schema"; +export * from "./store"; export * from "./sync"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index 1480dbe..12a339d 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 88ab85d..3ddef8e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1024,10 +1024,13 @@ function parseOhContractManifestV1(value) { class OhRecordCodecRegistry { #codecs = new Map; + #sealed = false; register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } parse(kind, value) { @@ -1044,6 +1047,27 @@ class OhRecordCodecRegistry { has(kind) { return this.#codecs.has(kind); } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } } // src/operation.ts @@ -1295,18 +1319,594 @@ function createLibSqlOperationSyncTransportV1(client) { } }; } +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} export { + verifyOhDependencyClosureV1, + verifyOhDependencyClosureAgainstV1, verifyKnowledgeValueV1, verifyKnowledgeSchemaEvolutionV1, utf8ByteLength, + transitionOhSnapshotV1, synchronizeOhStoreV1, sortUnique, sha256Hex, safeCode, + replayOhOperationsV1, reduceKnowledgeGraphRevisionsV1, parseSha256Hex, parseOhSyncBundleV1, + parseOhStoreProfileV1, + parseOhStoreBindingV1, + parseOhSpacePurgeReceiptV1, parseOhOperationV1, + parseOhHeadV1, + parseOhHeadRefV1, + parseOhDependencyClosureV1, parseOhContractManifestV1, parseKnowledgeVocabularyRevisionV1, parseKnowledgeValueV1, @@ -1334,8 +1934,13 @@ export { isPlainRecord, hasExactKeys, graphRevisionSha256V1, + emptyOhHeadV1, createOhSyncBundleV1, + createOhStoreProfileV1, + createOhStoreBindingV1, + createOhSpacePurgeReceiptV1, createOhOperationV1, + createOhDependencyClosureV1, createLibSqlOperationSyncTransportV1, createKnowledgeVocabularyRevisionV1, createKnowledgeStatementV1, @@ -1353,7 +1958,14 @@ export { canonicalJson, boundedText, OhValidationError, + OhSemanticBundleIngressV1, OhRecordCodecRegistry, + OhPurgedSpaceError, + OhProfileError, + OhIntegrityError, + OhDependencyError, + OhConflictError, + OH_WORKING_STORE_PROFILE_V1, OH_SYNC_PROTOCOL_V1, OH_SCHEMA_KINDS_V1, OH_SCHEMA_FORMAT_VERSION_V1, @@ -1369,6 +1981,8 @@ export { OH_KNOWLEDGE_ACTIVITY_KINDS_V1, OH_GRAPH_LIMITS_V1, OH_GRAPH_FORMAT_VERSION_V1, + OH_DEPENDENCY_CLOSURE_LIMITS_V1, OH_CONTRACT_MANIFEST_V1, - OH_CONTRACT_ID_V1 + OH_CONTRACT_ID_V1, + OH_CANONICAL_STORE_PROFILE_V1 }; diff --git a/dist/libsql.d.ts b/dist/libsql.d.ts new file mode 100644 index 0000000..d98458f --- /dev/null +++ b/dist/libsql.d.ts @@ -0,0 +1,42 @@ +import { type Sha256Hex } from "./canonical"; +import { type OhStoreAuthorityV1, type OhStoreProfileV1 } from "./store"; +export type OhLibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string; +export type OhLibSqlStatementV1 = Readonly<{ + args?: readonly OhLibSqlValueV1[]; + sql: string; +}>; +export type OhLibSqlResultV1 = Readonly<{ + rows: readonly (Readonly> | readonly unknown[])[]; + rowsAffected?: number; +}>; +/** Structural subset implemented by `@libsql/client` clients. */ +export interface OhLibSqlClientV1 { + batch(statements: readonly OhLibSqlStatementV1[], mode?: "deferred" | "read" | "write"): Promise; + close?(): void; + execute(statement: OhLibSqlStatementV1 | string): Promise; +} +export type OhLibSqlStoreAuthorityOptionsV1 = Readonly<{ + closeClient?: boolean; + profile?: OhStoreProfileV1; + realmId?: string; + spaceId?: string; +}>; +export declare const OH_LIBSQL_STORE_LIMITS_V1: Readonly<{ + changesPerCommit: 64; + changeFeedLimit: 7; + dependenciesPerCommit: 512; + historyBytes: number; + historyOperations: 16384; + operationBytes: number; + providerResponseBytes: 9000000; + snapshotComponentBytes: number; +}>; +/** One-time schema operation for a client authorized to create authority tables. */ +export declare function bootstrapOhLibSqlAuthorityV1(client: OhLibSqlClientV1): Promise>; +/** Opens a direct libSQL/Turso authority; this is not operation-log sync. */ +export declare function createOhLibSqlStoreAuthorityV1(client: OhLibSqlClientV1, options?: OhLibSqlStoreAuthorityOptionsV1): Promise; +//# sourceMappingURL=libsql.d.ts.map \ No newline at end of file diff --git a/dist/libsql.d.ts.map b/dist/libsql.d.ts.map new file mode 100644 index 0000000..7865758 --- /dev/null +++ b/dist/libsql.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"libsql.d.ts","sourceRoot":"","sources":["../src/libsql.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAMrB,OAAO,EAuBL,KAAK,kBAAkB,EAGvB,KAAK,gBAAgB,EAGtB,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,IAAI,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1G,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IAAE,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC/F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAEH,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CACH,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,GACnC,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,OAAO,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;EASpC,CAAC;AAuVH,oFAAoF;AACpF,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,QAAQ,CAAC;IAAE,YAAY,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,CAAC,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC,CAsBxE;AAu+BD,6EAA6E;AAC7E,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,kBAAkB,CAAC,CAsC7B"} \ No newline at end of file diff --git a/dist/libsql.js b/dist/libsql.js new file mode 100644 index 0000000..e21dee0 --- /dev/null +++ b/dist/libsql.js @@ -0,0 +1,2473 @@ +// src/canonical.ts +import { createHash, randomBytes } from "node:crypto"; + +class OhValidationError extends Error { + code; + path; + constructor(code, path, message) { + super(`${path}: ${message}`); + this.name = "OhValidationError"; + this.code = code; + this.path = path; + } +} +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} +function assertUnicodeScalarString(value, path) { + for (let index = 0;index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(index + 1); + if (!(next >= 56320 && next <= 57343)) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + index += 1; + } else if (code >= 56320 && code <= 57343) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + } +} +function encodeCanonical(value, path, ancestors) { + if (value === null || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "string") { + assertUnicodeScalarString(value, path); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new OhValidationError("non-json-number", path, "must be finite"); + } + if (Object.is(value, -0)) { + throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical"); + } + return JSON.stringify(value); + } + if (typeof value !== "object" || value === null) { + throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`); + } + if (ancestors.has(value)) { + throw new OhValidationError("cycle", path, "contains a cycle"); + } + ancestors.add(value); + try { + if (Array.isArray(value)) { + const encoded = []; + for (let index = 0;index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes"); + } + encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors)); + } + const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)); + if (extraKeys.length > 0) { + throw new OhValidationError("non-json-property", path, "array has non-index properties"); + } + return `[${encoded.join(",")}]`; + } + if (!isPlainRecord(value)) { + throw new OhValidationError("non-plain-object", path, "must be a plain object"); + } + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== "string")) { + throw new OhValidationError("non-json-property", path, "object has a symbol property"); + } + const keys = ownKeys; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) { + throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property"); + } + } + keys.sort(); + const entries = keys.map((key) => { + assertUnicodeScalarString(key, `${path}.`); + return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`; + }); + return `{${entries.join(",")}}`; + } finally { + ancestors.delete(value); + } +} +function canonicalJson(value) { + return encodeCanonical(value, "$", new Set); +} +function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) { + if (utf8ByteLength(text) > maximumBytes) { + throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new OhValidationError("invalid-json", "$", "is not valid JSON"); + } + if (canonicalJson(value) !== text) { + throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical"); + } + return value; +} +function utf8ByteLength(value) { + return Buffer.byteLength(value, "utf8"); +} +function sha256Hex(value) { + return createHash("sha256").update(value).digest("hex"); +} +function canonicalSha256(value) { + return sha256Hex(canonicalJson(value)); +} +function parseSha256Hex(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null; +} +function parseCanonicalInstantV1(value) { + if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) { + return null; + } + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null; +} +function canonicalNow() { + return new Date().toISOString(); +} +function safeCode(value, maximumLength = 128) { + return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function orderedUnique(values, key) { + return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)); +} +function sortUnique(values, key) { + const sorted = [...values].sort((left, right) => { + const leftKey = key(left); + const rightKey = key(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); + if (!orderedUnique(sorted, key)) { + throw new OhValidationError("duplicate", "$", "contains duplicate canonical values"); + } + return sorted; +} + +// src/graph.ts +var OH_GRAPH_FORMAT_VERSION_V1 = 1; +var OH_GRAPH_LIMITS_V1 = Object.freeze({ + changesPerOperation: 8192, + dependenciesPerRecord: 4096, + recordBytes: 1024 * 1024, + recordsPerSnapshot: 65536 +}); +var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [ + "activity", + "assertion", + "context", + "dependency-manifest", + "edition", + "entity", + "evidence", + "identity-operation", + "inquiry", + "inquiry-event", + "review-decision", + "rights-decision", + "schema", + "shape", + "statement", + "type-membership", + "view", + "vocabulary" +]; +function recordKey(value) { + return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function createKnowledgeGraphRecordV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies)) + throw new TypeError("Invalid graph record input."); + const key = recordKey(input.key); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind); + if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) + throw new TypeError("Invalid graph record identity."); + const dependencies = input.dependencies.map(recordKey); + if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) { + throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive."); + } + const valueJson = canonicalJson(input.value); + if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) { + throw new RangeError("Graph record value exceeds its canonical byte limit."); + } + const payload = { dependencies, key, kind, v: 1, value: input.value }; + return { ...payload, recordSha256: canonicalSha256(payload) }; +} +function parseKnowledgeGraphRecordV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256")) + return null; + const recordSha256 = parseSha256Hex(value.recordSha256); + const { recordSha256: _digest, ...input } = value; + try { + const created = createKnowledgeGraphRecordV1(input); + return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null; + } catch { + return null; + } +} +function knowledgeGraphRecordRefV1(record) { + return { + dependencies: record.dependencies, + key: record.key, + kind: record.kind, + sha256: record.recordSha256, + v: 1 + }; +} +function changeKey(change) { + return change.kind === "put" ? change.record.key : change.key; +} +function canonicalKnowledgeGraphChangesV1(changes) { + const normalized = []; + for (const change of changes) { + if (!isPlainRecord(change) || change.v !== 1) + throw new TypeError("Invalid graph change."); + if (change.kind === "put") { + const record = parseKnowledgeGraphRecordV1(change.record); + if (record === null) + throw new TypeError("Invalid graph record in change."); + normalized.push({ kind: "put", record, v: 1 }); + } else if (change.kind === "tombstone") { + const key = recordKey(change.key); + const priorSha256 = parseSha256Hex(change.priorSha256); + if (key === null || priorSha256 === null) + throw new TypeError("Invalid graph tombstone."); + normalized.push({ key, kind: "tombstone", priorSha256, v: 1 }); + } else + throw new TypeError("Unknown graph change kind."); + } + return sortUnique(normalized, changeKey); +} +function graphRevisionSha256V1(input) { + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + const operationId = safeCode(input.operationId); + const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256); + const recordsSha256 = parseSha256Hex(input.recordsSha256); + const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null; + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) { + throw new TypeError("Invalid graph revision digest input."); + } + return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 }); +} + +// src/ontology.ts +var OH_ONTOLOGY_VERSION_V1 = "1.0.0"; +var OH_CONTRACT_ID_V1 = "oh.ontology.v1"; +var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({ + dimensions: 64, + listValues: 256, + qualifiers: 128, + statementBytes: 256 * 1024, + textBytes: 64 * 1024 +}); + +// src/schema.ts +var OH_SCHEMA_FORMAT_VERSION_V1 = 1; + +// src/contract.ts +var manifestPayload = Object.freeze({ + contractId: OH_CONTRACT_ID_V1, + graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1, + ontologyVersion: OH_ONTOLOGY_VERSION_V1, + recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1, + v: 1 +}); +var OH_CONTRACT_MANIFEST_V1 = Object.freeze({ + ...manifestPayload, + contractSha256: canonicalSha256(manifestPayload) +}); +class OhRecordCodecRegistry { + #codecs = new Map; + #sealed = false; + register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); + if (this.#codecs.has(codec.kind)) + throw new TypeError(`A codec is already registered for ${codec.kind}.`); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); + return this; + } + parse(kind, value) { + const codec = this.#codecs.get(kind); + if (codec !== undefined) + return codec.parse(value); + try { + canonicalJson(value); + return value; + } catch { + return null; + } + } + has(kind) { + return this.#codecs.has(kind); + } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } +} +// src/operation.ts +var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024; +function parsePayload(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "changes", + "contractId", + "graphRevisionSha256", + "instant", + "operationId", + "parentOperationSha256", + "recordsSha256", + "sequence", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes)) + return null; + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const spaceId = safeCode(value.spaceId); + const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256); + const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const instant = parseCanonicalInstantV1(value.instant); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null; + let changes; + try { + changes = canonicalKnowledgeGraphChangesV1(value.changes); + } catch { + return null; + } + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) + return null; + return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? { + actorId, + changes, + contractId: OH_CONTRACT_ID_V1, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256, + recordsSha256, + sequence, + spaceId, + v: 1 + } : null; +} +function createOhOperationV1(input) { + const payload = parsePayload(input); + if (payload === null) + throw new TypeError("Invalid Oh operation payload."); + const operation = { ...payload, operationSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) { + throw new RangeError("Oh operation exceeds its canonical byte limit."); + } + return operation; +} +function parseOhOperationV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256")) + return null; + const operationSha256 = parseSha256Hex(value.operationSha256); + const { operationSha256: _digest, ...input } = value; + const payload = parsePayload(input); + return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null; +} + +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} + +// src/libsql.ts +var OH_LIBSQL_STORE_LIMITS_V1 = Object.freeze({ + changesPerCommit: 64, + changeFeedLimit: 7, + dependenciesPerCommit: 512, + historyBytes: 4 * 1024 * 1024, + historyOperations: 16384, + operationBytes: 512 * 1024, + providerResponseBytes: 9000000, + snapshotComponentBytes: 6 * 1024 * 1024 +}); +var AUTHORITY_SCHEMA_NAME = "oh.libsql-authority.v1"; +var AUTHORITY_SCHEMA_VERSION = 1; +var EMPTY_RECORDS_SHA2562 = canonicalSha256([]); +var PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256, + prior_sequence, purged_at, receipt_sha256, receipt_json + FROM oh_authority_purges WHERE space_id = ?`; +var BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind, + profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`; +var OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +var OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB)) + + 2 * (length(operation.operation_sha256) + length(operation.space_id) + + length(operation.operation_id) + coalesce(length(operation.parent_operation_sha256), 0) + + length(operation.graph_revision_sha256) + length(operation.records_sha256) + + length(operation.instant)) + 512`; +var RECORD_RESPONSE_BYTES = `2 * length(CAST(record.record_json AS BLOB)) + + 2 * (length(record.record_key) + length(record.kind) + length(record.record_sha256) + + length(record.operation_sha256)) + 384`; +var DEPENDENCY_RESPONSE_BYTES = `2 * (length(dependency.record_key) + + length(dependency.dependency_key)) + 192`; +var OPERATION_RECORD_RESPONSE_BYTES = `2 * (length(materialized.space_id) + + length(materialized.operation_sha256) + length(materialized.record_key) + + length(materialized.change_kind) + length(materialized.record_sha256)) + 320`; +var AUTHORITY_SCHEMA_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS oh_authority_schemas ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + schema_sha256 TEXT NOT NULL, + applied_at TEXT NOT NULL +) STRICT`; +var AUTHORITY_SCHEMA_STATEMENTS = Object.freeze([ + `CREATE TABLE IF NOT EXISTS oh_authority_contracts ( + contract_id TEXT PRIMARY KEY, + contract_sha256 TEXT NOT NULL, + manifest_json TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_spaces ( + space_id TEXT PRIMARY KEY, + contract_id TEXT NOT NULL, + generation INTEGER NOT NULL CHECK(generation >= 0), + head_operation_sha256 TEXT, + graph_revision_sha256 TEXT, + records_sha256 TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence >= 0), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + CHECK(generation = sequence) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_operations ( + operation_sha256 TEXT PRIMARY KEY, + space_id TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence > 0), + operation_id TEXT NOT NULL, + parent_operation_sha256 TEXT, + graph_revision_sha256 TEXT NOT NULL, + records_sha256 TEXT NOT NULL, + operation_json TEXT NOT NULL, + instant TEXT NOT NULL, + UNIQUE(space_id, sequence), + UNIQUE(space_id, operation_id) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_operation_records ( + space_id TEXT NOT NULL, + operation_sha256 TEXT NOT NULL, + ordinal INTEGER NOT NULL CHECK(ordinal >= 0), + record_key TEXT NOT NULL, + change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')), + record_sha256 TEXT NOT NULL, + PRIMARY KEY(operation_sha256, ordinal), + UNIQUE(operation_sha256, record_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_records ( + space_id TEXT NOT NULL, + record_key TEXT NOT NULL, + kind TEXT NOT NULL, + record_sha256 TEXT NOT NULL, + record_json TEXT NOT NULL, + operation_sha256 TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence > 0), + PRIMARY KEY(space_id, record_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_dependencies ( + space_id TEXT NOT NULL, + record_key TEXT NOT NULL, + dependency_key TEXT NOT NULL, + PRIMARY KEY(space_id, record_key, dependency_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_bindings ( + space_id TEXT PRIMARY KEY, + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL, + binding_sha256 TEXT NOT NULL UNIQUE, + binding_json TEXT NOT NULL, + created_at TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL, + prior_operation_sha256 TEXT, + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE, + receipt_json TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_commit_guards ( + value TEXT NOT NULL CHECK(value = 'ok') + ) STRICT`, + "CREATE INDEX IF NOT EXISTS oh_authority_operations_space_sequence ON oh_authority_operations(space_id, sequence)", + "CREATE INDEX IF NOT EXISTS oh_authority_records_space_kind ON oh_authority_records(space_id, kind, record_key)", + "CREATE INDEX IF NOT EXISTS oh_authority_dependencies_dependency ON oh_authority_dependencies(space_id, dependency_key)", + `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_no_update + BEFORE UPDATE ON oh_authority_operations + BEGIN SELECT RAISE(ABORT, 'Oh authority operations are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_guard_delete + BEFORE DELETE ON oh_authority_operations + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = OLD.space_id) + BEGIN SELECT RAISE(ABORT, 'Oh authority operations require a purge receipt'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_no_update + BEFORE UPDATE ON oh_authority_operation_records + BEGIN SELECT RAISE(ABORT, 'Oh authority operation records are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_insert + BEFORE INSERT ON oh_authority_operation_records + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations + WHERE operation_sha256 = NEW.operation_sha256 AND space_id = NEW.space_id) + BEGIN SELECT RAISE(ABORT, 'Oh authority operation record has no owning operation'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_delete + BEFORE DELETE ON oh_authority_operation_records + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations AS operation + JOIN oh_authority_purges AS purge ON purge.space_id = operation.space_id + WHERE operation.operation_sha256 = OLD.operation_sha256) + BEGIN SELECT RAISE(ABORT, 'Oh authority operation records require a purge receipt'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_update + BEFORE UPDATE ON oh_authority_purges + BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_delete + BEFORE DELETE ON oh_authority_purges + BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END` +]); +function normalizedSchemaSql(sql) { + return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim(); +} +function expectedSchemaObject(statement) { + const match = /^CREATE\s+(TABLE|INDEX|TRIGGER)(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-z0-9_]+)/iu.exec(statement.trim()); + if (match === null) + throw new Error("Invalid compiled authority schema statement."); + const declaredType = match[1]?.toLowerCase(); + const type = declaredType === "index" ? "index" : declaredType === "trigger" ? "trigger" : "table"; + const name = match[2]; + const tableMatch = type === "index" || type === "trigger" ? /\bON\s+([a-z0-9_]+)/iu.exec(statement) : null; + const tableName = type === "table" ? name : tableMatch?.[1]; + if (tableName === undefined) + throw new Error("Invalid compiled authority index statement."); + return { name, sql: normalizedSchemaSql(statement), tableName, type }; +} +var AUTHORITY_SCHEMA_OBJECTS = Object.freeze([AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS].map(expectedSchemaObject).sort((left, right) => canonicalJson([left.type, left.name]).localeCompare(canonicalJson([right.type, right.name])))); +var AUTHORITY_SCHEMA_SHA256 = canonicalSha256(AUTHORITY_SCHEMA_OBJECTS); +function rowValue(row, key, index) { + return Array.isArray(row) ? row[index] : row[key]; +} +function integer(value) { + if (typeof value === "number") + return Number.isSafeInteger(value) ? value : null; + if (typeof value === "bigint") { + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; + } + return null; +} +function normalizeLimit(value, fallback = 100, maximum = 1000) { + const limit = value ?? fallback; + if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) { + throw new RangeError(`limit must be an integer from 1 through ${maximum}.`); + } + return limit; +} +function parseOperationJson(value) { + if (typeof value !== "string") + throw new OhIntegrityError("A stored operation is not JSON text."); + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch { + throw new OhIntegrityError("A stored operation is not JSON."); + } + const operation = parseOhOperationV1(parsedValue); + if (operation === null || canonicalJson(operation) !== value) { + throw new OhIntegrityError("A stored operation is invalid."); + } + return operation; +} +function parseOperationRow(row, expected = {}) { + const operation = parseOperationJson(rowValue(row, "operation_json", 7)); + if (rowValue(row, "operation_sha256", 0) !== operation.operationSha256 || rowValue(row, "space_id", 1) !== operation.spaceId || integer(rowValue(row, "sequence", 2)) !== operation.sequence || rowValue(row, "operation_id", 3) !== operation.operationId || rowValue(row, "parent_operation_sha256", 4) !== operation.parentOperationSha256 || rowValue(row, "graph_revision_sha256", 5) !== operation.graphRevisionSha256 || rowValue(row, "records_sha256", 6) !== operation.recordsSha256 || rowValue(row, "instant", 8) !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) { + throw new OhIntegrityError("Remote operation columns do not match their canonical envelope."); + } + return operation; +} +function parseBindingRow(row, expectedSpaceId) { + const json = rowValue(row, "binding_json", 6); + if (typeof json !== "string") + throw new OhIntegrityError("A remote store binding is not JSON text."); + let value; + try { + value = JSON.parse(json); + } catch { + throw new OhIntegrityError("A remote store binding is not JSON."); + } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== json || binding.spaceId !== expectedSpaceId || rowValue(row, "space_id", 0) !== binding.spaceId || rowValue(row, "realm_id", 1) !== binding.realmId || rowValue(row, "profile_id", 2) !== binding.profile.profileId || rowValue(row, "profile_kind", 3) !== binding.profile.profileKind || rowValue(row, "profile_sha256", 4) !== binding.profile.profileSha256 || rowValue(row, "binding_sha256", 5) !== binding.bindingSha256) { + throw new OhIntegrityError("Remote binding columns do not match their canonical envelope."); + } + return binding; +} +function parsePurgeReceiptRow(row, expectedSpaceId, expectedBindingSha256) { + const json = rowValue(row, "receipt_json", 6); + if (typeof json !== "string") + throw new OhIntegrityError("A remote purge receipt is invalid."); + let value; + try { + value = JSON.parse(json); + } catch { + throw new OhIntegrityError("A remote purge receipt is invalid."); + } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== json) { + throw new OhIntegrityError("A remote purge receipt is invalid."); + } + if (receipt.spaceId !== expectedSpaceId || expectedBindingSha256 !== undefined && receipt.bindingSha256 !== expectedBindingSha256 || rowValue(row, "space_id", 0) !== receipt.spaceId || rowValue(row, "binding_sha256", 1) !== receipt.bindingSha256 || rowValue(row, "prior_operation_sha256", 2) !== receipt.priorHead.operationSha256 || integer(rowValue(row, "prior_sequence", 3)) !== receipt.priorHead.sequence || rowValue(row, "purged_at", 4) !== receipt.purgedAt || rowValue(row, "receipt_sha256", 5) !== receipt.receiptSha256) { + throw new OhIntegrityError("Remote purge columns do not match their canonical receipt."); + } + return receipt; +} +function parseHeadRow(row) { + const generation = integer(rowValue(row, "generation", 0)); + const graphValue = rowValue(row, "graph_revision_sha256", 1); + const operationValue = rowValue(row, "head_operation_sha256", 2); + const recordsSha256 = parseSha256Hex(rowValue(row, "records_sha256", 3)); + const sequence = integer(rowValue(row, "sequence", 4)); + const graphRevisionSha256 = graphValue === null ? null : parseSha256Hex(graphValue); + const operationSha256 = operationValue === null ? null : parseSha256Hex(operationValue); + if (generation === null || sequence === null || generation !== sequence || recordsSha256 === null || graphValue !== null && graphRevisionSha256 === null || operationValue !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null)) { + throw new OhIntegrityError("The remote authority contains an invalid head."); + } + return { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 }; +} +async function queryOne(client, statement) { + return (await client.execute(statement)).rows[0] ?? null; +} +async function verifyAuthoritySchemaObjects(client) { + const rows = (await client.execute({ sql: `SELECT type, name, tbl_name, sql FROM sqlite_schema + WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*' + OR tbl_name GLOB 'oh_authority_*') + ORDER BY type, name` })).rows; + const actual = rows.map((row) => { + const type = rowValue(row, "type", 0); + const name = rowValue(row, "name", 1); + const tableName = rowValue(row, "tbl_name", 2); + const sql = rowValue(row, "sql", 3); + if (type !== "table" && type !== "index" && type !== "trigger" || typeof name !== "string" || typeof tableName !== "string" || typeof sql !== "string") { + throw new OhIntegrityError("The installed libSQL authority has an invalid schema object."); + } + return { name, sql: normalizedSchemaSql(sql), tableName, type }; + }); + if (canonicalJson(actual) !== canonicalJson(AUTHORITY_SCHEMA_OBJECTS)) { + throw new OhIntegrityError("The installed libSQL authority objects differ from this runtime."); + } +} +async function verifyAuthoritySchema(client) { + const installed = await queryOne(client, { sql: `SELECT name, schema_sha256 + FROM oh_authority_schemas WHERE version = ?`, args: [AUTHORITY_SCHEMA_VERSION] }); + if (installed === null || rowValue(installed, "name", 0) !== AUTHORITY_SCHEMA_NAME || rowValue(installed, "schema_sha256", 1) !== AUTHORITY_SCHEMA_SHA256) { + throw new OhIntegrityError("The installed libSQL authority schema differs from this runtime."); + } + const contract = await queryOne(client, { sql: `SELECT contract_sha256, manifest_json + FROM oh_authority_contracts WHERE contract_id = ?`, args: [OH_CONTRACT_MANIFEST_V1.contractId] }); + if (contract === null || rowValue(contract, "contract_sha256", 0) !== OH_CONTRACT_MANIFEST_V1.contractSha256 || rowValue(contract, "manifest_json", 1) !== canonicalJson(OH_CONTRACT_MANIFEST_V1)) { + throw new OhIntegrityError("The remote authority contract differs from this runtime."); + } + await verifyAuthoritySchemaObjects(client); +} +async function bootstrapOhLibSqlAuthorityV1(client) { + const existingObjects = (await client.execute({ sql: `SELECT name FROM sqlite_schema + WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*' + OR tbl_name GLOB 'oh_authority_*')` })).rows; + if (existingObjects.length > 0) { + if (!existingObjects.some((row) => rowValue(row, "name", 0) === "oh_authority_schemas")) { + throw new OhIntegrityError("Refusing to bootstrap over preexisting Oh authority objects."); + } + await verifyAuthoritySchema(client); + return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 }; + } + const setup = [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS].map((sql) => ({ sql })); + setup.push({ + sql: `INSERT INTO oh_authority_schemas(version, name, schema_sha256, applied_at) + VALUES (?, ?, ?, ?) ON CONFLICT(version) DO NOTHING`, + args: [AUTHORITY_SCHEMA_VERSION, AUTHORITY_SCHEMA_NAME, AUTHORITY_SCHEMA_SHA256, canonicalNow()] + }); + setup.push({ sql: `INSERT INTO oh_authority_contracts(contract_id, contract_sha256, manifest_json) + VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING`, args: [ + OH_CONTRACT_MANIFEST_V1.contractId, + OH_CONTRACT_MANIFEST_V1.contractSha256, + canonicalJson(OH_CONTRACT_MANIFEST_V1) + ] }); + await client.batch(setup, "write"); + await verifyAuthoritySchema(client); + return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 }; +} +async function initializeSpace(client, binding) { + const purged = await queryOne(client, { + sql: PURGE_ROW_SELECT, + args: [binding.spaceId] + }); + if (purged !== null) + throw new OhPurgedSpaceError(parsePurgeReceiptRow(purged, binding.spaceId, binding.bindingSha256)); + const now = canonicalNow(); + try { + await client.batch([ + { + sql: `INSERT INTO oh_authority_spaces(space_id, contract_id, generation, + head_operation_sha256, graph_revision_sha256, records_sha256, sequence, created_at, updated_at) + SELECT ?, ?, 0, NULL, NULL, ?, 0, ?, ? + WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + ON CONFLICT(space_id) DO NOTHING`, + args: [ + binding.spaceId, + OH_CONTRACT_MANIFEST_V1.contractId, + EMPTY_RECORDS_SHA2562, + now, + now, + binding.spaceId + ] + }, + { + sql: `INSERT INTO oh_authority_bindings(space_id, realm_id, profile_id, profile_kind, + profile_sha256, binding_sha256, binding_json, created_at) + SELECT ?, ?, ?, ?, ?, ?, ?, ? + WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + AND EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + ON CONFLICT(space_id) DO NOTHING`, + args: [ + binding.spaceId, + binding.realmId, + binding.profile.profileId, + binding.profile.profileKind, + binding.profile.profileSha256, + binding.bindingSha256, + canonicalJson(binding), + now, + binding.spaceId, + binding.spaceId + ] + }, + { + sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + OR NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + OR NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`, + args: [binding.spaceId, binding.spaceId, binding.spaceId, binding.bindingSha256] + } + ], "write"); + } catch (error) { + const raced = await queryOne(client, { + sql: PURGE_ROW_SELECT, + args: [binding.spaceId] + }); + if (raced !== null) + throw new OhPurgedSpaceError(parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256)); + const persisted2 = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }); + if (persisted2 !== null && canonicalJson(parseBindingRow(persisted2, binding.spaceId)) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is already bound to a different realm or profile."); + } + throw error; + } + const persisted = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }); + if (persisted === null) { + const raced = await queryOne(client, { + sql: PURGE_ROW_SELECT, + args: [binding.spaceId] + }); + if (raced !== null) + throw new OhPurgedSpaceError(parsePurgeReceiptRow(raced, binding.spaceId, binding.bindingSha256)); + throw new OhIntegrityError("The remote space has no persisted binding after initialization."); + } + if (canonicalJson(parseBindingRow(persisted, binding.spaceId)) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is already bound to a different realm or profile."); + } +} + +class OhLibSqlStoreV1 { + binding; + #client; + #closeClient; + #closed = false; + #purged = null; + constructor(client, binding, closeClient) { + this.#client = client; + this.binding = binding; + this.#closeClient = closeClient; + } + #assertOpen() { + if (this.#purged !== null) + throw new OhPurgedSpaceError(this.#purged); + if (this.#closed) + throw new Error("The Oh libSQL store is closed."); + } + async head() { + this.#assertOpen(); + const row = await queryOne(this.#client, { + sql: `SELECT generation, graph_revision_sha256, + head_operation_sha256, records_sha256, sequence FROM oh_authority_spaces WHERE space_id = ?`, + args: [this.binding.spaceId] + }); + if (row === null) { + const purged = await this.#readPurge(); + if (purged !== null) { + this.#purged = purged; + throw new OhPurgedSpaceError(purged); + } + throw new OhIntegrityError("The remote Oh space does not exist."); + } + return parseHeadRow(row); + } + async#readPurge() { + const row = await queryOne(this.#client, { + sql: PURGE_ROW_SELECT, + args: [this.binding.spaceId] + }); + if (row === null) + return null; + return parsePurgeReceiptRow(row, this.binding.spaceId, this.binding.bindingSha256); + } + async#headAt(reference) { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) + throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) + return emptyOhHeadV1(); + const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, parsed.sequence] }); + if (row === null) + throw new OhConflictError("The requested head is not present in this space."); + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (operation.spaceId !== this.binding.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + async#currentMaterializedSnapshot(expectedHead, maximumRecords) { + const provenancePredicate = `operation.space_id = ? AND (operation.operation_sha256 IS ? + OR operation.operation_sha256 IN (SELECT record.operation_sha256 + FROM oh_authority_records AS record WHERE record.space_id = ?))`; + const results = await this.#client.batch([ + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { + sql: `SELECT + (SELECT count(*) FROM (SELECT DISTINCT operation.operation_sha256 + FROM oh_authority_operations AS operation WHERE ${provenancePredicate})) AS provenance_count, + (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT operation.operation_sha256, + ${OPERATION_RESPONSE_BYTES} AS bytes FROM oh_authority_operations AS operation + WHERE ${provenancePredicate})) AS provenance_bytes, + (SELECT count(*) FROM oh_authority_records WHERE space_id = ?) AS record_count, + (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) FROM oh_authority_records AS record + WHERE record.space_id = ?) AS record_bytes, + (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ?) AS dependency_bytes`, + args: [ + this.binding.spaceId, + expectedHead.operationSha256, + this.binding.spaceId, + this.binding.spaceId, + expectedHead.operationSha256, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId + ] + }, + { sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence + FROM oh_authority_records AS record WHERE record.space_id = ? + AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? ORDER BY record_key`, args: [ + this.binding.spaceId, + this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + ] }, + { + sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies + AS dependency WHERE dependency.space_id = ? + AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ? + ORDER BY record_key, dependency_key`, + args: [this.binding.spaceId, this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] + }, + { + sql: `SELECT ${OPERATION_ROW_COLUMNS} + FROM oh_authority_operations AS operation + WHERE ${provenancePredicate} + AND (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT candidate.operation_sha256, + ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND (candidate.operation_sha256 IS ? + OR candidate.operation_sha256 IN (SELECT record.operation_sha256 + FROM oh_authority_records AS record WHERE record.space_id = ?)))) <= ? + ORDER BY operation.sequence`, + args: [ + this.binding.spaceId, + expectedHead.operationSha256, + this.binding.spaceId, + this.binding.spaceId, + expectedHead.operationSha256, + this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + ] + }, + { sql: `SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_authority_operations WHERE space_id = ?`, args: [this.binding.spaceId] } + ], "read"); + if (results.length !== 6) + throw new OhIntegrityError("The remote authority returned an incomplete snapshot batch."); + const [headResult, sizeResult, recordResult, dependencyResult, provenanceResult, historyResult] = results; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purge = await this.#readPurge(); + if (purge !== null) { + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote Oh space disappeared while reading its snapshot."); + } + const head = parseHeadRow(headRow); + if (canonicalJson(head) !== canonicalJson(expectedHead)) { + throw new OhConflictError("The remote space head changed while reading its current snapshot."); + } + const size = sizeResult.rows[0]; + const provenanceOperations = size === undefined ? null : integer(rowValue(size, "provenance_count", 0)); + const provenanceBytes = size === undefined ? null : integer(rowValue(size, "provenance_bytes", 1)); + const recordCount = size === undefined ? null : integer(rowValue(size, "record_count", 2)); + const recordBytes = size === undefined ? null : integer(rowValue(size, "record_bytes", 3)); + const dependencyBytes = size === undefined ? null : integer(rowValue(size, "dependency_bytes", 4)); + if (provenanceOperations === null || provenanceBytes === null || recordCount === null || recordBytes === null || dependencyBytes === null || provenanceOperations > OH_LIBSQL_STORE_LIMITS_V1.historyOperations || provenanceBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || recordBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || dependencyBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) { + throw new RangeError("The current libSQL materialization exceeds its provider-safe response bounds."); + } + if (recordResult.rows.length !== recordCount || provenanceResult.rows.length !== provenanceOperations) { + throw new OhIntegrityError("The provider-safe snapshot queries omitted bounded authority rows."); + } + const history = historyResult.rows[0]; + const operationCount = history === undefined ? null : integer(rowValue(history, "count", 0)); + const minimumValue = history === undefined ? undefined : rowValue(history, "minimum", 1); + const maximumValue = history === undefined ? undefined : rowValue(history, "maximum", 2); + const minimumSequence = history === undefined ? null : integer(rowValue(history, "minimum", 1)); + const maximumSequence = history === undefined ? null : integer(rowValue(history, "maximum", 2)); + if (operationCount !== head.sequence || head.sequence === 0 && (minimumValue !== null || maximumValue !== null) || head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== head.sequence)) { + throw new OhIntegrityError("The remote operation history does not exactly cover its current head."); + } + if (recordResult.rows.length > maximumRecords) { + throw new RangeError("The remote graph exceeds the requested record snapshot bound."); + } + const provenanceBySha256 = new Map; + for (const row of provenanceResult.rows) { + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (provenanceBySha256.has(operation.operationSha256)) { + throw new OhIntegrityError("A current materialization provenance operation is invalid."); + } + provenanceBySha256.set(operation.operationSha256, operation); + } + if (provenanceBySha256.size !== provenanceOperations || head.operationSha256 !== null && !provenanceBySha256.has(head.operationSha256)) { + throw new OhIntegrityError("The current materialization omitted required provenance operations."); + } + if (head.sequence > 0) { + const terminal = head.operationSha256 === null ? undefined : provenanceBySha256.get(head.operationSha256); + if (terminal === undefined || terminal.sequence !== head.sequence || terminal.graphRevisionSha256 !== head.graphRevisionSha256 || terminal.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The remote space head differs from its terminal canonical operation."); + } + } + const materialized = recordResult.rows.map((row) => { + const json = rowValue(row, "record_json", 3); + if (typeof json !== "string") + throw new OhIntegrityError("A materialized remote record is not JSON text."); + let value; + try { + value = JSON.parse(json); + } catch { + throw new OhIntegrityError("A materialized remote record is invalid."); + } + const record = parseKnowledgeGraphRecordV1(value); + const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 4)); + const sequence = integer(rowValue(row, "sequence", 5)); + if (record === null || canonicalJson(record) !== json || operationSha256 === null || sequence === null || sequence < 1 || sequence > head.sequence || rowValue(row, "record_key", 0) !== record.key || rowValue(row, "kind", 1) !== record.kind || rowValue(row, "record_sha256", 2) !== record.recordSha256) { + throw new OhIntegrityError("A materialized remote record is invalid."); + } + const provenance = provenanceBySha256.get(operationSha256); + if (provenance === undefined || provenance.sequence !== sequence || !provenance.changes.some((change) => change.kind === "put" && canonicalJson(change.record) === json)) { + throw new OhIntegrityError("A materialized remote record has no exact canonical provenance put."); + } + return { record, sequence }; + }); + const records = materialized.map(({ record }) => record); + if (canonicalSha256(records.map(knowledgeGraphRecordRefV1)) !== head.recordsSha256) { + throw new OhIntegrityError("Materialized remote records do not reproduce the current head."); + } + const dependencyRows = dependencyResult.rows.map((row) => ({ + dependency_key: rowValue(row, "dependency_key", 1), + record_key: rowValue(row, "record_key", 0) + })); + const expectedDependencies = records.flatMap((record) => record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key }))); + if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) { + throw new OhIntegrityError("Materialized remote dependencies do not match their record envelopes."); + } + return { head, records, v: 1 }; + } + async snapshot(options = {}) { + this.#assertOpen(); + const current = await this.head(); + const target = options.head === undefined ? current : await this.#headAt(options.head); + if (target.sequence > current.sequence) + throw new OhConflictError("The requested head is ahead of this space."); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + if (target.operationSha256 === current.operationSha256) { + return await this.#currentMaterializedSnapshot(current, maximumRecords); + } + if (target.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) { + throw new RangeError("The requested libSQL history exceeds its operation replay bound."); + } + const historyResults = await this.#client.batch([ + { + sql: `SELECT count(*) AS count, min(operation.sequence) AS minimum, + max(operation.sequence) AS maximum, + coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes, + coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS response_bytes + FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ?`, + args: [this.binding.spaceId, target.sequence] + }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + ORDER BY operation.sequence`, args: [ + this.binding.spaceId, + target.sequence, + this.binding.spaceId, + target.sequence, + OH_LIBSQL_STORE_LIMITS_V1.historyBytes, + this.binding.spaceId, + target.sequence, + OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes + ] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] } + ], "read"); + if (historyResults.length !== 3) { + throw new OhIntegrityError("The remote authority returned an incomplete history batch."); + } + const [historySizeResult, historyRowResult, purgeResult] = historyResults; + const historySizeRow = historySizeResult.rows[0]; + const historyBytes = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "canonical_bytes", 3)); + const responseBytes = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "response_bytes", 4)); + if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes || responseBytes === null || responseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) { + throw new RangeError("The requested libSQL history exceeds its provider-safe replay bounds."); + } + const historyCount = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "count", 0)); + const minimumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "minimum", 1)); + const maximumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "maximum", 2)); + if (historyCount !== target.sequence || target.sequence === 0 && (rowValue(historySizeRow, "minimum", 1) !== null || rowValue(historySizeRow, "maximum", 2) !== null) || target.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== target.sequence) || historyRowResult.rows.length !== historyCount) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote operation history does not exactly cover the requested head."); + } + const operations = historyRowResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId })); + const snapshot = replayOhOperationsV1(this.binding.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Remote operation replay does not reproduce the requested head."); + } + return snapshot; + } + async changesSince(fromValue, options = {}) { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) + throw new TypeError("Invalid change-feed cursor."); + const requestedThrough = options.through === undefined ? undefined : parseOhHeadRefV1(options.through); + if (requestedThrough === null) + throw new TypeError("Invalid change-feed through head."); + const limit = normalizeLimit(options.limit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit); + const throughSequence = requestedThrough?.sequence ?? null; + const results = await this.#client.batch([ + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, from.sequence] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, throughSequence] }, + { sql: `SELECT count(*) AS count, coalesce(sum(response_bytes), 0) AS response_bytes FROM ( + SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence > ? + AND candidate.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + ORDER BY candidate.sequence LIMIT ? + )`, args: [ + this.binding.spaceId, + from.sequence, + throughSequence, + this.binding.spaceId, + limit + 1 + ] }, + { + sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + AS operation WHERE operation.space_id = ? AND operation.sequence > ? + AND operation.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + AND (SELECT coalesce(sum(response_bytes), 0) FROM ( + SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence > ? + AND candidate.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + ORDER BY candidate.sequence LIMIT ? + )) <= ? + ORDER BY operation.sequence LIMIT ?`, + args: [ + this.binding.spaceId, + from.sequence, + throughSequence, + this.binding.spaceId, + this.binding.spaceId, + from.sequence, + throughSequence, + this.binding.spaceId, + limit + 1, + OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes, + limit + 1 + ] + }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] } + ], "read"); + if (results.length !== 6) + throw new OhIntegrityError("The remote authority returned an incomplete change-feed batch."); + const [currentResult, fromResult, throughResult, pageSizeResult, pageResult, purgeResult] = results; + const currentRow = currentResult.rows[0]; + if (currentRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote Oh space disappeared while reading its change feed."); + } + const current = parseHeadRow(currentRow); + const resolveHead = (reference, result) => { + if (reference.sequence === 0) + return emptyOhHeadV1(); + const row = result.rows[0]; + if (row === undefined) + throw new OhConflictError("A requested change-feed head is not present in this space."); + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (operation.spaceId !== this.binding.spaceId || operation.sequence !== reference.sequence || operation.operationSha256 !== reference.operationSha256) { + throw new OhConflictError("A requested change-feed sequence identifies a different operation head."); + } + return { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + }; + const fromHead = resolveHead(from, fromResult); + const through = requestedThrough === undefined ? current : resolveHead(requestedThrough, throughResult); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one remote history prefix."); + } + const pageSizeRow = pageSizeResult.rows[0]; + const pageCount = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "count", 0)); + const pageResponseBytes = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "response_bytes", 1)); + if (pageCount === null || pageCount > limit + 1 || pageResponseBytes === null) { + throw new OhIntegrityError("The remote change feed returned invalid response bounds."); + } + if (pageResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) { + throw new RangeError("The requested change-feed page exceeds its provider response bound."); + } + const parsed = pageResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId })); + if (parsed.length !== pageCount) { + throw new OhIntegrityError("The remote change feed omitted provider-bounded rows."); + } + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior = fromHead; + for (const operation of parsed) { + if (operation.spaceId !== this.binding.spaceId || operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The remote change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The remote change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { + from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, + operations, + through, + to, + v: 1 + }; + } + async#assertMaterializedSnapshot(snapshot) { + if (snapshot.head.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) { + throw new RangeError("The libSQL authority exceeds its explicit verification operation bound."); + } + const verificationResults = await this.#client.batch([ + { sql: `SELECT * FROM (WITH bounded_operation AS ( + SELECT * FROM oh_authority_operations + WHERE space_id = ? AND sequence <= ? + ) SELECT count(*) AS operation_count, min(operation.sequence) AS minimum, + max(operation.sequence) AS maximum, + coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes, + coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS operation_response_bytes, + (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) AS record_response_bytes, + (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ?) AS dependency_response_bytes, + (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_operation_records AS materialized + JOIN bounded_operation AS owner + ON owner.operation_sha256 = materialized.operation_sha256) AS operation_record_response_bytes + FROM bounded_operation AS operation)`, args: [ + this.binding.spaceId, + snapshot.head.sequence, + this.binding.spaceId, + this.binding.spaceId + ] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} + FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + ORDER BY operation.sequence`, args: [ + this.binding.spaceId, + snapshot.head.sequence, + this.binding.spaceId, + snapshot.head.sequence, + OH_LIBSQL_STORE_LIMITS_V1.historyBytes, + this.binding.spaceId, + snapshot.head.sequence, + OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes + ] }, + { sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence + FROM oh_authority_records AS record WHERE record.space_id = ? + AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? + ORDER BY record.record_key`, args: [ + this.binding.spaceId, + this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + ] }, + { sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ? + AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ? + ORDER BY dependency.record_key, dependency.dependency_key`, args: [ + this.binding.spaceId, + this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + ] }, + { sql: `SELECT materialized.space_id, materialized.operation_sha256, materialized.ordinal, + materialized.record_key, materialized.change_kind, materialized.record_sha256 + FROM oh_authority_operation_records AS materialized + JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_operation_records AS materialized + JOIN oh_authority_operations AS owner + ON owner.operation_sha256 = materialized.operation_sha256 + WHERE owner.space_id = ? AND owner.sequence <= ?) <= ? + ORDER BY operation.sequence, materialized.ordinal`, args: [ + this.binding.spaceId, + snapshot.head.sequence, + this.binding.spaceId, + snapshot.head.sequence, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + ] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] } + ], "read"); + if (verificationResults.length !== 6) { + throw new OhIntegrityError("The remote authority returned an incomplete verification batch."); + } + const [sizeResult, operationResult, recordResult, dependencyResult, operationRecordResult, headResult] = verificationResults; + const sizeRow = sizeResult.rows[0]; + const operationCount = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_count", 0)); + const minimumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "minimum", 1); + const maximumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "maximum", 2); + const minimumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "minimum", 1)); + const maximumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "maximum", 2)); + const historyBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "canonical_bytes", 3)); + const operationResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_response_bytes", 4)); + const recordResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "record_response_bytes", 5)); + const dependencyResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "dependency_response_bytes", 6)); + const operationRecordResponseBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_record_response_bytes", 7)); + if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes || operationResponseBytes === null || operationResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes || recordResponseBytes === null || recordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || dependencyResponseBytes === null || dependencyResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes || operationRecordResponseBytes === null || operationRecordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) { + throw new RangeError("The libSQL authority exceeds its provider-safe verification bounds."); + } + if (operationCount !== snapshot.head.sequence || snapshot.head.sequence === 0 && (minimumValue !== null || maximumValue !== null) || snapshot.head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== snapshot.head.sequence) || operationResult.rows.length !== operationCount) { + throw new OhIntegrityError("The remote operation history does not exactly cover its verified head."); + } + const headRow = headResult.rows[0]; + if (headRow === undefined) + throw new OhIntegrityError("The remote authority lost its head during verification."); + if (canonicalJson(parseHeadRow(headRow)) !== canonicalJson(snapshot.head)) { + throw new OhConflictError("The remote authority head changed during verification."); + } + const operations = operationResult.rows.map((row) => { + return parseOperationRow(row, { spaceId: this.binding.spaceId }); + }); + const replayed = replayOhOperationsV1(this.binding.spaceId, operations); + if (canonicalJson(replayed) !== canonicalJson(snapshot)) { + throw new OhIntegrityError("Remote operation replay changed during materialization verification."); + } + const materializedBy = new Map; + for (const operation of operations) { + for (const change of operation.changes) { + const key = change.kind === "put" ? change.record.key : change.key; + if (change.kind === "put") + materializedBy.set(key, { operationSha256: operation.operationSha256, sequence: operation.sequence }); + else + materializedBy.delete(key); + } + } + const records = recordResult.rows.map((row) => { + const json = rowValue(row, "record_json", 3); + if (typeof json !== "string") + throw new OhIntegrityError("A materialized remote record is not JSON text."); + let value; + try { + value = JSON.parse(json); + } catch { + throw new OhIntegrityError("A materialized remote record is invalid."); + } + const record = parseKnowledgeGraphRecordV1(value); + const provenance = record === null ? undefined : materializedBy.get(record.key); + if (record === null || canonicalJson(record) !== json || rowValue(row, "record_key", 0) !== record.key || rowValue(row, "kind", 1) !== record.kind || rowValue(row, "record_sha256", 2) !== record.recordSha256 || provenance === undefined || rowValue(row, "operation_sha256", 4) !== provenance.operationSha256 || integer(rowValue(row, "sequence", 5)) !== provenance.sequence) { + throw new OhIntegrityError("A materialized remote record differs from operation replay."); + } + return record; + }); + if (canonicalJson(records) !== canonicalJson(snapshot.records)) { + throw new OhIntegrityError("Remote materialized records do not match operation replay."); + } + const dependencyRows = dependencyResult.rows.map((row) => ({ + dependency_key: rowValue(row, "dependency_key", 1), + record_key: rowValue(row, "record_key", 0) + })); + const expectedDependencies = snapshot.records.flatMap((record) => record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key }))); + if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) { + throw new OhIntegrityError("Remote materialized dependencies do not match operation replay."); + } + const operationRecordRows = operationRecordResult.rows.map((row) => ({ + change_kind: rowValue(row, "change_kind", 4), + operation_sha256: rowValue(row, "operation_sha256", 1), + ordinal: integer(rowValue(row, "ordinal", 2)), + record_key: rowValue(row, "record_key", 3), + record_sha256: rowValue(row, "record_sha256", 5), + space_id: rowValue(row, "space_id", 0) + })); + const expectedOperationRecords = operations.flatMap((operation) => operation.changes.map((change, ordinal) => ({ + change_kind: change.kind, + operation_sha256: operation.operationSha256, + ordinal, + record_key: change.kind === "put" ? change.record.key : change.key, + record_sha256: change.kind === "put" ? change.record.recordSha256 : change.priorSha256, + space_id: this.binding.spaceId + }))); + if (canonicalJson(operationRecordRows) !== canonicalJson(expectedOperationRecords)) { + throw new OhIntegrityError("Remote operation-record rows do not match operation replay."); + } + } + async#operationById(operationId) { + const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }); + if (row === null) + return null; + return parseOperationRow(row, { operationId, spaceId: this.binding.spaceId }); + } + async#commitPreflight(operationId) { + const results = await this.#client.batch([ + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] } + ], "read"); + if (results.length !== 3) + throw new OhIntegrityError("The remote authority returned an incomplete commit preflight."); + const [duplicateResult, headResult, purgeResult] = results; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote space disappeared during commit preflight."); + } + const duplicateRow = duplicateResult.rows[0]; + return { current: parseHeadRow(headRow), duplicate: duplicateRow === undefined ? null : parseOperationRow(duplicateRow, { operationId, spaceId: this.binding.spaceId }) }; + } + async#assertOperationReachable(operation, expectedHead) { + if (operation.sequence < 1 || operation.sequence > expectedHead.sequence) { + throw new OhIntegrityError("A remote idempotent operation is not reachable from the current head."); + } + const results = await this.#client.batch([ + { sql: `SELECT * FROM (WITH RECURSIVE authority_chain(sequence, operation_sha256) AS ( + SELECT sequence, operation_sha256 FROM oh_authority_operations + WHERE space_id = ? AND sequence = ? AND operation_sha256 = ? + UNION ALL + SELECT candidate.sequence, candidate.operation_sha256 + FROM oh_authority_operations AS candidate + JOIN authority_chain AS prior + ON candidate.space_id = ? AND candidate.sequence = prior.sequence + 1 + AND candidate.parent_operation_sha256 = prior.operation_sha256 + WHERE candidate.sequence <= ? + ) SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum, + (SELECT operation_sha256 FROM authority_chain ORDER BY sequence DESC LIMIT 1) AS terminal_sha256 + FROM authority_chain)`, args: [ + this.binding.spaceId, + operation.sequence, + operation.operationSha256, + this.binding.spaceId, + expectedHead.sequence + ] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, expectedHead.sequence] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] } + ], "read"); + if (results.length !== 4) + throw new OhIntegrityError("The remote authority returned an incomplete reachability proof."); + const [chainResult, terminalResult, headResult, purgeResult] = results; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote space disappeared during an idempotency proof."); + } + const current = parseHeadRow(headRow); + if (canonicalJson(current) !== canonicalJson(expectedHead)) { + throw new OhConflictError("The remote space head changed during an idempotency proof."); + } + const chain = chainResult.rows[0]; + const count = chain === undefined ? null : integer(rowValue(chain, "count", 0)); + const minimum = chain === undefined ? null : integer(rowValue(chain, "minimum", 1)); + const maximum = chain === undefined ? null : integer(rowValue(chain, "maximum", 2)); + const terminalSha256 = chain === undefined ? null : rowValue(chain, "terminal_sha256", 3); + if (count !== expectedHead.sequence - operation.sequence + 1 || minimum !== operation.sequence || maximum !== expectedHead.sequence || terminalSha256 !== expectedHead.operationSha256) { + throw new OhIntegrityError("A remote idempotent operation has no exact path to the current head."); + } + const terminalRow = terminalResult.rows[0]; + if (terminalRow === undefined || expectedHead.operationSha256 === null) { + throw new OhIntegrityError("The remote current head operation is missing."); + } + const terminal = parseOperationRow(terminalRow, { + operationSha256: expectedHead.operationSha256, + spaceId: this.binding.spaceId + }); + if (terminal.sequence !== expectedHead.sequence || terminal.graphRevisionSha256 !== expectedHead.graphRevisionSha256 || terminal.recordsSha256 !== expectedHead.recordsSha256) { + throw new OhIntegrityError("The remote space head differs from its terminal canonical operation."); + } + } + async commit(input) { + this.#assertOpen(); + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || changes.length === 0) + throw new TypeError("Invalid Oh commit input."); + if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) { + throw new RangeError("A direct libSQL commit exceeds its change-count bound."); + } + const dependencies = changes.reduce((count, change) => count + (change.kind === "put" ? change.record.dependencies.length : 0), 0); + if (dependencies > OH_LIBSQL_STORE_LIMITS_V1.dependenciesPerCommit) { + throw new RangeError("A direct libSQL commit exceeds its dependency-count bound."); + } + const { current, duplicate } = await this.#commitPreflight(operationId); + if (duplicate !== null) { + await this.#assertOperationReachable(duplicate, current); + if (duplicate.actorId !== actorId || canonicalJson(duplicate.changes) !== canonicalJson(changes)) { + throw new OhConflictError("The operation ID is already bound to different content."); + } + return duplicate; + } + if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0 || current.generation !== input.expectedHead.generation || current.operationSha256 !== input.expectedHead.operationSha256) { + throw new OhConflictError("The expected head does not match the current remote space head."); + } + const snapshot = await this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot); + const transition = transitionOhSnapshotV1({ + actorId, + changes, + instant: input.instant ?? canonicalNow(), + operationId, + snapshot, + spaceId: this.binding.spaceId + }); + const operation = transition.operation; + if (utf8ByteLength(canonicalJson(operation)) > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) { + throw new RangeError("A direct libSQL operation exceeds its canonical byte bound."); + } + const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)"; + const statements = [{ + sql: `INSERT INTO oh_authority_operations(operation_sha256, space_id, sequence, + operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, + operation_json, instant) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`, + args: [ + operation.operationSha256, + this.binding.spaceId, + operation.sequence, + operation.operationId, + operation.parentOperationSha256, + operation.graphRevisionSha256, + operation.recordsSha256, + canonicalJson(operation), + operation.instant, + this.binding.spaceId, + current.generation, + current.operationSha256 + ] + }]; + for (const [ordinal, change] of operation.changes.entries()) { + const key = change.kind === "put" ? change.record.key : change.key; + const digest = change.kind === "put" ? change.record.recordSha256 : change.priorSha256; + statements.push({ + sql: `INSERT INTO oh_authority_operation_records(space_id, operation_sha256, + ordinal, record_key, change_kind, record_sha256) + SELECT ?, ?, ?, ?, ?, ? WHERE ${existsOperation}`, + args: [ + this.binding.spaceId, + operation.operationSha256, + ordinal, + key, + change.kind, + digest, + operation.operationSha256 + ] + }); + statements.push({ sql: `DELETE FROM oh_authority_dependencies WHERE space_id = ? AND record_key = ? + AND ${existsOperation}`, args: [this.binding.spaceId, key, operation.operationSha256] }); + if (change.kind === "put") { + statements.push({ + sql: `INSERT INTO oh_authority_records(space_id, record_key, kind, + record_sha256, record_json, operation_sha256, sequence) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE ${existsOperation} + ON CONFLICT(space_id, record_key) DO UPDATE SET kind = excluded.kind, + record_sha256 = excluded.record_sha256, record_json = excluded.record_json, + operation_sha256 = excluded.operation_sha256, sequence = excluded.sequence`, + args: [ + this.binding.spaceId, + key, + change.record.kind, + change.record.recordSha256, + canonicalJson(change.record), + operation.operationSha256, + operation.sequence, + operation.operationSha256 + ] + }); + } else { + statements.push({ + sql: `DELETE FROM oh_authority_records WHERE space_id = ? AND record_key = ? + AND record_sha256 = ? AND ${existsOperation}`, + args: [this.binding.spaceId, key, change.priorSha256, operation.operationSha256] + }); + } + } + for (const change of operation.changes) { + if (change.kind !== "put") + continue; + for (const dependency of change.record.dependencies) { + statements.push({ + sql: `INSERT INTO oh_authority_dependencies(space_id, record_key, dependency_key) + SELECT ?, ?, ? WHERE ${existsOperation}`, + args: [this.binding.spaceId, change.record.key, dependency, operation.operationSha256] + }); + } + } + statements.push({ + sql: `UPDATE oh_authority_spaces SET generation = ?, head_operation_sha256 = ?, + graph_revision_sha256 = ?, records_sha256 = ?, sequence = ?, updated_at = ? + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ? AND ${existsOperation}`, + args: [ + operation.sequence, + operation.operationSha256, + operation.graphRevisionSha256, + operation.recordsSha256, + operation.sequence, + operation.instant, + this.binding.spaceId, + current.generation, + current.operationSha256, + operation.operationSha256 + ] + }); + statements.push({ + sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 = ?)`, + args: [this.binding.spaceId, operation.sequence, operation.operationSha256] + }); + statements.push({ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }); + statements.push({ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }); + let writeResults; + try { + writeResults = await this.#client.batch(statements, "write"); + } catch (error) { + const raced = await this.#operationById(operationId); + const head = await this.head(); + if (raced !== null && raced.actorId === actorId && canonicalJson(raced.changes) === canonicalJson(changes)) { + await this.#assertOperationReachable(raced, head); + return raced; + } + if (head.operationSha256 !== current.operationSha256) { + throw new OhConflictError("The remote space head changed while committing."); + } + throw error; + } + if (writeResults.length !== statements.length) { + throw new OhIntegrityError("The remote authority returned an incomplete commit result batch."); + } + const persistedRow = writeResults.at(-2)?.rows[0]; + const persistedHeadRow = writeResults.at(-1)?.rows[0]; + if (persistedRow === undefined || persistedHeadRow === undefined) { + throw new OhIntegrityError("The remote authority omitted its persisted commit result."); + } + const persisted = parseOperationRow(persistedRow, { operationId, spaceId: this.binding.spaceId }); + const persistedHead = parseHeadRow(persistedHeadRow); + if (canonicalJson(persisted) !== canonicalJson(operation) || persistedHead.operationSha256 !== operation.operationSha256 || persistedHead.sequence !== operation.sequence || persistedHead.graphRevisionSha256 !== operation.graphRevisionSha256 || persistedHead.recordsSha256 !== operation.recordsSha256) { + throw new OhIntegrityError("The remote authority did not persist the committed operation exactly."); + } + return persisted; + } + async exportDependencyClosure(input) { + if (!this.binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This remote profile does not permit dependency-closure export."); + } + const snapshot = await this.snapshot({ + ...input.head === undefined ? {} : { head: input.head }, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords } + }); + return createOhDependencyClosureV1({ + binding: this.binding, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }, + roots: input.roots, + snapshot + }); + } + async verify() { + this.#assertOpen(); + const snapshot = await this.snapshot(); + await this.#assertMaterializedSnapshot(snapshot); + return { + head: snapshot.head, + integrity: "verified", + operations: snapshot.head.sequence, + records: snapshot.records.length, + v: 1 + }; + } + async#assertPurgeComplete(expected) { + const tables = [ + "oh_authority_spaces", + "oh_authority_bindings", + "oh_authority_operations", + "oh_authority_operation_records", + "oh_authority_records", + "oh_authority_dependencies" + ]; + const results = await this.#client.batch([ + { + sql: PURGE_ROW_SELECT, + args: [this.binding.spaceId] + }, + ...tables.map((table) => ({ + sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`, + args: [this.binding.spaceId] + })), + { sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized + LEFT JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` } + ], "read"); + const receiptRow = results[0]?.rows[0]; + if (receiptRow === undefined || canonicalJson(parsePurgeReceiptRow(receiptRow, this.binding.spaceId, this.binding.bindingSha256)) !== canonicalJson(expected)) { + throw new OhIntegrityError("The remote purge receipt differs from the requested purge."); + } + for (let index = 0;index < tables.length; index += 1) { + const countRow = results[index + 1]?.rows[0]; + if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) { + throw new OhIntegrityError(`Remote purge left rows in ${tables[index]}.`); + } + } + const orphanRow = results[tables.length + 1]?.rows[0]; + if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) { + throw new OhIntegrityError("Remote purge left an orphaned or cross-space operation record."); + } + } + async purgeWorkingSpace(purgedAt) { + this.#assertOpen(); + if (this.binding.profile.profileKind !== "working" || !this.binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + for (let attempt = 0;attempt < 3; attempt += 1) { + const existing = await this.#readPurge(); + if (existing !== null) { + await this.#assertPurgeComplete(existing); + this.#purged = existing; + return existing; + } + const head = await this.head(); + const receipt = createOhSpacePurgeReceiptV1({ binding: this.binding, priorHead: head, purgedAt }); + const receiptExists = "EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ? AND receipt_sha256 = ?)"; + const statements = [{ + sql: `INSERT INTO oh_authority_purges(space_id, + binding_sha256, prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?) + AND EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`, + args: [ + this.binding.spaceId, + this.binding.bindingSha256, + head.operationSha256, + head.sequence, + receipt.purgedAt, + receipt.receiptSha256, + canonicalJson(receipt), + this.binding.spaceId, + head.generation, + head.operationSha256, + this.binding.spaceId, + this.binding.bindingSha256 + ] + }]; + const guardedDelete = (table) => ({ + sql: `DELETE FROM ${table} WHERE space_id = ? AND ${receiptExists}`, + args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256] + }); + statements.push({ + sql: `DELETE FROM oh_authority_operation_records + WHERE operation_sha256 IN (SELECT operation_sha256 FROM oh_authority_operations WHERE space_id = ?) + AND ${receiptExists}`, + args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256] + }); + statements.push(guardedDelete("oh_authority_dependencies")); + statements.push(guardedDelete("oh_authority_records")); + statements.push(guardedDelete("oh_authority_operations")); + statements.push(guardedDelete("oh_authority_bindings")); + statements.push(guardedDelete("oh_authority_spaces")); + statements.push({ + sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized + LEFT JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id) + OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?) + OR NOT ${receiptExists}`, + args: [ + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + this.binding.spaceId, + receipt.receiptSha256 + ] + }); + try { + await this.#client.batch(statements, "write"); + } catch { + const raced = await this.#readPurge(); + if (raced !== null) { + await this.#assertPurgeComplete(raced); + this.#purged = raced; + return raced; + } + continue; + } + const persisted = await this.#readPurge(); + if (persisted !== null) { + await this.#assertPurgeComplete(persisted); + this.#purged = persisted; + return persisted; + } + } + throw new OhConflictError("The remote working space changed repeatedly while purging."); + } + async close() { + if (this.#closed) + return; + this.#closed = true; + if (this.#closeClient) + this.#client.close?.(); + } +} +async function createOhLibSqlStoreAuthorityV1(client, options = {}) { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) + throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ + profile, + realmId: options.realmId ?? `realm:${spaceId}`, + spaceId, + v: 1 + }); + await verifyAuthoritySchema(client); + await initializeSpace(client, binding); + const authority = new OhLibSqlStoreV1(client, binding, options.closeClient ?? false); + const store = Object.freeze({ + binding, + changesSince: (from, changeOptions) => authority.changesSince(from, changeOptions), + close: () => authority.close(), + commit: (input) => authority.commit(input), + exportDependencyClosure: (input) => authority.exportDependencyClosure(input), + head: () => authority.head(), + snapshot: (snapshotOptions) => authority.snapshot(snapshotOptions), + verify: () => authority.verify() + }); + let purge = null; + const host = Object.freeze({ + binding, + purgeWorkingSpace: async (input) => { + if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("This host handle is not bound to a purgeable working profile."); + } + if (purge !== null) + return purge; + purge = await authority.purgeWorkingSpace(input.purgedAt ?? canonicalNow()); + return purge; + } + }); + return Object.freeze({ host, store }); +} +export { + createOhLibSqlStoreAuthorityV1, + bootstrapOhLibSqlAuthorityV1, + OH_LIBSQL_STORE_LIMITS_V1 +}; diff --git a/dist/memory.d.ts b/dist/memory.d.ts new file mode 100644 index 0000000..3c2e0b8 --- /dev/null +++ b/dist/memory.d.ts @@ -0,0 +1,231 @@ +import { type JsonPrimitive, type Sha256Hex } from "./canonical"; +import { type OhRecordCodecRegistry } from "./contract"; +import { type KnowledgeGraphRecordV1 } from "./graph"; +import { type OhProjectionAtomV1, type OhProjectionEvaluationOptionsV1, type OhProjectionQueryV1, type OhProjectionRulePackV1 } from "./projection"; +import { type OhDependencyClosureV1, type OhHeadV1, type OhStoreV1 } from "./store"; +export declare const OH_MEMORY_FORMAT_VERSION_V1: 1; +export declare const OH_MEMORY_CONFLICT_POLICY_V1: "visible-conflicts.v1"; +export declare const OH_MEMORY_LIMITS_V1: Readonly<{ + explainCapabilityEntryBytes: number; + explainCapabilities: 256; + explainCapabilityLifetimeMs: number; + explainCapabilityTotalBytes: number; + factsPerRecordPerExtractor: 512; + maximumExtractorInvocations: 262144; + maximumExtractors: 32; + maximumNominationRoutes: 64; + maximumPrograms: 128; + maximumRecordsPerLane: 8192; + maximumSyntheticRecords: 16384; + rememberBytes: number; + resultBytes: number; + snapshotBytesPerLane: number; + relationsPerExtractor: 64; +}>; +export declare const OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1: Readonly<{ + extractorSha256: Sha256Hex; + factPackId: "oh.memory.composite-facts"; + factPackRevision: 1; + relations: readonly string[]; + semantics: "oh.projection.positive-datalog.v1"; + v: 1; +}>; +export type OhMemoryLaneV1 = "canonical" | "working"; +export type OhMemoryAuthoritySourceV1 = Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + key: string; + lane: OhMemoryLaneV1; + recordSha256: Sha256Hex; + snapshotSha256: Sha256Hex; + v: 1; +}>; +export type OhMemoryProofV1 = Readonly<{ + factPolicy: OhMemoryFactPolicyV1; + kind: "fact"; + relation: string; + sources: readonly OhMemoryAuthoritySourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}> | Readonly<{ + kind: "derived"; + premises: readonly OhMemoryProofV1[]; + premisesTruncated: boolean; + relation: string; + ruleId: string; + ruleSha256: Sha256Hex; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}> | Readonly<{ + kind: "truncated"; + reason: "cycle" | "depth" | "nodes"; + relation: string; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}>; +export type OhMemoryLaneIdentityV1 = Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + datasetSha256: Sha256Hex; + head: OhHeadV1; + lane: OhMemoryLaneV1; + snapshotSha256: Sha256Hex; + v: 1; +}>; +export type OhMemoryIdentityV1 = Readonly<{ + canonical: OhMemoryLaneIdentityV1; + compositeDatasetSha256: Sha256Hex; + conflictPolicy: typeof OH_MEMORY_CONFLICT_POLICY_V1; + evaluationSha256: Sha256Hex; + memorySha256: Sha256Hex; + programId: string; + projectionSha256: Sha256Hex; + purpose: string; + querySha256: Sha256Hex; + rulePackSha256: Sha256Hex; + v: 1; + working: OhMemoryLaneIdentityV1; +}>; +export type OhMemoryConflictV1 = Readonly<{ + canonicalRecordSha256: Sha256Hex; + key: string; + v: 1; + workingRecordSha256: Sha256Hex; +}>; +export type OhMemoryResultRowV1 = Readonly<{ + premiseAuthority: "canonical" | "unknown" | "working"; + premiseLanes: readonly OhMemoryLaneV1[]; + proofsTruncated: boolean; + resultRowSha256: Sha256Hex; + supportCount: number; + v: 1; + values: readonly OhProjectionAtomV1[]; +}>; +export type OhMemoryQueryResultV1 = Readonly<{ + authority: "derived"; + conflicts: readonly OhMemoryConflictV1[]; + explainCapability: Readonly<{ + expiresAt: string; + token: string; + v: 1; + }>; + identity: OhMemoryIdentityV1; + projectionResultSha256: Sha256Hex; + resultSha256: Sha256Hex; + rows: readonly OhMemoryResultRowV1[]; + v: 1; +}>; +export type OhMemoryRememberReceiptV1 = Readonly<{ + actorId: string; + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + instant: string; + lane: "working"; + operationSha256: Sha256Hex; + receiptSha256: Sha256Hex; + requestId: string; + status: "committed"; + v: 1; +}>; +export type OhMemoryExplanationV1 = Readonly<{ + authority: "derived"; + explanationSha256: Sha256Hex; + identity: OhMemoryIdentityV1; + premiseAuthority: OhMemoryResultRowV1["premiseAuthority"]; + premiseLanes: readonly OhMemoryLaneV1[]; + proofs: readonly OhMemoryProofV1[]; + proofsTruncated: boolean; + resultRowSha256: Sha256Hex; + resultSha256: Sha256Hex; + supportCount: number; + v: 1; + values: readonly OhProjectionAtomV1[]; +}>; +export type OhMemoryNominationV1 = Readonly<{ + closure: OhDependencyClosureV1; + destinationPurpose: string; + nominationId: string; + nominationSha256: Sha256Hex; + source: Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + lane: "working"; + v: 1; + }>; + status: "prepared"; + v: 1; +}>; +export type OhMemoryNamedProgramV1 = Readonly<{ + evaluation?: OhProjectionEvaluationOptionsV1; + programId: string; + purpose: string; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; +}>; +export type OhMemoryNominationRouteV1 = Readonly<{ + destinationPurpose: string; + nominationId: string; +}>; +export type OhMemoryFactPolicyV1 = Readonly<{ + extractorSha256: Sha256Hex; + factPackId: string; + kind: "built-in"; + v: 1; +}> | Readonly<{ + extractorId: string; + extractorSha256: Sha256Hex; + kind: "domain"; + v: 1; +}>; +export type OhMemoryFactDeclarationV1 = Readonly<{ + relation: string; + tuple: readonly JsonPrimitive[]; + v: 1; +}>; +/** Host-owned, digest-identified domain projection; it cannot choose sources. */ +export type OhMemoryFactExtractorV1 = Readonly<{ + extract(input: Readonly<{ + lane: OhMemoryLaneV1; + record: KnowledgeGraphRecordV1; + }>): readonly OhMemoryFactDeclarationV1[]; + extractorId: string; + extractorSha256: Sha256Hex; + relations: readonly string[]; +}>; +export type OhMemoryFacadeOptionsV1 = Readonly<{ + actorId: string; + canonical: Readonly<{ + authorityId: string; + expectedBindingSha256: Sha256Hex; + expectedHead: OhHeadV1; + store: OhStoreV1; + }>; + explainCapabilityLifetimeMs?: number; + extractors?: readonly OhMemoryFactExtractorV1[]; + monotonicNow?: () => number; + nominationRoutes?: readonly OhMemoryNominationRouteV1[]; + now?: () => Date; + programs: readonly OhMemoryNamedProgramV1[]; + working: Readonly<{ + authorityId: string; + codecs: OhRecordCodecRegistry; + expectedBindingSha256: Sha256Hex; + store: OhStoreV1; + }>; +}>; +export interface OhMemoryAgentV1 { + explain(value: unknown): Promise; + nominate(value: unknown): Promise; + query(value: unknown): Promise; + remember(value: unknown): Promise; +} +/** + * Creates a model-facing memory surface over two host-bound physical Oh + * authorities. The returned object has no store, locator, rule, sync, canonical + * write, or purge handle. + */ +export declare function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): Promise; +//# sourceMappingURL=memory.d.ts.map \ No newline at end of file diff --git a/dist/memory.d.ts.map b/dist/memory.d.ts.map new file mode 100644 index 0000000..d65e9ce --- /dev/null +++ b/dist/memory.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAEA,OAAO,EASL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,SAAS,CAAC;AACjB,OAAO,EAUL,KAAK,kBAAkB,EAEvB,KAAK,+BAA+B,EAGpC,KAAK,mBAAmB,EAExB,KAAK,sBAAsB,EAE5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAQL,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EAGb,KAAK,SAAS,EACf,MAAM,SAAS,CAAC;AACjB,eAAO,MAAM,2BAA2B,EAAG,CAAU,CAAC;AACtD,eAAO,MAAM,4BAA4B,EAAG,sBAA+B,CAAC;AAC5E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;EAgB9B,CAAC;AAUH,eAAO,MAAM,qCAAqC;;;;;;;EAGhD,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,SAAS,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,SAAS,CAAC;IACxB,cAAc,EAAE,SAAS,CAAC;IAC1B,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GACvB,QAAQ,CAAC;IACT,UAAU,EAAE,oBAAoB,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC9C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GACA,QAAQ,CAAC;IACT,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IACrC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,CAAC;IACtB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GACA,QAAQ,CAAC;IACT,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEL,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,SAAS,CAAC;IACzB,aAAa,EAAE,SAAS,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,cAAc,EAAE,SAAS,CAAC;IAC1B,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,SAAS,EAAE,sBAAsB,CAAC;IAClC,sBAAsB,EAAE,SAAS,CAAC;IAClC,cAAc,EAAE,OAAO,4BAA4B,CAAC;IACpD,gBAAgB,EAAE,SAAS,CAAC;IAC5B,YAAY,EAAE,SAAS,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,SAAS,CAAC;IACvB,cAAc,EAAE,SAAS,CAAC;IAC1B,CAAC,EAAE,CAAC,CAAC;IACL,OAAO,EAAE,sBAAsB,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,qBAAqB,EAAE,SAAS,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,EAAE,CAAC,CAAC;IACL,mBAAmB,EAAE,SAAS,CAAC;CAChC,CAAC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,gBAAgB,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;IACtD,YAAY,EAAE,SAAS,cAAc,EAAE,CAAC;IACxC,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,CAAC,EAAE,CAAC,CAAC;IACL,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACzC,iBAAiB,EAAE,QAAQ,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IACxE,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,sBAAsB,EAAE,SAAS,CAAC;IAClC,YAAY,EAAE,SAAS,CAAC;IACxB,IAAI,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,SAAS,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,eAAe,EAAE,SAAS,CAAC;IAC3B,aAAa,EAAE,SAAS,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,SAAS,EAAE,SAAS,CAAC;IACrB,iBAAiB,EAAE,SAAS,CAAC;IAC7B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,gBAAgB,EAAE,mBAAmB,CAAC,kBAAkB,CAAC,CAAC;IAC1D,YAAY,EAAE,SAAS,cAAc,EAAE,CAAC;IACxC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,SAAS,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,CAAC,EAAE,CAAC,CAAC;IACL,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACvC,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,OAAO,EAAE,qBAAqB,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,MAAM,EAAE,QAAQ,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,SAAS,CAAC;QACzB,IAAI,EAAE,QAAQ,CAAC;QACf,IAAI,EAAE,SAAS,CAAC;QAChB,CAAC,EAAE,CAAC,CAAC;KACN,CAAC,CAAC;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,CAAC;AAEH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAC5B,QAAQ,CAAC;IACT,eAAe,EAAE,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GACA,QAAQ,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,SAAS,CAAC;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEL,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IAChC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,iFAAiF;AACjF,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;QACtB,IAAI,EAAE,cAAc,CAAC;QACrB,MAAM,EAAE,sBAAsB,CAAC;KAChC,CAAC,GAAG,SAAS,yBAAyB,EAAE,CAAC;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,SAAS,CAAC;IAC3B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,QAAQ,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,qBAAqB,EAAE,SAAS,CAAC;QACjC,YAAY,EAAE,QAAQ,CAAC;QACvB,KAAK,EAAE,SAAS,CAAC;KAClB,CAAC,CAAC;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,UAAU,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACxD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC5C,OAAO,EAAE,QAAQ,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,qBAAqB,CAAC;QAC9B,qBAAqB,EAAE,SAAS,CAAC;QACjC,KAAK,EAAE,SAAS,CAAC;KAClB,CAAC,CAAC;CACJ,CAAC,CAAC;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACxD,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAC9D;AAudD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CAwLtG"} \ No newline at end of file diff --git a/dist/memory.js b/dist/memory.js new file mode 100644 index 0000000..25d8bee --- /dev/null +++ b/dist/memory.js @@ -0,0 +1,3007 @@ +// src/canonical.ts +import { createHash, randomBytes } from "node:crypto"; + +class OhValidationError extends Error { + code; + path; + constructor(code, path, message) { + super(`${path}: ${message}`); + this.name = "OhValidationError"; + this.code = code; + this.path = path; + } +} +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} +function assertUnicodeScalarString(value, path) { + for (let index = 0;index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(index + 1); + if (!(next >= 56320 && next <= 57343)) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + index += 1; + } else if (code >= 56320 && code <= 57343) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + } +} +function encodeCanonical(value, path, ancestors) { + if (value === null || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "string") { + assertUnicodeScalarString(value, path); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new OhValidationError("non-json-number", path, "must be finite"); + } + if (Object.is(value, -0)) { + throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical"); + } + return JSON.stringify(value); + } + if (typeof value !== "object" || value === null) { + throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`); + } + if (ancestors.has(value)) { + throw new OhValidationError("cycle", path, "contains a cycle"); + } + ancestors.add(value); + try { + if (Array.isArray(value)) { + const encoded = []; + for (let index = 0;index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes"); + } + encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors)); + } + const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)); + if (extraKeys.length > 0) { + throw new OhValidationError("non-json-property", path, "array has non-index properties"); + } + return `[${encoded.join(",")}]`; + } + if (!isPlainRecord(value)) { + throw new OhValidationError("non-plain-object", path, "must be a plain object"); + } + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== "string")) { + throw new OhValidationError("non-json-property", path, "object has a symbol property"); + } + const keys = ownKeys; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) { + throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property"); + } + } + keys.sort(); + const entries = keys.map((key) => { + assertUnicodeScalarString(key, `${path}.`); + return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`; + }); + return `{${entries.join(",")}}`; + } finally { + ancestors.delete(value); + } +} +function canonicalJson(value) { + return encodeCanonical(value, "$", new Set); +} +function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) { + if (utf8ByteLength(text) > maximumBytes) { + throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new OhValidationError("invalid-json", "$", "is not valid JSON"); + } + if (canonicalJson(value) !== text) { + throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical"); + } + return value; +} +function utf8ByteLength(value) { + return Buffer.byteLength(value, "utf8"); +} +function sha256Hex(value) { + return createHash("sha256").update(value).digest("hex"); +} +function canonicalSha256(value) { + return sha256Hex(canonicalJson(value)); +} +function parseSha256Hex(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null; +} +function parseCanonicalInstantV1(value) { + if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) { + return null; + } + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null; +} +function canonicalNow() { + return new Date().toISOString(); +} +function safeCode(value, maximumLength = 128) { + return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function orderedUnique(values, key) { + return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)); +} +function sortUnique(values, key) { + const sorted = [...values].sort((left, right) => { + const leftKey = key(left); + const rightKey = key(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); + if (!orderedUnique(sorted, key)) { + throw new OhValidationError("duplicate", "$", "contains duplicate canonical values"); + } + return sorted; +} + +// src/graph.ts +var OH_GRAPH_FORMAT_VERSION_V1 = 1; +var OH_GRAPH_LIMITS_V1 = Object.freeze({ + changesPerOperation: 8192, + dependenciesPerRecord: 4096, + recordBytes: 1024 * 1024, + recordsPerSnapshot: 65536 +}); +var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [ + "activity", + "assertion", + "context", + "dependency-manifest", + "edition", + "entity", + "evidence", + "identity-operation", + "inquiry", + "inquiry-event", + "review-decision", + "rights-decision", + "schema", + "shape", + "statement", + "type-membership", + "view", + "vocabulary" +]; +function recordKey(value) { + return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function createKnowledgeGraphRecordV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies)) + throw new TypeError("Invalid graph record input."); + const key = recordKey(input.key); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind); + if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) + throw new TypeError("Invalid graph record identity."); + const dependencies = input.dependencies.map(recordKey); + if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) { + throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive."); + } + const valueJson = canonicalJson(input.value); + if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) { + throw new RangeError("Graph record value exceeds its canonical byte limit."); + } + const payload = { dependencies, key, kind, v: 1, value: input.value }; + return { ...payload, recordSha256: canonicalSha256(payload) }; +} +function parseKnowledgeGraphRecordV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256")) + return null; + const recordSha256 = parseSha256Hex(value.recordSha256); + const { recordSha256: _digest, ...input } = value; + try { + const created = createKnowledgeGraphRecordV1(input); + return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null; + } catch { + return null; + } +} +function knowledgeGraphRecordRefV1(record) { + return { + dependencies: record.dependencies, + key: record.key, + kind: record.kind, + sha256: record.recordSha256, + v: 1 + }; +} +function changeKey(change) { + return change.kind === "put" ? change.record.key : change.key; +} +function canonicalKnowledgeGraphChangesV1(changes) { + const normalized = []; + for (const change of changes) { + if (!isPlainRecord(change) || change.v !== 1) + throw new TypeError("Invalid graph change."); + if (change.kind === "put") { + const record = parseKnowledgeGraphRecordV1(change.record); + if (record === null) + throw new TypeError("Invalid graph record in change."); + normalized.push({ kind: "put", record, v: 1 }); + } else if (change.kind === "tombstone") { + const key = recordKey(change.key); + const priorSha256 = parseSha256Hex(change.priorSha256); + if (key === null || priorSha256 === null) + throw new TypeError("Invalid graph tombstone."); + normalized.push({ key, kind: "tombstone", priorSha256, v: 1 }); + } else + throw new TypeError("Unknown graph change kind."); + } + return sortUnique(normalized, changeKey); +} +function graphRevisionSha256V1(input) { + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + const operationId = safeCode(input.operationId); + const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256); + const recordsSha256 = parseSha256Hex(input.recordsSha256); + const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null; + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) { + throw new TypeError("Invalid graph revision digest input."); + } + return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 }); +} + +// src/ontology.ts +var OH_ONTOLOGY_VERSION_V1 = "1.0.0"; +var OH_CONTRACT_ID_V1 = "oh.ontology.v1"; +var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({ + dimensions: 64, + listValues: 256, + qualifiers: 128, + statementBytes: 256 * 1024, + textBytes: 64 * 1024 +}); + +// src/schema.ts +var OH_SCHEMA_FORMAT_VERSION_V1 = 1; + +// src/contract.ts +var manifestPayload = Object.freeze({ + contractId: OH_CONTRACT_ID_V1, + graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1, + ontologyVersion: OH_ONTOLOGY_VERSION_V1, + recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1, + v: 1 +}); +var OH_CONTRACT_MANIFEST_V1 = Object.freeze({ + ...manifestPayload, + contractSha256: canonicalSha256(manifestPayload) +}); +class OhRecordCodecRegistry { + #codecs = new Map; + #sealed = false; + register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); + if (this.#codecs.has(codec.kind)) + throw new TypeError(`A codec is already registered for ${codec.kind}.`); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); + return this; + } + parse(kind, value) { + const codec = this.#codecs.get(kind); + if (codec !== undefined) + return codec.parse(value); + try { + canonicalJson(value); + return value; + } catch { + return null; + } + } + has(kind) { + return this.#codecs.has(kind); + } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } +} +// src/operation.ts +var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024; +function parsePayload(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "changes", + "contractId", + "graphRevisionSha256", + "instant", + "operationId", + "parentOperationSha256", + "recordsSha256", + "sequence", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes)) + return null; + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const spaceId = safeCode(value.spaceId); + const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256); + const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const instant = parseCanonicalInstantV1(value.instant); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null; + let changes; + try { + changes = canonicalKnowledgeGraphChangesV1(value.changes); + } catch { + return null; + } + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) + return null; + return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? { + actorId, + changes, + contractId: OH_CONTRACT_ID_V1, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256, + recordsSha256, + sequence, + spaceId, + v: 1 + } : null; +} +function createOhOperationV1(input) { + const payload = parsePayload(input); + if (payload === null) + throw new TypeError("Invalid Oh operation payload."); + const operation = { ...payload, operationSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) { + throw new RangeError("Oh operation exceeds its canonical byte limit."); + } + return operation; +} +function parseOhOperationV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256")) + return null; + const operationSha256 = parseSha256Hex(value.operationSha256); + const { operationSha256: _digest, ...input } = value; + const payload = parsePayload(input); + return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null; +} + +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} + +// src/memory.ts +import { randomBytes as randomBytes2 } from "node:crypto"; + +// src/projection.ts +var OH_PROJECTION_FORMAT_VERSION_V1 = 1; +var OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1"; +var OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1"; +var OH_PROJECTION_LIMITS_V1 = Object.freeze({ + arity: 32, + atomBytes: 16 * 1024, + derivedTuples: 262144, + facts: 262144, + literalsPerRule: 64, + proofDepth: 128, + proofNodes: 4096, + queryLiterals: 64, + queryMatches: 262144, + queryResults: 65536, + relations: 4096, + resultBytes: 16 * 1024 * 1024, + rounds: 1024, + rules: 1024, + sourcesPerFact: 64, + totalProofNodes: 65536, + variables: 256, + workUnits: 16777216 +}); +var recordFactExtractorPayloadV1 = { + factPackId: "oh.record-facts", + factPackRevision: 1, + relations: ["oh.dependency", "oh.record"], + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 +}; +var OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({ + ...recordFactExtractorPayloadV1, + extractorSha256: canonicalSha256(recordFactExtractorPayloadV1) +}); +function nonnegativeInteger(value) { + return Number.isSafeInteger(value) && value >= 0 ? value : null; +} +function positiveInteger(value, maximum = Number.MAX_SAFE_INTEGER) { + return Number.isSafeInteger(value) && value >= 1 && value <= maximum ? value : null; +} +function projectionName(value, maximumLength = 128) { + return safeCode(value, maximumLength); +} +function compareCanonical(left, right) { + const leftKey = canonicalJson(left); + const rightKey = canonicalJson(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; +} +function compareProjectionFacts(left, right) { + return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]); +} +var INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom"); +function atom(value) { + if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") { + return INVALID_PROJECTION_ATOM; + } + try { + const encoded = canonicalJson(value); + return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes ? value : INVALID_PROJECTION_ATOM; + } catch { + return INVALID_PROJECTION_ATOM; + } +} +function tuple(value) { + if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity) + return null; + const parsed = value.map(atom); + return parsed.some((item) => item === INVALID_PROJECTION_ATOM) ? null : parsed; +} +function parseRecordRef(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"]) || value.v !== 1 || !Array.isArray(value.dependencies)) + return null; + const key = safeCode(value.key, 512); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind); + const sha256 = parseSha256Hex(value.sha256); + const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512)); + if (key === null || kind === undefined || sha256 === null || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord || dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) + return null; + return { dependencies, key, kind, sha256, v: 1 }; +} +function createOhProjectionSnapshotV1(input) { + const spaceId = projectionName(input.spaceId); + const generation = nonnegativeInteger(input.head.generation); + const sequence = nonnegativeInteger(input.head.sequence); + const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256); + const graphRevisionSha256 = input.head.graphRevisionSha256 === null ? null : parseSha256Hex(input.head.graphRevisionSha256); + const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256); + if (spaceId === null || generation === null || sequence === null || generation !== sequence || input.head.operationSha256 !== null && operationSha256 === null || input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid projection snapshot head."); + } + const records = input.records.map(parseKnowledgeGraphRecordV1); + if (records.some((record) => record === null)) + throw new TypeError("Invalid record in projection snapshot."); + const recordRefs = sortUnique(records.map(knowledgeGraphRecordRefV1), (reference) => reference.key); + const recordsSha256 = canonicalSha256(recordRefs); + if (recordsSha256 !== declaredRecordsSha256) { + throw new TypeError("Projection snapshot records do not reproduce the declared head."); + } + const keys = new Set(recordRefs.map((reference) => reference.key)); + if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) { + throw new TypeError("Projection snapshot has a missing record dependency."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return { ...payload, snapshotSha256: canonicalSha256(payload) }; +} +function parseOhProjectionSnapshotV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "generation", + "graphRevisionSha256", + "operationSha256", + "recordRefs", + "recordsSha256", + "sequence", + "snapshotSha256", + "spaceId", + "v" + ]) || value.v !== 1 || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const generation = nonnegativeInteger(value.generation); + const sequence = nonnegativeInteger(value.sequence); + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + const spaceId = projectionName(value.spaceId); + const recordRefs = value.recordRefs.map(parseRecordRef); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null || value.graphRevisionSha256 !== null && graphRevisionSha256 === null || value.operationSha256 !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || recordRefs.some((reference) => reference === null)) + return null; + const refs = recordRefs; + if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256) + return null; + const keys = new Set(refs.map((reference) => reference.key)); + if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) + return null; + const payload = { + contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs: refs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null; +} +function createOhProjectionFactV1(input) { + const relation = projectionName(input.relation); + const parsedTuple = tuple(input.tuple); + if (relation === null || parsedTuple === null || input.sources.length < 1 || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) { + throw new TypeError("Invalid projection fact."); + } + const sources = input.sources.map((source) => { + if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) { + throw new TypeError("Invalid projection fact source."); + } + const key = safeCode(source.key, 512); + const recordSha256 = parseSha256Hex(source.recordSha256); + if (key === null || recordSha256 === null) + throw new TypeError("Invalid projection fact source."); + return { key, recordSha256, v: 1 }; + }).sort(compareCanonical); + if (!orderedUnique(sources, (source) => source.key)) { + throw new TypeError("Projection fact sources must have unique record keys."); + } + const payload = { relation, sources, tuple: parsedTuple, v: 1 }; + return { ...payload, factSha256: canonicalSha256(payload) }; +} +function parseOhProjectionFactV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"]) || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple)) + return null; + const factSha256 = parseSha256Hex(value.factSha256); + try { + const fact = createOhProjectionFactV1({ + relation: value.relation, + sources: value.sources, + tuple: value.tuple + }); + return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null; + } catch { + return null; + } +} +function mergeProjectionFacts(facts) { + const grouped = new Map; + for (const fact of facts) { + const identity = canonicalJson([fact.relation, fact.tuple]); + let group = grouped.get(identity); + if (group === undefined) { + group = { relation: fact.relation, sources: new Map, tuple: fact.tuple }; + grouped.set(identity, group); + } + for (const source of fact.sources) { + const existing = group.sources.get(source.key); + if (existing !== undefined && existing.recordSha256 !== source.recordSha256) { + throw new TypeError("One fact source key is bound to multiple record digests."); + } + group.sources.set(source.key, source); + } + } + return [...grouped.values()].map((group) => createOhProjectionFactV1({ + relation: group.relation, + sources: [...group.sources.values()], + tuple: group.tuple + })).sort(compareProjectionFacts); +} +function createOhProjectionDatasetV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const extractorSha256 = parseSha256Hex(input.extractorSha256); + const factPackId = projectionName(input.factPackId); + const factPackRevision = positiveInteger(input.factPackRevision); + if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null || input.facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new TypeError("Invalid projection dataset."); + const parsedFacts = input.facts.map(parseOhProjectionFactV1); + if (parsedFacts.some((fact) => fact === null)) + throw new TypeError("Invalid fact in projection dataset."); + const facts = mergeProjectionFacts(parsedFacts); + if (facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new RangeError("Projection dataset has too many facts."); + const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256])); + for (const fact of facts) { + for (const source of fact.sources) { + if (refs.get(source.key) !== source.recordSha256) { + throw new TypeError("Projection fact source is not present at the exact input snapshot."); + } + } + } + const factPackPayload = { + extractorSha256, + factPackId, + factPackRevision, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + const factPackSha256 = canonicalSha256(factPackPayload); + const factsSha256 = canonicalSha256(facts); + const payload = { + extractorSha256, + factPackId, + factPackRevision, + factPackSha256, + facts, + factsSha256, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, datasetSha256: canonicalSha256(payload) }; +} +function parseOhProjectionDatasetV1(value, snapshot) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "datasetSha256", + "extractorSha256", + "factPackId", + "factPackRevision", + "factPackSha256", + "facts", + "factsSha256", + "snapshotSha256", + "v" + ]) || value.v !== 1 || !Array.isArray(value.facts)) + return null; + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256); + const declaredFactsSha256 = parseSha256Hex(value.factsSha256); + try { + const dataset = createOhProjectionDatasetV1({ + extractorSha256: value.extractorSha256, + factPackId: value.factPackId, + factPackRevision: value.factPackRevision, + facts: value.facts, + snapshot + }); + return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256 && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256 && dataset.datasetSha256 === datasetSha256 ? dataset : null; + } catch { + return null; + } +} +function ohProjectionVariableV1(name) { + const parsed = projectionName(name); + if (parsed === null) + throw new TypeError("Invalid projection variable name."); + return { kind: "variable", name: parsed, v: 1 }; +} +function ohProjectionConstantV1(value) { + const parsed = atom(value); + if (parsed === INVALID_PROJECTION_ATOM) + throw new TypeError("Invalid projection constant."); + return { kind: "constant", v: 1, value: parsed }; +} +function createOhProjectionLiteralV1(input) { + const relation = projectionName(input.relation); + if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) { + throw new TypeError("Invalid projection literal."); + } + const terms = input.terms.map((term) => parseOhProjectionTermV1(term)); + if (terms.some((term) => term === null)) + throw new TypeError("Invalid term in projection literal."); + return { relation, terms, v: 1 }; +} +function parseOhProjectionTermV1(value) { + if (!isPlainRecord(value) || value.v !== 1) + return null; + if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) { + const name = projectionName(value.name); + return name === null ? null : { kind: "variable", name, v: 1 }; + } + if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) { + const parsed = atom(value.value); + return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed }; + } + return null; +} +function parseOhProjectionLiteralV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"]) || value.v !== 1 || !Array.isArray(value.terms)) + return null; + try { + return createOhProjectionLiteralV1({ + relation: value.relation, + terms: value.terms + }); + } catch { + return null; + } +} +function literalVariables(literal) { + return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []); +} +function createOhProjectionRuleV1(input) { + const ruleId = projectionName(input.ruleId); + const head = parseOhProjectionLiteralV1(input.head); + if (ruleId === null || head === null || input.body.length < 1 || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule) + throw new TypeError("Invalid projection rule."); + const body = input.body.map(parseOhProjectionLiteralV1); + if (body.some((literal) => literal === null)) + throw new TypeError("Invalid body literal in projection rule."); + const bound = new Set(body.flatMap(literalVariables)); + if (literalVariables(head).some((variable) => !bound.has(variable)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projection rule head variable must be bound in its body."); + } + const payload = { body, head, ruleId, v: 1 }; + return { ...payload, ruleSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRuleV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"]) || value.v !== 1 || !Array.isArray(value.body)) + return null; + const ruleSha256 = parseSha256Hex(value.ruleSha256); + try { + const rule = createOhProjectionRuleV1({ + body: value.body, + head: value.head, + ruleId: value.ruleId + }); + return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null; + } catch { + return null; + } +} +function createOhProjectionRulePackV1(input) { + const rulePackId = projectionName(input.rulePackId); + const rulePackRevision = positiveInteger(input.rulePackRevision); + if (rulePackId === null || rulePackRevision === null || input.rules.length < 1 || input.rules.length > OH_PROJECTION_LIMITS_V1.rules) + throw new TypeError("Invalid projection rule pack."); + const parsedRules = input.rules.map(parseOhProjectionRuleV1); + if (parsedRules.some((rule) => rule === null)) + throw new TypeError("Invalid rule in projection rule pack."); + const rules = sortUnique(parsedRules, (rule) => rule.ruleId); + const rulesSha256 = canonicalSha256(rules); + const payload = { + rulePackId, + rulePackRevision, + rules, + rulesSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + return { ...payload, rulePackSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRulePackV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "rulePackId", + "rulePackRevision", + "rulePackSha256", + "rules", + "rulesSha256", + "semantics", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules)) + return null; + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const rulesSha256 = parseSha256Hex(value.rulesSha256); + try { + const pack = createOhProjectionRulePackV1({ + rulePackId: value.rulePackId, + rulePackRevision: value.rulePackRevision, + rules: value.rules + }); + return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null; + } catch { + return null; + } +} +function createOhProjectionQueryV1(input) { + const queryId = projectionName(input.queryId); + const limit = positiveInteger(input.limit ?? 1000, OH_PROJECTION_LIMITS_V1.queryResults); + if (queryId === null || limit === null || input.find.length < 1 || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1 || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals) + throw new TypeError("Invalid projection query."); + const find = input.find.map((name) => projectionName(name)); + const where = input.where.map(parseOhProjectionLiteralV1); + if (find.some((name) => name === null) || !orderedUnique([...find].sort(), String) || where.some((literal) => literal === null)) + throw new TypeError("Invalid projection query variables or literals."); + const bound = new Set(where.flatMap(literalVariables)); + if (find.some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projected query variable must be bound in the query body."); + } + const payload = { + find, + limit, + queryId, + where, + v: 1 + }; + return { ...payload, querySha256: canonicalSha256(payload) }; +} +function parseOhProjectionQueryV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"]) || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where)) + return null; + const querySha256 = parseSha256Hex(value.querySha256); + try { + const query = createOhProjectionQueryV1({ + find: value.find, + limit: value.limit, + queryId: value.queryId, + where: value.where + }); + return querySha256 !== null && query.querySha256 === querySha256 ? query : null; + } catch { + return null; + } +} +function createOhProjectionIdentityV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const query = parseOhProjectionQueryV1(input.query); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + if (snapshot === null || dataset === null || query === null || rulePack === null) { + throw new TypeError("Invalid projection identity input."); + } + const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256); + if (engine === null) + throw new TypeError("Invalid projection engine identity."); + const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 }; + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + datasetSha256: dataset.datasetSha256, + engineSha256: canonicalSha256({ engine, v: 1 }), + evaluationSha256: canonicalSha256(evaluation), + querySha256: query.querySha256, + rulePackSha256: rulePack.rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, projectionSha256: canonicalSha256(payload) }; +} +function parseOhProjectionIdentityV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "datasetSha256", + "engineSha256", + "evaluationSha256", + "projectionSha256", + "querySha256", + "rulePackSha256", + "semantics", + "snapshotSha256", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const engineSha256 = parseSha256Hex(value.engineSha256); + const evaluationSha256 = parseSha256Hex(value.evaluationSha256); + const projectionSha256 = parseSha256Hex(value.projectionSha256); + const querySha256 = parseSha256Hex(value.querySha256); + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null || querySha256 === null || rulePackSha256 === null || snapshotSha256 === null) + return null; + const payload = { + contractSha256, + datasetSha256, + engineSha256, + evaluationSha256, + querySha256, + rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256, + v: 1 + }; + return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null; +} +function invalidationForOhProjectionV1(previous, next) { + const parsedPrevious = parseOhProjectionIdentityV1(previous); + const parsedNext = parseOhProjectionIdentityV1(next); + if (parsedPrevious === null || parsedNext === null) + throw new TypeError("Invalid projection identity."); + if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256) + return { kind: "reusable", v: 1 }; + const reasons = []; + if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256) + reasons.push("snapshot-changed"); + if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256) + reasons.push("dataset-changed"); + if (parsedPrevious.engineSha256 !== parsedNext.engineSha256) + reasons.push("engine-changed"); + if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256) + reasons.push("evaluation-changed"); + if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256) + reasons.push("rule-pack-changed"); + if (parsedPrevious.querySha256 !== parsedNext.querySha256) + reasons.push("query-changed"); + return { kind: "full-rebuild", reasons, v: 1 }; +} +function tupleKey(value) { + return canonicalJson(value); +} +function referenceKey(reference) { + return canonicalJson([reference.relation, reference.tuple]); +} +function relationTuples(relations, relation) { + return [...relations.get(relation)?.values() ?? []].sort((left, right) => compareCanonical(left.tuple, right.tuple)); +} +function setArity(arities, relation, arity) { + const existing = arities.get(relation); + if (existing !== undefined && existing !== arity) { + throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`); + } + arities.set(relation, arity); + if (arities.size > OH_PROJECTION_LIMITS_V1.relations) + throw new RangeError("Projection uses too many relations."); +} +function validateProgramArities(dataset, rulePack, query) { + const arities = new Map; + for (const fact of dataset.facts) + setArity(arities, fact.relation, fact.tuple.length); + for (const rule of rulePack.rules) { + setArity(arities, rule.head.relation, rule.head.terms.length); + for (const literal of rule.body) + setArity(arities, literal.relation, literal.terms.length); + } + for (const literal of query.where) + setArity(arities, literal.relation, literal.terms.length); +} +function sameAtom(left, right) { + return left === right; +} +function unifyLiteral(literal, state, binding) { + const next = new Map(binding); + for (let index = 0;index < literal.terms.length; index += 1) { + const term = literal.terms[index]; + const value = state.tuple[index]; + if (term.kind === "constant") { + if (!sameAtom(term.value, value)) + return null; + continue; + } + if (next.has(term.name)) { + if (!sameAtom(next.get(term.name), value)) + return null; + } else + next.set(term.name, value); + } + return next; +} +function consumeWorkUnit(budget) { + if (budget.units >= budget.maximum) + throw new RangeError("Projection exceeds its work-unit bound."); + budget.units += 1; +} +function matchBody(relations, body, maximumMatches, work) { + let matches = [{ binding: new Map, premises: [] }]; + for (const literal of body) { + const next = []; + const candidates = relationTuples(relations, literal.relation); + for (const match of matches) { + for (const candidate of candidates) { + consumeWorkUnit(work); + const binding = unifyLiteral(literal, candidate, match.binding); + if (binding === null) + continue; + next.push({ binding, premises: [...match.premises, { + relation: literal.relation, + tuple: candidate.tuple + }] }); + if (next.length > maximumMatches) + throw new RangeError("Projection join exceeds its match bound."); + } + } + matches = next; + if (matches.length === 0) + break; + } + return matches; +} +function instantiateHead(head, binding) { + return head.terms.map((term) => term.kind === "constant" ? term.value : binding.get(term.name)); +} +function canonicalWitness(witness) { + if (witness.kind === "fact") + return canonicalJson(witness); + return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 }); +} +function materializeNaive(input) { + const relations = new Map; + for (const fact of input.dataset.facts) { + let relation = relations.get(fact.relation); + if (relation === undefined) { + relation = new Map; + relations.set(fact.relation, relation); + } + relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } }); + } + let derivedFacts = 0; + let rounds = 0; + while (true) { + const candidates = new Map; + for (const rule of input.rulePack.rules) { + for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) { + const derivedTuple = instantiateHead(rule.head, match.binding); + const relation = relations.get(rule.head.relation); + const key = tupleKey(derivedTuple); + if (relation?.has(key) === true) + continue; + const state = { + tuple: derivedTuple, + witness: { kind: "derived", premises: match.premises, rule } + }; + const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple }); + const existing = candidates.get(identity); + if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) { + candidates.set(identity, { relation: rule.head.relation, state }); + } + } + } + if (candidates.size === 0) + break; + if (rounds >= input.maximumRounds) + throw new RangeError("Projection exceeds its evaluation round bound."); + if (derivedFacts + candidates.size > input.maximumDerivedTuples) { + throw new RangeError("Projection exceeds its derived tuple bound."); + } + const ordered = [...candidates.values()].sort((left, right) => compareCanonical([left.relation, left.state.tuple], [right.relation, right.state.tuple])); + for (const candidate of ordered) { + let relation = relations.get(candidate.relation); + if (relation === undefined) { + relation = new Map; + relations.set(candidate.relation, relation); + } + relation.set(tupleKey(candidate.state.tuple), candidate.state); + } + derivedFacts += candidates.size; + rounds += 1; + } + return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds }; +} +function boundedOption(value, fallback, maximum, label) { + const parsed = positiveInteger(value ?? fallback, maximum); + if (parsed === null) + throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`); + return parsed; +} +function resolveEvaluationOptions(options) { + const resolved = { + maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"), + maximumProofDepth: boundedOption(options.maximumProofDepth, 32, OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"), + maximumProofNodes: boundedOption(options.maximumProofNodes, 1024, OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"), + maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"), + maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"), + maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"), + maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits") + }; + if (resolved.maximumResultBytes < 64 * 1024) { + throw new RangeError("maximumResultBytes must be at least 65536."); + } + return resolved; +} +function reserveResultBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function reserveProofNode(budget, options, envelope) { + if (budget.nodes >= options.maximumProofNodes || budget.result.nodes >= options.maximumTotalProofNodes || !reserveResultBytes(budget.result, envelope)) + return false; + budget.nodes += 1; + budget.result.nodes += 1; + return true; +} +function proofForReference(relations, reference, budget, options, depth, visiting) { + if (depth >= options.maximumProofDepth) { + const proof = { + kind: "truncated", + reason: "depth", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const identity = referenceKey(reference); + if (visiting.has(identity)) { + const proof = { + kind: "truncated", + reason: "cycle", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple)); + if (state === undefined) + throw new Error("Projection proof references a tuple outside the materialized result."); + if (state.witness.kind === "fact") { + const proof = { + kind: "fact", + relation: reference.relation, + sources: state.witness.sources, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const envelope = { + kind: "derived", + premises: [], + premisesTruncated: false, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + if (!reserveProofNode(budget, options, envelope)) + return null; + visiting.add(identity); + try { + const premises = []; + let premisesTruncated = false; + for (const premise of state.witness.premises) { + const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting); + if (proof === null) { + premisesTruncated = true; + break; + } + premises.push(proof); + } + return { + kind: "derived", + premises, + premisesTruncated, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + } finally { + visiting.delete(identity); + } +} +function proofIsTruncated(proof) { + return proof.kind === "truncated" || proof.kind === "derived" && (proof.premisesTruncated || proof.premises.some(proofIsTruncated)); +} +function reserveProjectionParseBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function parseProjectionFactSource(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) { + return null; + } + const key = safeCode(value.key, 512); + const recordSha256 = parseSha256Hex(value.recordSha256); + return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 }; +} +function parseProjectionProofWithBudget(value, budget, depth) { + if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes || !isPlainRecord(value) || value.v !== 1) + return null; + const relation = projectionName(value.relation); + const parsedTuple = tuple(value.tuple); + if (relation === null || parsedTuple === null) + return null; + if (value.kind === "fact") { + if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"]) || !Array.isArray(value.sources) || value.sources.length < 1 || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) + return null; + const sources = value.sources.map(parseProjectionFactSource); + if (sources.some((source) => source === null)) + return null; + const parsedSources = sources; + if (!orderedUnique(parsedSources, (source) => source.key)) + return null; + const proof = { + kind: "fact", + relation, + sources: parsedSources, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind === "truncated") { + if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"]) || value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes") + return null; + const reason = value.reason; + const proof = { + kind: "truncated", + reason, + relation, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind !== "derived" || !hasExactKeys(value, [ + "kind", + "premises", + "premisesTruncated", + "relation", + "ruleId", + "ruleSha256", + "tuple", + "v" + ]) || !Array.isArray(value.premises) || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule || typeof value.premisesTruncated !== "boolean") + return null; + const ruleId = projectionName(value.ruleId); + const ruleSha256 = parseSha256Hex(value.ruleSha256); + if (ruleId === null || ruleSha256 === null || !value.premisesTruncated && value.premises.length === 0 || value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule) { + return null; + } + const skeleton = { + kind: "derived", + premises: [], + premisesTruncated: value.premisesTruncated, + relation, + ruleId, + ruleSha256, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, skeleton)) + return null; + budget.nodes += 1; + const premises = []; + for (const premise of value.premises) { + const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1); + if (parsed === null) + return null; + premises.push(parsed); + } + return { ...skeleton, premises }; +} +function parseOhProjectionProofV1(value) { + try { + const budget = { + bytes: 0, + maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes, + maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth, + maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes, + nodes: 0 + }; + const proof = parseProjectionProofWithBudget(value, budget, 0); + return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null; + } catch { + return null; + } +} +function parseProjectionEvaluation(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "maximumDerivedTuples", + "maximumProofDepth", + "maximumProofNodes", + "maximumResultBytes", + "maximumRounds", + "maximumTotalProofNodes", + "maximumWorkUnits", + "v" + ]) || value.v !== 1) + return null; + const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples); + const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth); + const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes); + const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes); + const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds); + const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes); + const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits); + if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null || maximumTotalProofNodes === null || maximumWorkUnits === null) + return null; + return { + maximumDerivedTuples, + maximumProofDepth, + maximumProofNodes, + maximumResultBytes, + maximumRounds, + maximumTotalProofNodes, + maximumWorkUnits, + v: 1 + }; +} +function parseProjectionResultRow(value, evaluation, resultBudget) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1 || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals || typeof value.proofsTruncated !== "boolean") + return null; + const values = tuple(value.values); + const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches); + if (values === null || supportCount === null || !value.proofsTruncated && value.proofs.length === 0) + return null; + if (!reserveProjectionParseBytes(resultBudget, { + proofs: [], + proofsTruncated: value.proofsTruncated, + supportCount, + values, + v: 1 + })) + return null; + const before = resultBudget.nodes; + resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes); + const proofs = []; + for (const proof of value.proofs) { + const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0); + if (parsed === null) + return null; + proofs.push(parsed); + } + resultBudget.maximumNodes = evaluation.maximumTotalProofNodes; + const containsTruncation = proofs.some(proofIsTruncated); + if (!value.proofsTruncated && containsTruncation || value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals && !containsTruncation) + return null; + return { + nodes: resultBudget.nodes - before, + row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 } + }; +} +function parseOhProjectionResultV1(value, expectedProjectionSha256) { + try { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "authority", + "cache", + "engine", + "evaluation", + "identity", + "resultSha256", + "rows", + "stats", + "v" + ]) || value.v !== 1 || value.authority !== "derived" || !isPlainRecord(value.cache) || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild" || value.cache.v !== 1 || !Array.isArray(value.rows) || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats) || !hasExactKeys(value.stats, [ + "baseFacts", + "derivedFacts", + "proofNodes", + "proofsTruncated", + "queryMatches", + "relations", + "rounds", + "truncated", + "truncationReasons", + "v", + "workUnits" + ]) || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons) || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean") + return null; + const engine = safeCode(value.engine, 256); + const evaluation = parseProjectionEvaluation(value.evaluation); + const identity = parseOhProjectionIdentityV1(value.identity); + const resultSha256 = parseSha256Hex(value.resultSha256); + const expected = expectedProjectionSha256 === undefined ? undefined : parseSha256Hex(expectedProjectionSha256); + if (engine === null || evaluation === null || identity === null || resultSha256 === null || expectedProjectionSha256 !== undefined && expected === null || expected !== undefined && identity.projectionSha256 !== expected || identity.engineSha256 !== canonicalSha256({ engine, v: 1 }) || identity.evaluationSha256 !== canonicalSha256(evaluation)) + return null; + const baseFacts = nonnegativeInteger(value.stats.baseFacts); + const derivedFacts = nonnegativeInteger(value.stats.derivedFacts); + const proofNodes = nonnegativeInteger(value.stats.proofNodes); + const queryMatches = nonnegativeInteger(value.stats.queryMatches); + const relations = nonnegativeInteger(value.stats.relations); + const rounds = nonnegativeInteger(value.stats.rounds); + const workUnits = nonnegativeInteger(value.stats.workUnits); + if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations || rounds === null || rounds > evaluation.maximumRounds || workUnits === null || workUnits > evaluation.maximumWorkUnits || relations > baseFacts + derivedFacts || rounds > derivedFacts || rounds === 0 !== (derivedFacts === 0) || queryMatches > workUnits) + return null; + const truncationReasons = value.stats.truncationReasons; + if (truncationReasons.length > 2 || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x") || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes") || value.stats.truncated !== truncationReasons.length > 0) + return null; + const budget = { + bytes: 0, + maximumBytes: evaluation.maximumResultBytes, + maximumDepth: evaluation.maximumProofDepth, + maximumNodes: evaluation.maximumTotalProofNodes, + nodes: 0 + }; + const rows = []; + let supportCount = 0; + for (const row of value.rows) { + const parsed = parseProjectionResultRow(row, evaluation, budget); + if (parsed === null) + return null; + rows.push(parsed.row); + supportCount += parsed.row.supportCount; + if (supportCount > queryMatches) + return null; + } + if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated) || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches)) + return null; + const reasons = truncationReasons; + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine, + evaluation, + identity, + rows, + stats: { + baseFacts, + derivedFacts, + proofNodes, + proofsTruncated: value.stats.proofsTruncated, + queryMatches, + relations, + rounds, + truncated: value.stats.truncated, + truncationReasons: reasons, + v: 1, + workUnits + }, + v: 1 + }; + const serialized = canonicalJson(payload); + return utf8ByteLength(serialized) <= evaluation.maximumResultBytes && sha256Hex(serialized) === resultSha256 ? { ...payload, resultSha256 } : null; + } catch { + return null; + } +} +function buildProjectionResult(input) { + const matches = matchBody(input.materialized.relations, input.query.where, OH_PROJECTION_LIMITS_V1.queryMatches, input.work); + const byValues = new Map; + for (const match of matches) { + const values = input.query.find.map((name) => match.binding.get(name)); + const key = tupleKey(values); + const existing = byValues.get(key); + if (existing === undefined) + byValues.set(key, { match, supportCount: 1 }); + else + byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0 ? match : existing.match, supportCount: existing.supportCount + 1 }); + } + const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); + const resultBudget = { + bytes: 0, + maximumBytes: input.options.maximumResultBytes - 64 * 1024, + nodes: 0 + }; + const rows = []; + let resultBytesTruncated = false; + for (const [key, support] of ordered.slice(0, input.query.limit)) { + const values = JSON.parse(key); + if (!reserveResultBytes(resultBudget, { + proofs: [], + proofsTruncated: false, + supportCount: support.supportCount, + values, + v: 1 + })) { + resultBytesTruncated = true; + break; + } + const budget = { nodes: 0, result: resultBudget }; + const proofs = []; + for (const premise of support.match.premises) { + const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set); + if (proof === null) + break; + proofs.push(proof); + } + const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated); + rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 }); + } + const queryLimitTruncated = ordered.length > input.query.limit; + const truncationReasons = [ + ...queryLimitTruncated ? ["query-limit"] : [], + ...resultBytesTruncated ? ["result-bytes"] : [] + ]; + const truncated = truncationReasons.length > 0; + const identity = createOhProjectionIdentityV1({ + dataset: input.dataset, + query: input.query, + engine: input.engine, + options: input.options, + rulePack: input.rulePack, + snapshot: input.snapshot + }); + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine: input.engine, + evaluation: { ...input.options, v: 1 }, + identity, + rows, + stats: { + baseFacts: input.materialized.baseFacts, + derivedFacts: input.materialized.derivedFacts, + proofNodes: resultBudget.nodes, + proofsTruncated: rows.some((row) => row.proofsTruncated), + queryMatches: matches.length, + relations: input.materialized.relations.size, + rounds: input.materialized.rounds, + truncated, + truncationReasons, + v: 1, + workUnits: input.work.units + }, + v: 1 + }; + const serialized = canonicalJson(payload); + if (utf8ByteLength(serialized) > input.options.maximumResultBytes) { + throw new RangeError("Projection result exceeds its canonical byte bound."); + } + return { ...payload, resultSha256: sha256Hex(serialized) }; +} +function evaluateOhProjectionV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + if (snapshot === null || dataset === null || rulePack === null || query === null) { + throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const materialized = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + return buildProjectionResult({ + dataset, + engine: OH_PROJECTION_INTERNAL_ENGINE_V1, + materialized, + options, + query, + rulePack, + snapshot, + work + }); +} +function evaluateOhProjectionWithMaterializerV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + const engine = safeCode(input.engine, 256); + if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) { + throw new TypeError("Invalid projection adapter input."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const witnessMaterialization = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + const external = input.materialize({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + query, + rulePack + }); + const externalCanonical = new Map; + for (const [relationName, tuples] of external.relationFacts) { + const relation = projectionName(relationName); + if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) { + throw new TypeError("Projection adapter returned an invalid relation."); + } + const parsed = tuples.map(tuple); + if (parsed.some((value) => value === null)) + throw new TypeError("Projection adapter returned an invalid tuple."); + const keys = []; + for (const value of parsed) { + if (value === null) + throw new TypeError("Projection adapter returned an invalid tuple."); + keys.push(tupleKey(value)); + } + externalCanonical.set(relation, [...new Set(keys)].sort()); + } + const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) => [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()])); + const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort(); + for (const relation of relationNames) { + if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) { + throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`); + } + } + return buildProjectionResult({ + dataset, + engine, + materialized: witnessMaterialization, + options, + query, + rulePack, + snapshot, + work + }); +} +function createOhProjectionRecordFactsV1(records, options = {}) { + if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + throw new RangeError("Too many records for projection facts."); + const parsedRecords = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((candidate) => { + const record = parseKnowledgeGraphRecordV1(candidate); + if (record === null) + throw new TypeError("Invalid graph record for projection facts."); + return record; + }); + let projectedFactCount = 0; + for (const record of parsedRecords) { + if (options.includeRecords !== false) + projectedFactCount += 1; + if (options.includeDependencies !== false) + projectedFactCount += record.dependencies.length; + if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("Structural projection exceeds its fact bound."); + } + } + const facts = []; + for (const record of parsedRecords) { + const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 }]; + if (options.includeRecords !== false) { + facts.push(createOhProjectionFactV1({ + relation: "oh.record", + sources: source, + tuple: [record.key, record.kind, record.recordSha256] + })); + } + if (options.includeDependencies !== false) { + for (const dependency of record.dependencies) { + facts.push(createOhProjectionFactV1({ + relation: "oh.dependency", + sources: source, + tuple: [record.key, dependency] + })); + } + } + } + return facts.sort(compareProjectionFacts); +} +function isOhProjectionRecordKindV1(value) { + return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value); +} + +// src/memory.ts +var OH_MEMORY_FORMAT_VERSION_V1 = 1; +var OH_MEMORY_CONFLICT_POLICY_V1 = "visible-conflicts.v1"; +var OH_MEMORY_LIMITS_V1 = Object.freeze({ + explainCapabilityEntryBytes: 32 * 1024 * 1024, + explainCapabilities: 256, + explainCapabilityLifetimeMs: 15 * 60 * 1000, + explainCapabilityTotalBytes: 64 * 1024 * 1024, + factsPerRecordPerExtractor: 512, + maximumExtractorInvocations: 262144, + maximumExtractors: 32, + maximumNominationRoutes: 64, + maximumPrograms: 128, + maximumRecordsPerLane: 8192, + maximumSyntheticRecords: 16384, + rememberBytes: 8 * 1024 * 1024, + resultBytes: 32 * 1024 * 1024, + snapshotBytesPerLane: 32 * 1024 * 1024, + relationsPerExtractor: 64 +}); +var memoryFactPackPayload = Object.freeze({ + factPackId: "oh.memory.composite-facts", + factPackRevision: 1, + relations: Object.freeze(["memory.agreement", "memory.conflict", "memory.dependency", "memory.record"]), + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 +}); +var OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1 = Object.freeze({ + ...memoryFactPackPayload, + extractorSha256: canonicalSha256(memoryFactPackPayload) +}); +var builtInFactPolicy = Object.freeze({ + extractorSha256: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.extractorSha256, + factPackId: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackId, + kind: "built-in", + v: 1 +}); +function immutableClone(value) { + if (Array.isArray(value)) { + return Object.freeze(value.map((item) => immutableClone(item))); + } + if (value !== null && typeof value === "object") { + if (!isPlainRecord(value)) + throw new TypeError("Memory output contains a non-JSON object."); + const cloned = {}; + for (const key of Object.keys(value)) { + Object.defineProperty(cloned, key, { + configurable: false, + enumerable: true, + value: immutableClone(value[key]), + writable: false + }); + } + return Object.freeze(cloned); + } + return value; +} +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} +function exactHead(left, right) { + return canonicalJson(left) === canonicalJson(right); +} +function authorityId(value) { + const parsed = safeCode(value, 128); + if (parsed === null) + throw new TypeError("Invalid memory authority ID."); + return parsed; +} +function bindingFor(store, expected, lane) { + const binding = parseOhStoreBindingV1(store.binding); + if (binding === null || binding.bindingSha256 !== parseSha256Hex(expected)) { + throw new OhIntegrityError(`The ${lane} store is not the host-bound authority.`); + } + if (binding.profile.profileKind !== lane) { + throw new OhProfileError(`The ${lane} memory lane has the wrong store profile.`); + } + return binding; +} +function laneIdentity(value) { + return Object.freeze({ + authorityId: value.authorityId, + bindingSha256: value.binding.bindingSha256, + datasetSha256: value.dataset.datasetSha256, + head: value.snapshot.head, + lane: value.lane, + snapshotSha256: value.projectionSnapshot.snapshotSha256, + v: 1 + }); +} +function datasetForSnapshot(binding, snapshot) { + const projectionSnapshot = createOhProjectionSnapshotV1({ + head: snapshot.head, + records: snapshot.records, + spaceId: binding.spaceId + }); + const dataset = createOhProjectionDatasetV1({ + extractorSha256: canonicalSha256({ extractor: "oh.memory.lane-structural", v: 1 }), + factPackId: "oh.memory.lane-structural", + factPackRevision: 1, + facts: createOhProjectionRecordFactsV1(snapshot.records), + snapshot: projectionSnapshot + }); + return { dataset, projectionSnapshot }; +} +async function readLane(authority, lane, expectedHead) { + const returnedHead = expectedHead ?? await authority.store.head(); + const head = parseOhHeadV1(immutableClone(returnedHead)); + if (head === null) + throw new OhIntegrityError(`The ${lane} store returned an invalid head.`); + const returnedSnapshot = await authority.store.snapshot({ + head: { operationSha256: head.operationSha256, sequence: head.sequence }, + maximumRecords: OH_MEMORY_LIMITS_V1.maximumRecordsPerLane + }); + if (!isPlainRecord(returnedSnapshot) || !hasExactKeys(returnedSnapshot, ["head", "records", "v"]) || returnedSnapshot.v !== 1 || !Array.isArray(returnedSnapshot.records)) { + throw new OhIntegrityError(`The ${lane} store returned an invalid snapshot envelope.`); + } + const detached = immutableClone(returnedSnapshot); + const detachedHead = parseOhHeadV1(detached.head); + if (detachedHead === null) + throw new OhIntegrityError(`The ${lane} store returned an invalid snapshot head.`); + const snapshot = immutableClone({ + head: detachedHead, + records: detached.records, + v: 1 + }); + if (!exactHead(snapshot.head, head)) { + throw new OhIntegrityError(`The ${lane} snapshot differs from its pinned head.`); + } + if (utf8ByteLength(canonicalJson(snapshot)) > OH_MEMORY_LIMITS_V1.snapshotBytesPerLane) { + throw new RangeError(`The ${lane} memory snapshot exceeds its canonical byte bound.`); + } + const projected = datasetForSnapshot(authority.binding, snapshot); + return Object.freeze({ + authorityId: authority.authorityId, + binding: authority.binding, + dataset: projected.dataset, + lane, + projectionSnapshot: projected.projectionSnapshot, + snapshot + }); +} +function syntheticKey(lane, recordSha256) { + return `memory-source:${lane}:${recordSha256}`; +} +function createSyntheticSources(lanes) { + const sources = new Map; + for (const lane of lanes) { + for (const physicalRecord of lane.snapshot.records) { + const key = syntheticKey(lane.lane, physicalRecord.recordSha256); + const record = createKnowledgeGraphRecordV1({ + dependencies: [], + key, + kind: "view", + v: 1, + value: { + authorityId: lane.authorityId, + bindingSha256: lane.binding.bindingSha256, + key: physicalRecord.key, + lane: lane.lane, + recordSha256: physicalRecord.recordSha256, + snapshotSha256: lane.projectionSnapshot.snapshotSha256, + v: 1 + } + }); + const physical = Object.freeze({ + authorityId: lane.authorityId, + bindingSha256: lane.binding.bindingSha256, + head: lane.snapshot.head, + key: physicalRecord.key, + lane: lane.lane, + recordSha256: physicalRecord.recordSha256, + snapshotSha256: lane.projectionSnapshot.snapshotSha256, + v: 1 + }); + if (sources.has(key)) + throw new OhIntegrityError("A memory lane contains a duplicate source digest."); + sources.set(key, Object.freeze({ physical, record })); + } + } + if (sources.size > OH_MEMORY_LIMITS_V1.maximumSyntheticRecords) { + throw new RangeError("The composite memory snapshot has too many records."); + } + const records = [...sources.values()].map(({ record }) => record).sort((left, right) => compareText(left.key, right.key)); + return { records, sources }; +} +function sourceFor(sources, lane, record) { + const source = sources.get(syntheticKey(lane, record.recordSha256)); + if (source === undefined) + throw new OhIntegrityError("A composite memory source is missing."); + return [{ key: source.record.key, recordSha256: source.record.recordSha256, v: 1 }]; +} +function createCompositeDataset(canonical, working, extractors) { + const synthetic = createSyntheticSources([canonical, working]); + const extractorInvocations = synthetic.records.length * extractors.length; + if (extractorInvocations > OH_MEMORY_LIMITS_V1.maximumExtractorInvocations) { + throw new RangeError("The composite memory extractor invocation count exceeds its explicit bound."); + } + const facts = []; + const factDigests = new Set; + const factPolicies = new Map; + const addFact = (fact, policy) => { + if (facts.length >= OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("The composite memory fact set exceeds its explicit bound."); + } + if (factDigests.has(fact.factSha256)) { + throw new OhIntegrityError("A memory fact extractor emitted the same exact fact twice."); + } + const priorPolicy = factPolicies.get(fact.relation); + if (priorPolicy !== undefined && canonicalJson(priorPolicy) !== canonicalJson(policy)) { + throw new OhIntegrityError("A memory relation has more than one fact policy."); + } + facts.push(fact); + factDigests.add(fact.factSha256); + factPolicies.set(fact.relation, policy); + }; + const byLane = new Map([ + ["canonical", new Map(canonical.snapshot.records.map((record) => [record.key, record]))], + ["working", new Map(working.snapshot.records.map((record) => [record.key, record]))] + ]); + for (const lane of [canonical, working]) { + for (const record of lane.snapshot.records) { + const extractorRecord = immutableClone(record); + const source = sourceFor(synthetic.sources, lane.lane, record); + addFact(createOhProjectionFactV1({ + relation: "memory.record", + sources: source, + tuple: [lane.lane, record.key, record.kind, record.recordSha256] + }), builtInFactPolicy); + for (const dependency of record.dependencies) { + addFact(createOhProjectionFactV1({ + relation: "memory.dependency", + sources: source, + tuple: [lane.lane, record.key, dependency] + }), builtInFactPolicy); + } + for (const extractor of extractors) { + const declared = extractor.extract(Object.freeze({ lane: lane.lane, record: extractorRecord })); + if (!Array.isArray(declared) || declared.length > OH_MEMORY_LIMITS_V1.factsPerRecordPerExtractor) { + throw new RangeError("A memory fact extractor exceeded its per-record bound."); + } + for (const fact of declared) { + if (!isPlainRecord(fact) || !hasExactKeys(fact, ["relation", "tuple", "v"]) || fact.v !== 1 || !Array.isArray(fact.tuple) || typeof fact.relation !== "string" || !extractor.relations.includes(fact.relation)) { + throw new TypeError("A memory fact extractor returned an invalid or reserved fact."); + } + addFact(createOhProjectionFactV1({ relation: fact.relation, sources: source, tuple: fact.tuple }), Object.freeze({ + extractorId: extractor.extractorId, + extractorSha256: extractor.extractorSha256, + kind: "domain", + v: 1 + })); + } + } + } + } + const conflicts = []; + const canonicalByKey = byLane.get("canonical"); + const workingByKey = byLane.get("working"); + for (const key of [...canonicalByKey.keys()].filter((candidate) => workingByKey.has(candidate)).sort()) { + const canonicalRecord = canonicalByKey.get(key); + const workingRecord = workingByKey.get(key); + const sources = [ + ...sourceFor(synthetic.sources, "canonical", canonicalRecord), + ...sourceFor(synthetic.sources, "working", workingRecord) + ]; + if (canonicalRecord.recordSha256 === workingRecord.recordSha256) { + addFact(createOhProjectionFactV1({ + relation: "memory.agreement", + sources, + tuple: [key, canonicalRecord.recordSha256] + }), builtInFactPolicy); + } else { + addFact(createOhProjectionFactV1({ + relation: "memory.conflict", + sources, + tuple: [key, canonicalRecord.recordSha256, workingRecord.recordSha256] + }), builtInFactPolicy); + conflicts.push(Object.freeze({ + canonicalRecordSha256: canonicalRecord.recordSha256, + key, + v: 1, + workingRecordSha256: workingRecord.recordSha256 + })); + } + } + const recordRefs = synthetic.records.map(knowledgeGraphRecordRefV1).sort((left, right) => compareText(left.key, right.key)); + const sourceIdentity = { + canonical: laneIdentity(canonical), + conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1, + recordRefs, + v: 1, + working: laneIdentity(working) + }; + const head = Object.freeze({ + generation: 1, + graphRevisionSha256: canonicalSha256({ kind: "oh.memory.composite-graph", sourceIdentity }), + operationSha256: canonicalSha256({ kind: "oh.memory.composite-operation", sourceIdentity }), + recordsSha256: canonicalSha256(recordRefs), + sequence: 1, + v: 1 + }); + const snapshot = createOhProjectionSnapshotV1({ + head, + records: synthetic.records, + spaceId: "oh.memory.composite" + }); + const dataset = createOhProjectionDatasetV1({ + extractorSha256: canonicalSha256({ + builtIn: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.extractorSha256, + extensions: extractors.map(({ extractorId, extractorSha256, relations }) => ({ + extractorId, + extractorSha256, + relations + })), + v: 1 + }), + factPackId: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackId, + factPackRevision: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackRevision, + facts, + snapshot + }); + return Object.freeze({ + conflicts: Object.freeze(conflicts), + dataset, + factPolicies, + snapshot, + sources: synthetic.sources + }); +} +function mapProof(proof, sources, factPolicies) { + if (proof.kind === "truncated") + return Object.freeze({ ...proof }); + if (proof.kind === "derived") { + return Object.freeze({ + ...proof, + premises: Object.freeze(proof.premises.map((premise) => mapProof(premise, sources, factPolicies))) + }); + } + const physical = proof.sources.map((source) => { + const mapped = sources.get(source.key); + if (mapped === undefined || mapped.record.recordSha256 !== source.recordSha256) { + throw new OhIntegrityError("A projection proof has no exact physical memory source."); + } + return mapped.physical; + }).sort((left, right) => compareText(canonicalJson(left), canonicalJson(right))); + const factPolicy = factPolicies.get(proof.relation); + if (factPolicy === undefined) + throw new OhIntegrityError("A projection proof has no memory fact policy."); + return Object.freeze({ + factPolicy, + kind: "fact", + relation: proof.relation, + sources: Object.freeze(physical), + tuple: proof.tuple, + v: 1 + }); +} +function collectLanes(proof, lanes) { + if (proof.kind === "truncated") + return true; + if (proof.kind === "fact") { + for (const source of proof.sources) + lanes.add(source.lane); + return false; + } + let unknown = proof.premisesTruncated; + for (const premise of proof.premises) + unknown = collectLanes(premise, lanes) || unknown; + return unknown; +} +function publicRow(row, proofs) { + const lanes = new Set; + let unknown = row.proofsTruncated; + for (const proof of proofs) + unknown = collectLanes(proof, lanes) || unknown; + const premiseLanes = [...lanes].sort(); + const premiseAuthority = unknown || premiseLanes.length === 0 ? "unknown" : premiseLanes.includes("working") ? "working" : "canonical"; + const payload = { + premiseAuthority, + premiseLanes, + proofsTruncated: row.proofsTruncated, + supportCount: row.supportCount, + v: 1, + values: row.values + }; + return Object.freeze({ ...payload, resultRowSha256: canonicalSha256(payload) }); +} +function resolvePrograms(programs) { + if (programs.length < 1 || programs.length > OH_MEMORY_LIMITS_V1.maximumPrograms) { + throw new RangeError("Memory requires a bounded nonempty named program registry."); + } + const resolved = new Map; + for (const program of programs) { + const programId = safeCode(program.programId, 128); + const purpose = safeCode(program.purpose, 256); + const query = parseOhProjectionQueryV1(program.query); + const rulePack = parseOhProjectionRulePackV1(program.rulePack); + if (programId === null || purpose === null || query === null || rulePack === null || resolved.has(programId)) { + throw new TypeError("Invalid or duplicate named memory program."); + } + resolved.set(programId, immutableClone({ ...program.evaluation === undefined ? {} : { evaluation: { ...program.evaluation } }, programId, purpose, query, rulePack })); + } + return resolved; +} +function resolveExtractors(extractors) { + if (extractors.length > OH_MEMORY_LIMITS_V1.maximumExtractors) { + throw new RangeError("The memory domain extractor registry is too large."); + } + const claimedRelations = new Set; + const resolved = extractors.map((extractor) => { + const extractorId = safeCode(extractor.extractorId, 128); + const extractorSha256 = parseSha256Hex(extractor.extractorSha256); + if (extractorId === null || extractorSha256 === null || typeof extractor.extract !== "function" || !Array.isArray(extractor.relations) || extractor.relations.length < 1 || extractor.relations.length > OH_MEMORY_LIMITS_V1.relationsPerExtractor) { + throw new TypeError("Invalid memory domain fact extractor."); + } + const relations = extractor.relations.map((relation) => safeCode(relation, 128)).sort(); + if (relations.some((relation) => relation === null || relation.startsWith("memory.") || relation.startsWith("oh.")) || new Set(relations).size !== relations.length) { + throw new TypeError("A memory domain fact extractor has invalid or reserved relations."); + } + for (const relation of relations) { + if (claimedRelations.has(relation)) { + throw new TypeError("Memory domain fact extractor relations must have one owner."); + } + claimedRelations.add(relation); + } + return Object.freeze({ + extract: extractor.extract, + extractorId, + extractorSha256, + relations: Object.freeze(relations) + }); + }).sort((left, right) => compareText(left.extractorId, right.extractorId)); + if (new Set(resolved.map(({ extractorId }) => extractorId)).size !== resolved.length) { + throw new TypeError("Duplicate memory domain fact extractor ID."); + } + return Object.freeze(resolved); +} +function resolveNominationRoutes(routes) { + if (routes.length > OH_MEMORY_LIMITS_V1.maximumNominationRoutes) { + throw new RangeError("The memory nomination route registry is too large."); + } + const resolved = new Map; + for (const route of routes) { + const nominationId = safeCode(route.nominationId, 128); + const destinationPurpose = safeCode(route.destinationPurpose, 256); + if (nominationId === null || destinationPurpose === null || resolved.has(nominationId)) { + throw new TypeError("Invalid or duplicate memory nomination route."); + } + resolved.set(nominationId, Object.freeze({ destinationPurpose, nominationId })); + } + return resolved; +} +function parseQueryRequest(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["programId", "v"]) || value.v !== 1) + throw new TypeError("Invalid named memory query."); + const programId = safeCode(value.programId, 128); + if (programId === null) + throw new TypeError("Invalid named memory query identity."); + return { programId }; +} +function parseExplainRequest(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["resultSha256", "row", "token", "v"]) || value.v !== 1 || typeof value.token !== "string" || value.token.length !== 43 || !Number.isSafeInteger(value.row) || value.row < 0) { + throw new TypeError("Invalid memory explanation request."); + } + const resultSha256 = parseSha256Hex(value.resultSha256); + if (resultSha256 === null) + throw new TypeError("Invalid memory explanation result identity."); + return { resultSha256, row: value.row, token: value.token }; +} +function parseNominationRequest(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["nominationId", "roots", "v"]) || value.v !== 1 || !Array.isArray(value.roots) || value.roots.length < 1 || value.roots.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new TypeError("Invalid memory nomination request."); + } + const nominationId = safeCode(value.nominationId, 128); + const roots = value.roots.map((root) => safeCode(root, 512)).sort(); + if (nominationId === null || roots.some((root) => root === null) || new Set(roots).size !== roots.length) + throw new TypeError("Invalid memory nomination identity."); + return { nominationId, roots }; +} +function isoInstant(date) { + const value = date.toISOString(); + if (parseCanonicalInstantV1(value) === null) + throw new TypeError("The memory clock returned an invalid instant."); + return value; +} +function clockMilliseconds(now) { + const milliseconds = now().getTime(); + if (!Number.isFinite(milliseconds)) + throw new TypeError("The memory clock returned an invalid date."); + return milliseconds; +} +function monotonicMilliseconds(now) { + const milliseconds = now(); + if (!Number.isFinite(milliseconds) || milliseconds < 0) { + throw new TypeError("The memory monotonic clock returned an invalid value."); + } + return milliseconds; +} +async function createOhMemoryAgentV1(options) { + const memoryActorId = safeCode(options.actorId, 128); + if (memoryActorId === null) + throw new TypeError("Invalid host-bound memory actor ID."); + const canonicalStore = options.canonical.store; + const workingStore = options.working.store; + const workingCodecs = options.working.codecs; + const canonicalAuthorityId = authorityId(options.canonical.authorityId); + const workingAuthorityId = authorityId(options.working.authorityId); + if (canonicalAuthorityId === workingAuthorityId) { + throw new OhProfileError("Working and canonical memory must be distinct physical authorities."); + } + const canonicalBinding = bindingFor(canonicalStore, options.canonical.expectedBindingSha256, "canonical"); + const workingBinding = bindingFor(workingStore, options.working.expectedBindingSha256, "working"); + const expectedCanonicalHead = parseOhHeadV1(options.canonical.expectedHead); + if (expectedCanonicalHead === null) + throw new TypeError("Invalid pinned canonical memory head."); + const programs = resolvePrograms(options.programs); + const extractors = resolveExtractors(options.extractors ?? []); + const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []); + const ingress = new OhSemanticBundleIngressV1(workingStore, workingCodecs); + const now = options.now ?? (() => new Date); + const monotonicNow = options.monotonicNow ?? (() => performance.now()); + const capabilityLifetime = options.explainCapabilityLifetimeMs ?? OH_MEMORY_LIMITS_V1.explainCapabilityLifetimeMs; + if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1000 || capabilityLifetime > 60 * 60 * 1000) { + throw new RangeError("Invalid memory explanation capability lifetime."); + } + const canonical = await readLane({ + authorityId: canonicalAuthorityId, + binding: canonicalBinding, + store: canonicalStore + }, "canonical", expectedCanonicalHead); + const explanations = new Map; + let explanationBytes = 0; + let lastMonotonicMs = -1; + let lastWallClockMs = Number.NEGATIVE_INFINITY; + const wallClock = () => { + const milliseconds = clockMilliseconds(now); + if (milliseconds < lastWallClockMs) + throw new OhProfileError("The memory wall clock regressed."); + lastWallClockMs = milliseconds; + return milliseconds; + }; + const monotonicClock = () => { + const milliseconds = monotonicMilliseconds(monotonicNow); + if (milliseconds < lastMonotonicMs) + throw new OhProfileError("The memory monotonic clock regressed."); + lastMonotonicMs = milliseconds; + return milliseconds; + }; + const deleteExplanation = (token) => { + const stored = explanations.get(token); + if (stored !== undefined && explanations.delete(token)) + explanationBytes -= stored.bytes; + }; + const remember = async (value) => { + if (utf8ByteLength(canonicalJson(value)) > OH_MEMORY_LIMITS_V1.rememberBytes) { + throw new RangeError("The memory semantic bundle exceeds its canonical byte bound."); + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) { + throw new TypeError("Invalid memory remember request."); + } + const requestId = safeCode(value.requestId, 128); + if (requestId === null) + throw new TypeError("Invalid memory remember request identity."); + const operationId = `memory_${canonicalSha256({ + actorId: memoryActorId, + bindingSha256: workingBinding.bindingSha256, + requestId, + v: 1 + }).slice(0, 48)}`; + const operation = await ingress.commit({ + actorId: memoryActorId, + expectedHead: value.expectedHead, + instant: isoInstant(new Date(wallClock())), + operationId, + puts: value.puts, + tombstones: value.tombstones, + v: 1 + }); + const head = { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + const payload = { + actorId: operation.actorId, + authorityId: workingAuthorityId, + bindingSha256: workingBinding.bindingSha256, + head, + instant: operation.instant, + lane: "working", + operationSha256: operation.operationSha256, + requestId, + status: "committed", + v: 1 + }; + return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) }); + }; + const query = async (value) => { + const request = parseQueryRequest(value); + const program = programs.get(request.programId); + if (program === undefined) + throw new TypeError("Unknown named memory program."); + const working = await readLane({ + authorityId: workingAuthorityId, + binding: workingBinding, + store: workingStore + }, "working"); + const composite = createCompositeDataset(canonical, working, extractors); + const projection = evaluateOhProjectionV1({ + dataset: composite.dataset, + ...program.evaluation === undefined ? {} : { options: program.evaluation }, + query: program.query, + rulePack: program.rulePack, + snapshot: composite.snapshot + }); + const identityPayload = { + canonical: laneIdentity(canonical), + compositeDatasetSha256: composite.dataset.datasetSha256, + conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1, + evaluationSha256: projection.identity.evaluationSha256, + programId: program.programId, + projectionSha256: projection.identity.projectionSha256, + purpose: program.purpose, + querySha256: program.query.querySha256, + rulePackSha256: program.rulePack.rulePackSha256, + v: 1, + working: laneIdentity(working) + }; + const identity = immutableClone({ + ...identityPayload, + memorySha256: canonicalSha256(identityPayload) + }); + const proofs = immutableClone(projection.rows.map((row) => row.proofs.map((proof) => mapProof(proof, composite.sources, composite.factPolicies)))); + const rows = immutableClone(projection.rows.map((row, index) => publicRow(row, proofs[index]))); + const resultPayload = immutableClone({ + authority: "derived", + conflicts: composite.conflicts, + identity, + projectionResultSha256: projection.resultSha256, + rows, + v: 1 + }); + const resultSha256 = canonicalSha256(resultPayload); + const issuedAt = wallClock(); + const issuedAtMonotonic = monotonicClock(); + const expiresAtMs = issuedAt + capabilityLifetime; + const expiresAtMonotonicMs = issuedAtMonotonic + capabilityLifetime; + const expiresAt = isoInstant(new Date(expiresAtMs)); + for (const [existingToken, stored] of explanations) { + if (issuedAtMonotonic >= stored.expiresAtMonotonicMs) + deleteExplanation(existingToken); + } + const storedPayload = immutableClone({ expiresAtMonotonicMs, identity, proofs, resultSha256, rows }); + const storedBytes = utf8ByteLength(canonicalJson(storedPayload)) + 128; + if (storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityEntryBytes || storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) { + throw new RangeError("The memory explanation exceeds its retained capability bound."); + } + while (explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities || explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) { + const oldest = explanations.keys().next().value; + if (oldest === undefined) + break; + deleteExplanation(oldest); + } + let token = randomBytes2(32).toString("base64url"); + while (explanations.has(token)) + token = randomBytes2(32).toString("base64url"); + explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes })); + explanationBytes += storedBytes; + const result = immutableClone({ + ...resultPayload, + explainCapability: { expiresAt, token, v: 1 }, + resultSha256 + }); + if (utf8ByteLength(canonicalJson(result)) > OH_MEMORY_LIMITS_V1.resultBytes) { + deleteExplanation(token); + throw new RangeError("The composite memory result exceeds its canonical byte bound."); + } + return result; + }; + const explain = async (value) => { + const request = parseExplainRequest(value); + const stored = explanations.get(request.token); + const currentTime = monotonicClock(); + if (stored === undefined || stored.resultSha256 !== request.resultSha256 || currentTime >= stored.expiresAtMonotonicMs) { + deleteExplanation(request.token); + throw new OhProfileError("The memory explanation capability is absent, expired, or misbound."); + } + const row = stored.rows[request.row]; + const proofs = stored.proofs[request.row]; + if (row === undefined || proofs === undefined) + throw new RangeError("The explanation row is out of bounds."); + const payload = { + authority: "derived", + identity: stored.identity, + premiseAuthority: row.premiseAuthority, + premiseLanes: row.premiseLanes, + proofs, + proofsTruncated: row.proofsTruncated, + resultRowSha256: row.resultRowSha256, + resultSha256: stored.resultSha256, + supportCount: row.supportCount, + v: 1, + values: row.values + }; + return immutableClone({ ...payload, explanationSha256: canonicalSha256(payload) }); + }; + const nominate = async (value) => { + const request = parseNominationRequest(value); + const route = nominationRoutes.get(request.nominationId); + if (route === undefined) + throw new TypeError("Unknown named memory nomination route."); + const head = parseOhHeadV1(immutableClone(await workingStore.head())); + if (head === null) + throw new OhIntegrityError("The working nomination store returned an invalid head."); + const closure = await workingStore.exportDependencyClosure({ head: { + operationSha256: head.operationSha256, + sequence: head.sequence + }, roots: request.roots }); + const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head }); + if (!verified.ok) + throw new OhIntegrityError("The working nomination closure failed exact verification."); + if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) { + throw new OhIntegrityError("The working nomination closure substituted different roots."); + } + const source = Object.freeze({ + authorityId: workingAuthorityId, + bindingSha256: workingBinding.bindingSha256, + head, + lane: "working", + v: 1 + }); + const payload = { + closure: verified.closure, + destinationPurpose: route.destinationPurpose, + nominationId: route.nominationId, + source, + status: "prepared", + v: 1 + }; + return immutableClone({ ...payload, nominationSha256: canonicalSha256(payload) }); + }; + return Object.freeze({ explain, nominate, query, remember }); +} +export { + createOhMemoryAgentV1, + OH_MEMORY_LIMITS_V1, + OH_MEMORY_FORMAT_VERSION_V1, + OH_MEMORY_CONFLICT_POLICY_V1, + OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1 +}; diff --git a/dist/projection-public.d.ts b/dist/projection-public.d.ts new file mode 100644 index 0000000..b05bf4d --- /dev/null +++ b/dist/projection-public.d.ts @@ -0,0 +1,59 @@ +import * as Projection from "./projection"; +export declare const OH_PROJECTION_FORMAT_VERSION_V1: 1; +export declare const OH_PROJECTION_INTERNAL_ENGINE_V1: "oh.naive.positive.v1"; +export declare const OH_PROJECTION_LIMITS_V1: Readonly<{ + arity: 32; + atomBytes: number; + derivedTuples: 262144; + facts: 262144; + literalsPerRule: 64; + proofDepth: 128; + proofNodes: 4096; + queryLiterals: 64; + queryMatches: 262144; + queryResults: 65536; + relations: 4096; + resultBytes: number; + rounds: 1024; + rules: 1024; + sourcesPerFact: 64; + totalProofNodes: 65536; + variables: 256; + workUnits: 16777216; +}>; +export declare const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1: Readonly<{ + extractorSha256: import("./canonical").Sha256Hex; + factPackId: "oh.record-facts"; + factPackRevision: 1; + relations: readonly ["oh.dependency", "oh.record"]; + semantics: "oh.projection.positive-datalog.v1"; + v: 1; +}>; +export declare const OH_PROJECTION_SEMANTICS_V1: "oh.projection.positive-datalog.v1"; +export declare const createOhProjectionDatasetV1: typeof Projection.createOhProjectionDatasetV1; +export declare const createOhProjectionFactV1: typeof Projection.createOhProjectionFactV1; +export declare const createOhProjectionIdentityV1: typeof Projection.createOhProjectionIdentityV1; +export declare const createOhProjectionLiteralV1: typeof Projection.createOhProjectionLiteralV1; +export declare const createOhProjectionQueryV1: typeof Projection.createOhProjectionQueryV1; +export declare const createOhProjectionRecordFactsV1: typeof Projection.createOhProjectionRecordFactsV1; +export declare const createOhProjectionRulePackV1: typeof Projection.createOhProjectionRulePackV1; +export declare const createOhProjectionRuleV1: typeof Projection.createOhProjectionRuleV1; +export declare const createOhProjectionSnapshotV1: typeof Projection.createOhProjectionSnapshotV1; +export declare const evaluateOhProjectionV1: typeof Projection.evaluateOhProjectionV1; +export declare const invalidationForOhProjectionV1: typeof Projection.invalidationForOhProjectionV1; +export declare const isOhProjectionRecordKindV1: typeof Projection.isOhProjectionRecordKindV1; +export declare const ohProjectionConstantV1: typeof Projection.ohProjectionConstantV1; +export declare const ohProjectionVariableV1: typeof Projection.ohProjectionVariableV1; +export declare const parseOhProjectionDatasetV1: typeof Projection.parseOhProjectionDatasetV1; +export declare const parseOhProjectionFactV1: typeof Projection.parseOhProjectionFactV1; +export declare const parseOhProjectionIdentityV1: typeof Projection.parseOhProjectionIdentityV1; +export declare const parseOhProjectionLiteralV1: typeof Projection.parseOhProjectionLiteralV1; +export declare const parseOhProjectionProofV1: typeof Projection.parseOhProjectionProofV1; +export declare const parseOhProjectionQueryV1: typeof Projection.parseOhProjectionQueryV1; +export declare const parseOhProjectionResultV1: typeof Projection.parseOhProjectionResultV1; +export declare const parseOhProjectionRulePackV1: typeof Projection.parseOhProjectionRulePackV1; +export declare const parseOhProjectionRuleV1: typeof Projection.parseOhProjectionRuleV1; +export declare const parseOhProjectionSnapshotV1: typeof Projection.parseOhProjectionSnapshotV1; +export declare const parseOhProjectionTermV1: typeof Projection.parseOhProjectionTermV1; +export type { OhProjectionAtomV1, OhProjectionDatasetV1, OhProjectionEvaluationOptionsV1, OhProjectionFactSourceV1, OhProjectionFactV1, OhProjectionIdentityV1, OhProjectionInvalidationReasonV1, OhProjectionInvalidationV1, OhProjectionLiteralV1, OhProjectionProofV1, OhProjectionQueryV1, OhProjectionRecordFactOptionsV1, OhProjectionResultRowV1, OhProjectionResultV1, OhProjectionRulePackV1, OhProjectionRuleV1, OhProjectionSnapshotV1, OhProjectionTermV1, } from "./projection"; +//# sourceMappingURL=projection-public.d.ts.map \ No newline at end of file diff --git a/dist/projection-public.d.ts.map b/dist/projection-public.d.ts.map new file mode 100644 index 0000000..24309c2 --- /dev/null +++ b/dist/projection-public.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"projection-public.d.ts","sourceRoot":"","sources":["../src/projection-public.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAE3C,eAAO,MAAM,+BAA+B,GAA6C,CAAC;AAC1F,eAAO,MAAM,gCAAgC,wBAA8C,CAAC;AAC5F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;EAAqC,CAAC;AAC1E,eAAO,MAAM,sCAAsC;;;;;;;EAAoD,CAAC;AACxG,eAAO,MAAM,0BAA0B,qCAAwC,CAAC;AAChF,eAAO,MAAM,2BAA2B,+CAAyC,CAAC;AAClF,eAAO,MAAM,wBAAwB,4CAAsC,CAAC;AAC5E,eAAO,MAAM,4BAA4B,gDAA0C,CAAC;AACpF,eAAO,MAAM,2BAA2B,+CAAyC,CAAC;AAClF,eAAO,MAAM,yBAAyB,6CAAuC,CAAC;AAC9E,eAAO,MAAM,+BAA+B,mDAA6C,CAAC;AAC1F,eAAO,MAAM,4BAA4B,gDAA0C,CAAC;AACpF,eAAO,MAAM,wBAAwB,4CAAsC,CAAC;AAC5E,eAAO,MAAM,4BAA4B,gDAA0C,CAAC;AACpF,eAAO,MAAM,sBAAsB,0CAAoC,CAAC;AACxE,eAAO,MAAM,6BAA6B,iDAA2C,CAAC;AACtF,eAAO,MAAM,0BAA0B,8CAAwC,CAAC;AAChF,eAAO,MAAM,sBAAsB,0CAAoC,CAAC;AACxE,eAAO,MAAM,sBAAsB,0CAAoC,CAAC;AACxE,eAAO,MAAM,0BAA0B,8CAAwC,CAAC;AAChF,eAAO,MAAM,uBAAuB,2CAAqC,CAAC;AAC1E,eAAO,MAAM,2BAA2B,+CAAyC,CAAC;AAClF,eAAO,MAAM,0BAA0B,8CAAwC,CAAC;AAChF,eAAO,MAAM,wBAAwB,4CAAsC,CAAC;AAC5E,eAAO,MAAM,wBAAwB,4CAAsC,CAAC;AAC5E,eAAO,MAAM,yBAAyB,6CAAuC,CAAC;AAC9E,eAAO,MAAM,2BAA2B,+CAAyC,CAAC;AAClF,eAAO,MAAM,uBAAuB,2CAAqC,CAAC;AAC1E,eAAO,MAAM,2BAA2B,+CAAyC,CAAC;AAClF,eAAO,MAAM,uBAAuB,2CAAqC,CAAC;AAE1E,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,+BAA+B,EAC/B,wBAAwB,EACxB,kBAAkB,EAClB,sBAAsB,EACtB,gCAAgC,EAChC,0BAA0B,EAC1B,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,+BAA+B,EAC/B,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,cAAc,CAAC"} \ No newline at end of file diff --git a/dist/projection-public.js b/dist/projection-public.js new file mode 100644 index 0000000..e97265e --- /dev/null +++ b/dist/projection-public.js @@ -0,0 +1,1682 @@ +// src/canonical.ts +import { createHash, randomBytes } from "node:crypto"; + +class OhValidationError extends Error { + code; + path; + constructor(code, path, message) { + super(`${path}: ${message}`); + this.name = "OhValidationError"; + this.code = code; + this.path = path; + } +} +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} +function assertUnicodeScalarString(value, path) { + for (let index = 0;index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(index + 1); + if (!(next >= 56320 && next <= 57343)) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + index += 1; + } else if (code >= 56320 && code <= 57343) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + } +} +function encodeCanonical(value, path, ancestors) { + if (value === null || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "string") { + assertUnicodeScalarString(value, path); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new OhValidationError("non-json-number", path, "must be finite"); + } + if (Object.is(value, -0)) { + throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical"); + } + return JSON.stringify(value); + } + if (typeof value !== "object" || value === null) { + throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`); + } + if (ancestors.has(value)) { + throw new OhValidationError("cycle", path, "contains a cycle"); + } + ancestors.add(value); + try { + if (Array.isArray(value)) { + const encoded = []; + for (let index = 0;index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes"); + } + encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors)); + } + const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)); + if (extraKeys.length > 0) { + throw new OhValidationError("non-json-property", path, "array has non-index properties"); + } + return `[${encoded.join(",")}]`; + } + if (!isPlainRecord(value)) { + throw new OhValidationError("non-plain-object", path, "must be a plain object"); + } + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== "string")) { + throw new OhValidationError("non-json-property", path, "object has a symbol property"); + } + const keys = ownKeys; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) { + throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property"); + } + } + keys.sort(); + const entries = keys.map((key) => { + assertUnicodeScalarString(key, `${path}.`); + return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`; + }); + return `{${entries.join(",")}}`; + } finally { + ancestors.delete(value); + } +} +function canonicalJson(value) { + return encodeCanonical(value, "$", new Set); +} +function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) { + if (utf8ByteLength(text) > maximumBytes) { + throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new OhValidationError("invalid-json", "$", "is not valid JSON"); + } + if (canonicalJson(value) !== text) { + throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical"); + } + return value; +} +function utf8ByteLength(value) { + return Buffer.byteLength(value, "utf8"); +} +function sha256Hex(value) { + return createHash("sha256").update(value).digest("hex"); +} +function canonicalSha256(value) { + return sha256Hex(canonicalJson(value)); +} +function parseSha256Hex(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null; +} +function parseCanonicalInstantV1(value) { + if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) { + return null; + } + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null; +} +function canonicalNow() { + return new Date().toISOString(); +} +function safeCode(value, maximumLength = 128) { + return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function orderedUnique(values, key) { + return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)); +} +function sortUnique(values, key) { + const sorted = [...values].sort((left, right) => { + const leftKey = key(left); + const rightKey = key(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); + if (!orderedUnique(sorted, key)) { + throw new OhValidationError("duplicate", "$", "contains duplicate canonical values"); + } + return sorted; +} + +// src/graph.ts +var OH_GRAPH_FORMAT_VERSION_V1 = 1; +var OH_GRAPH_LIMITS_V1 = Object.freeze({ + changesPerOperation: 8192, + dependenciesPerRecord: 4096, + recordBytes: 1024 * 1024, + recordsPerSnapshot: 65536 +}); +var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [ + "activity", + "assertion", + "context", + "dependency-manifest", + "edition", + "entity", + "evidence", + "identity-operation", + "inquiry", + "inquiry-event", + "review-decision", + "rights-decision", + "schema", + "shape", + "statement", + "type-membership", + "view", + "vocabulary" +]; +function recordKey(value) { + return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function createKnowledgeGraphRecordV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies)) + throw new TypeError("Invalid graph record input."); + const key = recordKey(input.key); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind); + if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) + throw new TypeError("Invalid graph record identity."); + const dependencies = input.dependencies.map(recordKey); + if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) { + throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive."); + } + const valueJson = canonicalJson(input.value); + if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) { + throw new RangeError("Graph record value exceeds its canonical byte limit."); + } + const payload = { dependencies, key, kind, v: 1, value: input.value }; + return { ...payload, recordSha256: canonicalSha256(payload) }; +} +function parseKnowledgeGraphRecordV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256")) + return null; + const recordSha256 = parseSha256Hex(value.recordSha256); + const { recordSha256: _digest, ...input } = value; + try { + const created = createKnowledgeGraphRecordV1(input); + return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null; + } catch { + return null; + } +} +function knowledgeGraphRecordRefV1(record) { + return { + dependencies: record.dependencies, + key: record.key, + kind: record.kind, + sha256: record.recordSha256, + v: 1 + }; +} +function changeKey(change) { + return change.kind === "put" ? change.record.key : change.key; +} +function canonicalKnowledgeGraphChangesV1(changes) { + const normalized = []; + for (const change of changes) { + if (!isPlainRecord(change) || change.v !== 1) + throw new TypeError("Invalid graph change."); + if (change.kind === "put") { + const record = parseKnowledgeGraphRecordV1(change.record); + if (record === null) + throw new TypeError("Invalid graph record in change."); + normalized.push({ kind: "put", record, v: 1 }); + } else if (change.kind === "tombstone") { + const key = recordKey(change.key); + const priorSha256 = parseSha256Hex(change.priorSha256); + if (key === null || priorSha256 === null) + throw new TypeError("Invalid graph tombstone."); + normalized.push({ key, kind: "tombstone", priorSha256, v: 1 }); + } else + throw new TypeError("Unknown graph change kind."); + } + return sortUnique(normalized, changeKey); +} +function graphRevisionSha256V1(input) { + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + const operationId = safeCode(input.operationId); + const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256); + const recordsSha256 = parseSha256Hex(input.recordsSha256); + const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null; + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) { + throw new TypeError("Invalid graph revision digest input."); + } + return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 }); +} + +// src/ontology.ts +var OH_ONTOLOGY_VERSION_V1 = "1.0.0"; +var OH_CONTRACT_ID_V1 = "oh.ontology.v1"; +var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({ + dimensions: 64, + listValues: 256, + qualifiers: 128, + statementBytes: 256 * 1024, + textBytes: 64 * 1024 +}); + +// src/schema.ts +var OH_SCHEMA_FORMAT_VERSION_V1 = 1; + +// src/contract.ts +var manifestPayload = Object.freeze({ + contractId: OH_CONTRACT_ID_V1, + graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1, + ontologyVersion: OH_ONTOLOGY_VERSION_V1, + recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1, + v: 1 +}); +var OH_CONTRACT_MANIFEST_V1 = Object.freeze({ + ...manifestPayload, + contractSha256: canonicalSha256(manifestPayload) +}); +class OhRecordCodecRegistry { + #codecs = new Map; + #sealed = false; + register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); + if (this.#codecs.has(codec.kind)) + throw new TypeError(`A codec is already registered for ${codec.kind}.`); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); + return this; + } + parse(kind, value) { + const codec = this.#codecs.get(kind); + if (codec !== undefined) + return codec.parse(value); + try { + canonicalJson(value); + return value; + } catch { + return null; + } + } + has(kind) { + return this.#codecs.has(kind); + } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } +} + +// src/projection.ts +var OH_PROJECTION_FORMAT_VERSION_V1 = 1; +var OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1"; +var OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1"; +var OH_PROJECTION_LIMITS_V1 = Object.freeze({ + arity: 32, + atomBytes: 16 * 1024, + derivedTuples: 262144, + facts: 262144, + literalsPerRule: 64, + proofDepth: 128, + proofNodes: 4096, + queryLiterals: 64, + queryMatches: 262144, + queryResults: 65536, + relations: 4096, + resultBytes: 16 * 1024 * 1024, + rounds: 1024, + rules: 1024, + sourcesPerFact: 64, + totalProofNodes: 65536, + variables: 256, + workUnits: 16777216 +}); +var recordFactExtractorPayloadV1 = { + factPackId: "oh.record-facts", + factPackRevision: 1, + relations: ["oh.dependency", "oh.record"], + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 +}; +var OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({ + ...recordFactExtractorPayloadV1, + extractorSha256: canonicalSha256(recordFactExtractorPayloadV1) +}); +function nonnegativeInteger(value) { + return Number.isSafeInteger(value) && value >= 0 ? value : null; +} +function positiveInteger(value, maximum = Number.MAX_SAFE_INTEGER) { + return Number.isSafeInteger(value) && value >= 1 && value <= maximum ? value : null; +} +function projectionName(value, maximumLength = 128) { + return safeCode(value, maximumLength); +} +function compareCanonical(left, right) { + const leftKey = canonicalJson(left); + const rightKey = canonicalJson(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; +} +function compareProjectionFacts(left, right) { + return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]); +} +var INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom"); +function atom(value) { + if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") { + return INVALID_PROJECTION_ATOM; + } + try { + const encoded = canonicalJson(value); + return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes ? value : INVALID_PROJECTION_ATOM; + } catch { + return INVALID_PROJECTION_ATOM; + } +} +function tuple(value) { + if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity) + return null; + const parsed = value.map(atom); + return parsed.some((item) => item === INVALID_PROJECTION_ATOM) ? null : parsed; +} +function parseRecordRef(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"]) || value.v !== 1 || !Array.isArray(value.dependencies)) + return null; + const key = safeCode(value.key, 512); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind); + const sha256 = parseSha256Hex(value.sha256); + const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512)); + if (key === null || kind === undefined || sha256 === null || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord || dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) + return null; + return { dependencies, key, kind, sha256, v: 1 }; +} +function createOhProjectionSnapshotV1(input) { + const spaceId = projectionName(input.spaceId); + const generation = nonnegativeInteger(input.head.generation); + const sequence = nonnegativeInteger(input.head.sequence); + const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256); + const graphRevisionSha256 = input.head.graphRevisionSha256 === null ? null : parseSha256Hex(input.head.graphRevisionSha256); + const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256); + if (spaceId === null || generation === null || sequence === null || generation !== sequence || input.head.operationSha256 !== null && operationSha256 === null || input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid projection snapshot head."); + } + const records = input.records.map(parseKnowledgeGraphRecordV1); + if (records.some((record) => record === null)) + throw new TypeError("Invalid record in projection snapshot."); + const recordRefs = sortUnique(records.map(knowledgeGraphRecordRefV1), (reference) => reference.key); + const recordsSha256 = canonicalSha256(recordRefs); + if (recordsSha256 !== declaredRecordsSha256) { + throw new TypeError("Projection snapshot records do not reproduce the declared head."); + } + const keys = new Set(recordRefs.map((reference) => reference.key)); + if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) { + throw new TypeError("Projection snapshot has a missing record dependency."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return { ...payload, snapshotSha256: canonicalSha256(payload) }; +} +function parseOhProjectionSnapshotV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "generation", + "graphRevisionSha256", + "operationSha256", + "recordRefs", + "recordsSha256", + "sequence", + "snapshotSha256", + "spaceId", + "v" + ]) || value.v !== 1 || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const generation = nonnegativeInteger(value.generation); + const sequence = nonnegativeInteger(value.sequence); + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + const spaceId = projectionName(value.spaceId); + const recordRefs = value.recordRefs.map(parseRecordRef); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null || value.graphRevisionSha256 !== null && graphRevisionSha256 === null || value.operationSha256 !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || recordRefs.some((reference) => reference === null)) + return null; + const refs = recordRefs; + if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256) + return null; + const keys = new Set(refs.map((reference) => reference.key)); + if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) + return null; + const payload = { + contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs: refs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null; +} +function createOhProjectionFactV1(input) { + const relation = projectionName(input.relation); + const parsedTuple = tuple(input.tuple); + if (relation === null || parsedTuple === null || input.sources.length < 1 || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) { + throw new TypeError("Invalid projection fact."); + } + const sources = input.sources.map((source) => { + if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) { + throw new TypeError("Invalid projection fact source."); + } + const key = safeCode(source.key, 512); + const recordSha256 = parseSha256Hex(source.recordSha256); + if (key === null || recordSha256 === null) + throw new TypeError("Invalid projection fact source."); + return { key, recordSha256, v: 1 }; + }).sort(compareCanonical); + if (!orderedUnique(sources, (source) => source.key)) { + throw new TypeError("Projection fact sources must have unique record keys."); + } + const payload = { relation, sources, tuple: parsedTuple, v: 1 }; + return { ...payload, factSha256: canonicalSha256(payload) }; +} +function parseOhProjectionFactV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"]) || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple)) + return null; + const factSha256 = parseSha256Hex(value.factSha256); + try { + const fact = createOhProjectionFactV1({ + relation: value.relation, + sources: value.sources, + tuple: value.tuple + }); + return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null; + } catch { + return null; + } +} +function mergeProjectionFacts(facts) { + const grouped = new Map; + for (const fact of facts) { + const identity = canonicalJson([fact.relation, fact.tuple]); + let group = grouped.get(identity); + if (group === undefined) { + group = { relation: fact.relation, sources: new Map, tuple: fact.tuple }; + grouped.set(identity, group); + } + for (const source of fact.sources) { + const existing = group.sources.get(source.key); + if (existing !== undefined && existing.recordSha256 !== source.recordSha256) { + throw new TypeError("One fact source key is bound to multiple record digests."); + } + group.sources.set(source.key, source); + } + } + return [...grouped.values()].map((group) => createOhProjectionFactV1({ + relation: group.relation, + sources: [...group.sources.values()], + tuple: group.tuple + })).sort(compareProjectionFacts); +} +function createOhProjectionDatasetV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const extractorSha256 = parseSha256Hex(input.extractorSha256); + const factPackId = projectionName(input.factPackId); + const factPackRevision = positiveInteger(input.factPackRevision); + if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null || input.facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new TypeError("Invalid projection dataset."); + const parsedFacts = input.facts.map(parseOhProjectionFactV1); + if (parsedFacts.some((fact) => fact === null)) + throw new TypeError("Invalid fact in projection dataset."); + const facts = mergeProjectionFacts(parsedFacts); + if (facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new RangeError("Projection dataset has too many facts."); + const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256])); + for (const fact of facts) { + for (const source of fact.sources) { + if (refs.get(source.key) !== source.recordSha256) { + throw new TypeError("Projection fact source is not present at the exact input snapshot."); + } + } + } + const factPackPayload = { + extractorSha256, + factPackId, + factPackRevision, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + const factPackSha256 = canonicalSha256(factPackPayload); + const factsSha256 = canonicalSha256(facts); + const payload = { + extractorSha256, + factPackId, + factPackRevision, + factPackSha256, + facts, + factsSha256, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, datasetSha256: canonicalSha256(payload) }; +} +function parseOhProjectionDatasetV1(value, snapshot) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "datasetSha256", + "extractorSha256", + "factPackId", + "factPackRevision", + "factPackSha256", + "facts", + "factsSha256", + "snapshotSha256", + "v" + ]) || value.v !== 1 || !Array.isArray(value.facts)) + return null; + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256); + const declaredFactsSha256 = parseSha256Hex(value.factsSha256); + try { + const dataset = createOhProjectionDatasetV1({ + extractorSha256: value.extractorSha256, + factPackId: value.factPackId, + factPackRevision: value.factPackRevision, + facts: value.facts, + snapshot + }); + return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256 && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256 && dataset.datasetSha256 === datasetSha256 ? dataset : null; + } catch { + return null; + } +} +function ohProjectionVariableV1(name) { + const parsed = projectionName(name); + if (parsed === null) + throw new TypeError("Invalid projection variable name."); + return { kind: "variable", name: parsed, v: 1 }; +} +function ohProjectionConstantV1(value) { + const parsed = atom(value); + if (parsed === INVALID_PROJECTION_ATOM) + throw new TypeError("Invalid projection constant."); + return { kind: "constant", v: 1, value: parsed }; +} +function createOhProjectionLiteralV1(input) { + const relation = projectionName(input.relation); + if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) { + throw new TypeError("Invalid projection literal."); + } + const terms = input.terms.map((term) => parseOhProjectionTermV1(term)); + if (terms.some((term) => term === null)) + throw new TypeError("Invalid term in projection literal."); + return { relation, terms, v: 1 }; +} +function parseOhProjectionTermV1(value) { + if (!isPlainRecord(value) || value.v !== 1) + return null; + if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) { + const name = projectionName(value.name); + return name === null ? null : { kind: "variable", name, v: 1 }; + } + if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) { + const parsed = atom(value.value); + return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed }; + } + return null; +} +function parseOhProjectionLiteralV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"]) || value.v !== 1 || !Array.isArray(value.terms)) + return null; + try { + return createOhProjectionLiteralV1({ + relation: value.relation, + terms: value.terms + }); + } catch { + return null; + } +} +function literalVariables(literal) { + return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []); +} +function createOhProjectionRuleV1(input) { + const ruleId = projectionName(input.ruleId); + const head = parseOhProjectionLiteralV1(input.head); + if (ruleId === null || head === null || input.body.length < 1 || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule) + throw new TypeError("Invalid projection rule."); + const body = input.body.map(parseOhProjectionLiteralV1); + if (body.some((literal) => literal === null)) + throw new TypeError("Invalid body literal in projection rule."); + const bound = new Set(body.flatMap(literalVariables)); + if (literalVariables(head).some((variable) => !bound.has(variable)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projection rule head variable must be bound in its body."); + } + const payload = { body, head, ruleId, v: 1 }; + return { ...payload, ruleSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRuleV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"]) || value.v !== 1 || !Array.isArray(value.body)) + return null; + const ruleSha256 = parseSha256Hex(value.ruleSha256); + try { + const rule = createOhProjectionRuleV1({ + body: value.body, + head: value.head, + ruleId: value.ruleId + }); + return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null; + } catch { + return null; + } +} +function createOhProjectionRulePackV1(input) { + const rulePackId = projectionName(input.rulePackId); + const rulePackRevision = positiveInteger(input.rulePackRevision); + if (rulePackId === null || rulePackRevision === null || input.rules.length < 1 || input.rules.length > OH_PROJECTION_LIMITS_V1.rules) + throw new TypeError("Invalid projection rule pack."); + const parsedRules = input.rules.map(parseOhProjectionRuleV1); + if (parsedRules.some((rule) => rule === null)) + throw new TypeError("Invalid rule in projection rule pack."); + const rules = sortUnique(parsedRules, (rule) => rule.ruleId); + const rulesSha256 = canonicalSha256(rules); + const payload = { + rulePackId, + rulePackRevision, + rules, + rulesSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + return { ...payload, rulePackSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRulePackV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "rulePackId", + "rulePackRevision", + "rulePackSha256", + "rules", + "rulesSha256", + "semantics", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules)) + return null; + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const rulesSha256 = parseSha256Hex(value.rulesSha256); + try { + const pack = createOhProjectionRulePackV1({ + rulePackId: value.rulePackId, + rulePackRevision: value.rulePackRevision, + rules: value.rules + }); + return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null; + } catch { + return null; + } +} +function createOhProjectionQueryV1(input) { + const queryId = projectionName(input.queryId); + const limit = positiveInteger(input.limit ?? 1000, OH_PROJECTION_LIMITS_V1.queryResults); + if (queryId === null || limit === null || input.find.length < 1 || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1 || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals) + throw new TypeError("Invalid projection query."); + const find = input.find.map((name) => projectionName(name)); + const where = input.where.map(parseOhProjectionLiteralV1); + if (find.some((name) => name === null) || !orderedUnique([...find].sort(), String) || where.some((literal) => literal === null)) + throw new TypeError("Invalid projection query variables or literals."); + const bound = new Set(where.flatMap(literalVariables)); + if (find.some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projected query variable must be bound in the query body."); + } + const payload = { + find, + limit, + queryId, + where, + v: 1 + }; + return { ...payload, querySha256: canonicalSha256(payload) }; +} +function parseOhProjectionQueryV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"]) || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where)) + return null; + const querySha256 = parseSha256Hex(value.querySha256); + try { + const query = createOhProjectionQueryV1({ + find: value.find, + limit: value.limit, + queryId: value.queryId, + where: value.where + }); + return querySha256 !== null && query.querySha256 === querySha256 ? query : null; + } catch { + return null; + } +} +function createOhProjectionIdentityV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const query = parseOhProjectionQueryV1(input.query); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + if (snapshot === null || dataset === null || query === null || rulePack === null) { + throw new TypeError("Invalid projection identity input."); + } + const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256); + if (engine === null) + throw new TypeError("Invalid projection engine identity."); + const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 }; + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + datasetSha256: dataset.datasetSha256, + engineSha256: canonicalSha256({ engine, v: 1 }), + evaluationSha256: canonicalSha256(evaluation), + querySha256: query.querySha256, + rulePackSha256: rulePack.rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, projectionSha256: canonicalSha256(payload) }; +} +function parseOhProjectionIdentityV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "datasetSha256", + "engineSha256", + "evaluationSha256", + "projectionSha256", + "querySha256", + "rulePackSha256", + "semantics", + "snapshotSha256", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const engineSha256 = parseSha256Hex(value.engineSha256); + const evaluationSha256 = parseSha256Hex(value.evaluationSha256); + const projectionSha256 = parseSha256Hex(value.projectionSha256); + const querySha256 = parseSha256Hex(value.querySha256); + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null || querySha256 === null || rulePackSha256 === null || snapshotSha256 === null) + return null; + const payload = { + contractSha256, + datasetSha256, + engineSha256, + evaluationSha256, + querySha256, + rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256, + v: 1 + }; + return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null; +} +function invalidationForOhProjectionV1(previous, next) { + const parsedPrevious = parseOhProjectionIdentityV1(previous); + const parsedNext = parseOhProjectionIdentityV1(next); + if (parsedPrevious === null || parsedNext === null) + throw new TypeError("Invalid projection identity."); + if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256) + return { kind: "reusable", v: 1 }; + const reasons = []; + if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256) + reasons.push("snapshot-changed"); + if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256) + reasons.push("dataset-changed"); + if (parsedPrevious.engineSha256 !== parsedNext.engineSha256) + reasons.push("engine-changed"); + if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256) + reasons.push("evaluation-changed"); + if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256) + reasons.push("rule-pack-changed"); + if (parsedPrevious.querySha256 !== parsedNext.querySha256) + reasons.push("query-changed"); + return { kind: "full-rebuild", reasons, v: 1 }; +} +function tupleKey(value) { + return canonicalJson(value); +} +function referenceKey(reference) { + return canonicalJson([reference.relation, reference.tuple]); +} +function relationTuples(relations, relation) { + return [...relations.get(relation)?.values() ?? []].sort((left, right) => compareCanonical(left.tuple, right.tuple)); +} +function setArity(arities, relation, arity) { + const existing = arities.get(relation); + if (existing !== undefined && existing !== arity) { + throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`); + } + arities.set(relation, arity); + if (arities.size > OH_PROJECTION_LIMITS_V1.relations) + throw new RangeError("Projection uses too many relations."); +} +function validateProgramArities(dataset, rulePack, query) { + const arities = new Map; + for (const fact of dataset.facts) + setArity(arities, fact.relation, fact.tuple.length); + for (const rule of rulePack.rules) { + setArity(arities, rule.head.relation, rule.head.terms.length); + for (const literal of rule.body) + setArity(arities, literal.relation, literal.terms.length); + } + for (const literal of query.where) + setArity(arities, literal.relation, literal.terms.length); +} +function sameAtom(left, right) { + return left === right; +} +function unifyLiteral(literal, state, binding) { + const next = new Map(binding); + for (let index = 0;index < literal.terms.length; index += 1) { + const term = literal.terms[index]; + const value = state.tuple[index]; + if (term.kind === "constant") { + if (!sameAtom(term.value, value)) + return null; + continue; + } + if (next.has(term.name)) { + if (!sameAtom(next.get(term.name), value)) + return null; + } else + next.set(term.name, value); + } + return next; +} +function consumeWorkUnit(budget) { + if (budget.units >= budget.maximum) + throw new RangeError("Projection exceeds its work-unit bound."); + budget.units += 1; +} +function matchBody(relations, body, maximumMatches, work) { + let matches = [{ binding: new Map, premises: [] }]; + for (const literal of body) { + const next = []; + const candidates = relationTuples(relations, literal.relation); + for (const match of matches) { + for (const candidate of candidates) { + consumeWorkUnit(work); + const binding = unifyLiteral(literal, candidate, match.binding); + if (binding === null) + continue; + next.push({ binding, premises: [...match.premises, { + relation: literal.relation, + tuple: candidate.tuple + }] }); + if (next.length > maximumMatches) + throw new RangeError("Projection join exceeds its match bound."); + } + } + matches = next; + if (matches.length === 0) + break; + } + return matches; +} +function instantiateHead(head, binding) { + return head.terms.map((term) => term.kind === "constant" ? term.value : binding.get(term.name)); +} +function canonicalWitness(witness) { + if (witness.kind === "fact") + return canonicalJson(witness); + return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 }); +} +function materializeNaive(input) { + const relations = new Map; + for (const fact of input.dataset.facts) { + let relation = relations.get(fact.relation); + if (relation === undefined) { + relation = new Map; + relations.set(fact.relation, relation); + } + relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } }); + } + let derivedFacts = 0; + let rounds = 0; + while (true) { + const candidates = new Map; + for (const rule of input.rulePack.rules) { + for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) { + const derivedTuple = instantiateHead(rule.head, match.binding); + const relation = relations.get(rule.head.relation); + const key = tupleKey(derivedTuple); + if (relation?.has(key) === true) + continue; + const state = { + tuple: derivedTuple, + witness: { kind: "derived", premises: match.premises, rule } + }; + const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple }); + const existing = candidates.get(identity); + if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) { + candidates.set(identity, { relation: rule.head.relation, state }); + } + } + } + if (candidates.size === 0) + break; + if (rounds >= input.maximumRounds) + throw new RangeError("Projection exceeds its evaluation round bound."); + if (derivedFacts + candidates.size > input.maximumDerivedTuples) { + throw new RangeError("Projection exceeds its derived tuple bound."); + } + const ordered = [...candidates.values()].sort((left, right) => compareCanonical([left.relation, left.state.tuple], [right.relation, right.state.tuple])); + for (const candidate of ordered) { + let relation = relations.get(candidate.relation); + if (relation === undefined) { + relation = new Map; + relations.set(candidate.relation, relation); + } + relation.set(tupleKey(candidate.state.tuple), candidate.state); + } + derivedFacts += candidates.size; + rounds += 1; + } + return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds }; +} +function boundedOption(value, fallback, maximum, label) { + const parsed = positiveInteger(value ?? fallback, maximum); + if (parsed === null) + throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`); + return parsed; +} +function resolveEvaluationOptions(options) { + const resolved = { + maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"), + maximumProofDepth: boundedOption(options.maximumProofDepth, 32, OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"), + maximumProofNodes: boundedOption(options.maximumProofNodes, 1024, OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"), + maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"), + maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"), + maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"), + maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits") + }; + if (resolved.maximumResultBytes < 64 * 1024) { + throw new RangeError("maximumResultBytes must be at least 65536."); + } + return resolved; +} +function reserveResultBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function reserveProofNode(budget, options, envelope) { + if (budget.nodes >= options.maximumProofNodes || budget.result.nodes >= options.maximumTotalProofNodes || !reserveResultBytes(budget.result, envelope)) + return false; + budget.nodes += 1; + budget.result.nodes += 1; + return true; +} +function proofForReference(relations, reference, budget, options, depth, visiting) { + if (depth >= options.maximumProofDepth) { + const proof = { + kind: "truncated", + reason: "depth", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const identity = referenceKey(reference); + if (visiting.has(identity)) { + const proof = { + kind: "truncated", + reason: "cycle", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple)); + if (state === undefined) + throw new Error("Projection proof references a tuple outside the materialized result."); + if (state.witness.kind === "fact") { + const proof = { + kind: "fact", + relation: reference.relation, + sources: state.witness.sources, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const envelope = { + kind: "derived", + premises: [], + premisesTruncated: false, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + if (!reserveProofNode(budget, options, envelope)) + return null; + visiting.add(identity); + try { + const premises = []; + let premisesTruncated = false; + for (const premise of state.witness.premises) { + const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting); + if (proof === null) { + premisesTruncated = true; + break; + } + premises.push(proof); + } + return { + kind: "derived", + premises, + premisesTruncated, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + } finally { + visiting.delete(identity); + } +} +function proofIsTruncated(proof) { + return proof.kind === "truncated" || proof.kind === "derived" && (proof.premisesTruncated || proof.premises.some(proofIsTruncated)); +} +function reserveProjectionParseBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function parseProjectionFactSource(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) { + return null; + } + const key = safeCode(value.key, 512); + const recordSha256 = parseSha256Hex(value.recordSha256); + return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 }; +} +function parseProjectionProofWithBudget(value, budget, depth) { + if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes || !isPlainRecord(value) || value.v !== 1) + return null; + const relation = projectionName(value.relation); + const parsedTuple = tuple(value.tuple); + if (relation === null || parsedTuple === null) + return null; + if (value.kind === "fact") { + if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"]) || !Array.isArray(value.sources) || value.sources.length < 1 || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) + return null; + const sources = value.sources.map(parseProjectionFactSource); + if (sources.some((source) => source === null)) + return null; + const parsedSources = sources; + if (!orderedUnique(parsedSources, (source) => source.key)) + return null; + const proof = { + kind: "fact", + relation, + sources: parsedSources, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind === "truncated") { + if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"]) || value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes") + return null; + const reason = value.reason; + const proof = { + kind: "truncated", + reason, + relation, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind !== "derived" || !hasExactKeys(value, [ + "kind", + "premises", + "premisesTruncated", + "relation", + "ruleId", + "ruleSha256", + "tuple", + "v" + ]) || !Array.isArray(value.premises) || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule || typeof value.premisesTruncated !== "boolean") + return null; + const ruleId = projectionName(value.ruleId); + const ruleSha256 = parseSha256Hex(value.ruleSha256); + if (ruleId === null || ruleSha256 === null || !value.premisesTruncated && value.premises.length === 0 || value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule) { + return null; + } + const skeleton = { + kind: "derived", + premises: [], + premisesTruncated: value.premisesTruncated, + relation, + ruleId, + ruleSha256, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, skeleton)) + return null; + budget.nodes += 1; + const premises = []; + for (const premise of value.premises) { + const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1); + if (parsed === null) + return null; + premises.push(parsed); + } + return { ...skeleton, premises }; +} +function parseOhProjectionProofV1(value) { + try { + const budget = { + bytes: 0, + maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes, + maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth, + maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes, + nodes: 0 + }; + const proof = parseProjectionProofWithBudget(value, budget, 0); + return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null; + } catch { + return null; + } +} +function parseProjectionEvaluation(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "maximumDerivedTuples", + "maximumProofDepth", + "maximumProofNodes", + "maximumResultBytes", + "maximumRounds", + "maximumTotalProofNodes", + "maximumWorkUnits", + "v" + ]) || value.v !== 1) + return null; + const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples); + const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth); + const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes); + const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes); + const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds); + const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes); + const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits); + if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null || maximumTotalProofNodes === null || maximumWorkUnits === null) + return null; + return { + maximumDerivedTuples, + maximumProofDepth, + maximumProofNodes, + maximumResultBytes, + maximumRounds, + maximumTotalProofNodes, + maximumWorkUnits, + v: 1 + }; +} +function parseProjectionResultRow(value, evaluation, resultBudget) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1 || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals || typeof value.proofsTruncated !== "boolean") + return null; + const values = tuple(value.values); + const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches); + if (values === null || supportCount === null || !value.proofsTruncated && value.proofs.length === 0) + return null; + if (!reserveProjectionParseBytes(resultBudget, { + proofs: [], + proofsTruncated: value.proofsTruncated, + supportCount, + values, + v: 1 + })) + return null; + const before = resultBudget.nodes; + resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes); + const proofs = []; + for (const proof of value.proofs) { + const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0); + if (parsed === null) + return null; + proofs.push(parsed); + } + resultBudget.maximumNodes = evaluation.maximumTotalProofNodes; + const containsTruncation = proofs.some(proofIsTruncated); + if (!value.proofsTruncated && containsTruncation || value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals && !containsTruncation) + return null; + return { + nodes: resultBudget.nodes - before, + row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 } + }; +} +function parseOhProjectionResultV1(value, expectedProjectionSha256) { + try { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "authority", + "cache", + "engine", + "evaluation", + "identity", + "resultSha256", + "rows", + "stats", + "v" + ]) || value.v !== 1 || value.authority !== "derived" || !isPlainRecord(value.cache) || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild" || value.cache.v !== 1 || !Array.isArray(value.rows) || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats) || !hasExactKeys(value.stats, [ + "baseFacts", + "derivedFacts", + "proofNodes", + "proofsTruncated", + "queryMatches", + "relations", + "rounds", + "truncated", + "truncationReasons", + "v", + "workUnits" + ]) || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons) || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean") + return null; + const engine = safeCode(value.engine, 256); + const evaluation = parseProjectionEvaluation(value.evaluation); + const identity = parseOhProjectionIdentityV1(value.identity); + const resultSha256 = parseSha256Hex(value.resultSha256); + const expected = expectedProjectionSha256 === undefined ? undefined : parseSha256Hex(expectedProjectionSha256); + if (engine === null || evaluation === null || identity === null || resultSha256 === null || expectedProjectionSha256 !== undefined && expected === null || expected !== undefined && identity.projectionSha256 !== expected || identity.engineSha256 !== canonicalSha256({ engine, v: 1 }) || identity.evaluationSha256 !== canonicalSha256(evaluation)) + return null; + const baseFacts = nonnegativeInteger(value.stats.baseFacts); + const derivedFacts = nonnegativeInteger(value.stats.derivedFacts); + const proofNodes = nonnegativeInteger(value.stats.proofNodes); + const queryMatches = nonnegativeInteger(value.stats.queryMatches); + const relations = nonnegativeInteger(value.stats.relations); + const rounds = nonnegativeInteger(value.stats.rounds); + const workUnits = nonnegativeInteger(value.stats.workUnits); + if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations || rounds === null || rounds > evaluation.maximumRounds || workUnits === null || workUnits > evaluation.maximumWorkUnits || relations > baseFacts + derivedFacts || rounds > derivedFacts || rounds === 0 !== (derivedFacts === 0) || queryMatches > workUnits) + return null; + const truncationReasons = value.stats.truncationReasons; + if (truncationReasons.length > 2 || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x") || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes") || value.stats.truncated !== truncationReasons.length > 0) + return null; + const budget = { + bytes: 0, + maximumBytes: evaluation.maximumResultBytes, + maximumDepth: evaluation.maximumProofDepth, + maximumNodes: evaluation.maximumTotalProofNodes, + nodes: 0 + }; + const rows = []; + let supportCount = 0; + for (const row of value.rows) { + const parsed = parseProjectionResultRow(row, evaluation, budget); + if (parsed === null) + return null; + rows.push(parsed.row); + supportCount += parsed.row.supportCount; + if (supportCount > queryMatches) + return null; + } + if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated) || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches)) + return null; + const reasons = truncationReasons; + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine, + evaluation, + identity, + rows, + stats: { + baseFacts, + derivedFacts, + proofNodes, + proofsTruncated: value.stats.proofsTruncated, + queryMatches, + relations, + rounds, + truncated: value.stats.truncated, + truncationReasons: reasons, + v: 1, + workUnits + }, + v: 1 + }; + const serialized = canonicalJson(payload); + return utf8ByteLength(serialized) <= evaluation.maximumResultBytes && sha256Hex(serialized) === resultSha256 ? { ...payload, resultSha256 } : null; + } catch { + return null; + } +} +function buildProjectionResult(input) { + const matches = matchBody(input.materialized.relations, input.query.where, OH_PROJECTION_LIMITS_V1.queryMatches, input.work); + const byValues = new Map; + for (const match of matches) { + const values = input.query.find.map((name) => match.binding.get(name)); + const key = tupleKey(values); + const existing = byValues.get(key); + if (existing === undefined) + byValues.set(key, { match, supportCount: 1 }); + else + byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0 ? match : existing.match, supportCount: existing.supportCount + 1 }); + } + const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); + const resultBudget = { + bytes: 0, + maximumBytes: input.options.maximumResultBytes - 64 * 1024, + nodes: 0 + }; + const rows = []; + let resultBytesTruncated = false; + for (const [key, support] of ordered.slice(0, input.query.limit)) { + const values = JSON.parse(key); + if (!reserveResultBytes(resultBudget, { + proofs: [], + proofsTruncated: false, + supportCount: support.supportCount, + values, + v: 1 + })) { + resultBytesTruncated = true; + break; + } + const budget = { nodes: 0, result: resultBudget }; + const proofs = []; + for (const premise of support.match.premises) { + const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set); + if (proof === null) + break; + proofs.push(proof); + } + const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated); + rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 }); + } + const queryLimitTruncated = ordered.length > input.query.limit; + const truncationReasons = [ + ...queryLimitTruncated ? ["query-limit"] : [], + ...resultBytesTruncated ? ["result-bytes"] : [] + ]; + const truncated = truncationReasons.length > 0; + const identity = createOhProjectionIdentityV1({ + dataset: input.dataset, + query: input.query, + engine: input.engine, + options: input.options, + rulePack: input.rulePack, + snapshot: input.snapshot + }); + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine: input.engine, + evaluation: { ...input.options, v: 1 }, + identity, + rows, + stats: { + baseFacts: input.materialized.baseFacts, + derivedFacts: input.materialized.derivedFacts, + proofNodes: resultBudget.nodes, + proofsTruncated: rows.some((row) => row.proofsTruncated), + queryMatches: matches.length, + relations: input.materialized.relations.size, + rounds: input.materialized.rounds, + truncated, + truncationReasons, + v: 1, + workUnits: input.work.units + }, + v: 1 + }; + const serialized = canonicalJson(payload); + if (utf8ByteLength(serialized) > input.options.maximumResultBytes) { + throw new RangeError("Projection result exceeds its canonical byte bound."); + } + return { ...payload, resultSha256: sha256Hex(serialized) }; +} +function evaluateOhProjectionV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + if (snapshot === null || dataset === null || rulePack === null || query === null) { + throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const materialized = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + return buildProjectionResult({ + dataset, + engine: OH_PROJECTION_INTERNAL_ENGINE_V1, + materialized, + options, + query, + rulePack, + snapshot, + work + }); +} +function evaluateOhProjectionWithMaterializerV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + const engine = safeCode(input.engine, 256); + if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) { + throw new TypeError("Invalid projection adapter input."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const witnessMaterialization = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + const external = input.materialize({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + query, + rulePack + }); + const externalCanonical = new Map; + for (const [relationName, tuples] of external.relationFacts) { + const relation = projectionName(relationName); + if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) { + throw new TypeError("Projection adapter returned an invalid relation."); + } + const parsed = tuples.map(tuple); + if (parsed.some((value) => value === null)) + throw new TypeError("Projection adapter returned an invalid tuple."); + const keys = []; + for (const value of parsed) { + if (value === null) + throw new TypeError("Projection adapter returned an invalid tuple."); + keys.push(tupleKey(value)); + } + externalCanonical.set(relation, [...new Set(keys)].sort()); + } + const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) => [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()])); + const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort(); + for (const relation of relationNames) { + if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) { + throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`); + } + } + return buildProjectionResult({ + dataset, + engine, + materialized: witnessMaterialization, + options, + query, + rulePack, + snapshot, + work + }); +} +function createOhProjectionRecordFactsV1(records, options = {}) { + if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + throw new RangeError("Too many records for projection facts."); + const parsedRecords = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((candidate) => { + const record = parseKnowledgeGraphRecordV1(candidate); + if (record === null) + throw new TypeError("Invalid graph record for projection facts."); + return record; + }); + let projectedFactCount = 0; + for (const record of parsedRecords) { + if (options.includeRecords !== false) + projectedFactCount += 1; + if (options.includeDependencies !== false) + projectedFactCount += record.dependencies.length; + if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("Structural projection exceeds its fact bound."); + } + } + const facts = []; + for (const record of parsedRecords) { + const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 }]; + if (options.includeRecords !== false) { + facts.push(createOhProjectionFactV1({ + relation: "oh.record", + sources: source, + tuple: [record.key, record.kind, record.recordSha256] + })); + } + if (options.includeDependencies !== false) { + for (const dependency of record.dependencies) { + facts.push(createOhProjectionFactV1({ + relation: "oh.dependency", + sources: source, + tuple: [record.key, dependency] + })); + } + } + } + return facts.sort(compareProjectionFacts); +} +function isOhProjectionRecordKindV1(value) { + return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value); +} + +// src/projection-public.ts +var OH_PROJECTION_FORMAT_VERSION_V12 = OH_PROJECTION_FORMAT_VERSION_V1; +var OH_PROJECTION_INTERNAL_ENGINE_V12 = OH_PROJECTION_INTERNAL_ENGINE_V1; +var OH_PROJECTION_LIMITS_V12 = OH_PROJECTION_LIMITS_V1; +var OH_PROJECTION_RECORD_FACT_EXTRACTOR_V12 = OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1; +var OH_PROJECTION_SEMANTICS_V12 = OH_PROJECTION_SEMANTICS_V1; +var createOhProjectionDatasetV12 = createOhProjectionDatasetV1; +var createOhProjectionFactV12 = createOhProjectionFactV1; +var createOhProjectionIdentityV12 = createOhProjectionIdentityV1; +var createOhProjectionLiteralV12 = createOhProjectionLiteralV1; +var createOhProjectionQueryV12 = createOhProjectionQueryV1; +var createOhProjectionRecordFactsV12 = createOhProjectionRecordFactsV1; +var createOhProjectionRulePackV12 = createOhProjectionRulePackV1; +var createOhProjectionRuleV12 = createOhProjectionRuleV1; +var createOhProjectionSnapshotV12 = createOhProjectionSnapshotV1; +var evaluateOhProjectionV12 = evaluateOhProjectionV1; +var invalidationForOhProjectionV12 = invalidationForOhProjectionV1; +var isOhProjectionRecordKindV12 = isOhProjectionRecordKindV1; +var ohProjectionConstantV12 = ohProjectionConstantV1; +var ohProjectionVariableV12 = ohProjectionVariableV1; +var parseOhProjectionDatasetV12 = parseOhProjectionDatasetV1; +var parseOhProjectionFactV12 = parseOhProjectionFactV1; +var parseOhProjectionIdentityV12 = parseOhProjectionIdentityV1; +var parseOhProjectionLiteralV12 = parseOhProjectionLiteralV1; +var parseOhProjectionProofV12 = parseOhProjectionProofV1; +var parseOhProjectionQueryV12 = parseOhProjectionQueryV1; +var parseOhProjectionResultV12 = parseOhProjectionResultV1; +var parseOhProjectionRulePackV12 = parseOhProjectionRulePackV1; +var parseOhProjectionRuleV12 = parseOhProjectionRuleV1; +var parseOhProjectionSnapshotV12 = parseOhProjectionSnapshotV1; +var parseOhProjectionTermV12 = parseOhProjectionTermV1; +export { + parseOhProjectionTermV12 as parseOhProjectionTermV1, + parseOhProjectionSnapshotV12 as parseOhProjectionSnapshotV1, + parseOhProjectionRuleV12 as parseOhProjectionRuleV1, + parseOhProjectionRulePackV12 as parseOhProjectionRulePackV1, + parseOhProjectionResultV12 as parseOhProjectionResultV1, + parseOhProjectionQueryV12 as parseOhProjectionQueryV1, + parseOhProjectionProofV12 as parseOhProjectionProofV1, + parseOhProjectionLiteralV12 as parseOhProjectionLiteralV1, + parseOhProjectionIdentityV12 as parseOhProjectionIdentityV1, + parseOhProjectionFactV12 as parseOhProjectionFactV1, + parseOhProjectionDatasetV12 as parseOhProjectionDatasetV1, + ohProjectionVariableV12 as ohProjectionVariableV1, + ohProjectionConstantV12 as ohProjectionConstantV1, + isOhProjectionRecordKindV12 as isOhProjectionRecordKindV1, + invalidationForOhProjectionV12 as invalidationForOhProjectionV1, + evaluateOhProjectionV12 as evaluateOhProjectionV1, + createOhProjectionSnapshotV12 as createOhProjectionSnapshotV1, + createOhProjectionRuleV12 as createOhProjectionRuleV1, + createOhProjectionRulePackV12 as createOhProjectionRulePackV1, + createOhProjectionRecordFactsV12 as createOhProjectionRecordFactsV1, + createOhProjectionQueryV12 as createOhProjectionQueryV1, + createOhProjectionLiteralV12 as createOhProjectionLiteralV1, + createOhProjectionIdentityV12 as createOhProjectionIdentityV1, + createOhProjectionFactV12 as createOhProjectionFactV1, + createOhProjectionDatasetV12 as createOhProjectionDatasetV1, + OH_PROJECTION_SEMANTICS_V12 as OH_PROJECTION_SEMANTICS_V1, + OH_PROJECTION_RECORD_FACT_EXTRACTOR_V12 as OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1, + OH_PROJECTION_LIMITS_V12 as OH_PROJECTION_LIMITS_V1, + OH_PROJECTION_INTERNAL_ENGINE_V12 as OH_PROJECTION_INTERNAL_ENGINE_V1, + OH_PROJECTION_FORMAT_VERSION_V12 as OH_PROJECTION_FORMAT_VERSION_V1 +}; diff --git a/dist/projection-suss.d.ts b/dist/projection-suss.d.ts new file mode 100644 index 0000000..14367a2 --- /dev/null +++ b/dist/projection-suss.d.ts @@ -0,0 +1,17 @@ +import { type OhProjectionDatasetV1, type OhProjectionEvaluationOptionsV1, type OhProjectionQueryV1, type OhProjectionResultV1, type OhProjectionRulePackV1, type OhProjectionSnapshotV1 } from "./projection"; +export declare const OH_PROJECTION_SUSS_VERSION_V1: "0.20.0"; +export declare const OH_PROJECTION_SUSS_ENGINE_V1: "suss.datalog.v0-20-0.equivalence"; +/** + * Evaluates the positive rule pack with Suss 0.20.0, then requires its complete + * relation sets to equal Oh's bounded reference semantics before returning the + * canonical derived-only result. The conservative admission check keeps Suss, + * whose public evaluator has no execution-budget hook, inside Oh's tuple bound. + */ +export declare function evaluateOhProjectionWithSussV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1; +//# sourceMappingURL=projection-suss.d.ts.map \ No newline at end of file diff --git a/dist/projection-suss.d.ts.map b/dist/projection-suss.d.ts.map new file mode 100644 index 0000000..9eb582a --- /dev/null +++ b/dist/projection-suss.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"projection-suss.d.ts","sourceRoot":"","sources":["../src/projection-suss.ts"],"names":[],"mappings":"AAYA,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,+BAA+B,EAEpC,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAE5B,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,6BAA6B,EAAG,QAAiB,CAAC;AAC/D,eAAO,MAAM,4BAA4B,EAAG,kCAA2C,CAAC;AAoExF;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC7D,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,CAAC,EAAE,+BAA+B,CAAC;IAC1C,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,GAAG,oBAAoB,CAsBxB"} \ No newline at end of file diff --git a/dist/projection-suss.js b/dist/projection-suss.js new file mode 100644 index 0000000..d82727e --- /dev/null +++ b/dist/projection-suss.js @@ -0,0 +1,1721 @@ +// src/projection-suss.ts +import { + Database, + constant, + evaluate, + lit, + rule, + variable +} from "@suss/datalog"; + +// src/canonical.ts +import { createHash, randomBytes } from "node:crypto"; + +class OhValidationError extends Error { + code; + path; + constructor(code, path, message) { + super(`${path}: ${message}`); + this.name = "OhValidationError"; + this.code = code; + this.path = path; + } +} +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} +function assertUnicodeScalarString(value, path) { + for (let index = 0;index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(index + 1); + if (!(next >= 56320 && next <= 57343)) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + index += 1; + } else if (code >= 56320 && code <= 57343) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + } +} +function encodeCanonical(value, path, ancestors) { + if (value === null || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "string") { + assertUnicodeScalarString(value, path); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new OhValidationError("non-json-number", path, "must be finite"); + } + if (Object.is(value, -0)) { + throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical"); + } + return JSON.stringify(value); + } + if (typeof value !== "object" || value === null) { + throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`); + } + if (ancestors.has(value)) { + throw new OhValidationError("cycle", path, "contains a cycle"); + } + ancestors.add(value); + try { + if (Array.isArray(value)) { + const encoded = []; + for (let index = 0;index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes"); + } + encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors)); + } + const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)); + if (extraKeys.length > 0) { + throw new OhValidationError("non-json-property", path, "array has non-index properties"); + } + return `[${encoded.join(",")}]`; + } + if (!isPlainRecord(value)) { + throw new OhValidationError("non-plain-object", path, "must be a plain object"); + } + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== "string")) { + throw new OhValidationError("non-json-property", path, "object has a symbol property"); + } + const keys = ownKeys; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) { + throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property"); + } + } + keys.sort(); + const entries = keys.map((key) => { + assertUnicodeScalarString(key, `${path}.`); + return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`; + }); + return `{${entries.join(",")}}`; + } finally { + ancestors.delete(value); + } +} +function canonicalJson(value) { + return encodeCanonical(value, "$", new Set); +} +function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) { + if (utf8ByteLength(text) > maximumBytes) { + throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new OhValidationError("invalid-json", "$", "is not valid JSON"); + } + if (canonicalJson(value) !== text) { + throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical"); + } + return value; +} +function utf8ByteLength(value) { + return Buffer.byteLength(value, "utf8"); +} +function sha256Hex(value) { + return createHash("sha256").update(value).digest("hex"); +} +function canonicalSha256(value) { + return sha256Hex(canonicalJson(value)); +} +function parseSha256Hex(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null; +} +function parseCanonicalInstantV1(value) { + if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) { + return null; + } + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null; +} +function canonicalNow() { + return new Date().toISOString(); +} +function safeCode(value, maximumLength = 128) { + return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function orderedUnique(values, key) { + return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)); +} +function sortUnique(values, key) { + const sorted = [...values].sort((left, right) => { + const leftKey = key(left); + const rightKey = key(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); + if (!orderedUnique(sorted, key)) { + throw new OhValidationError("duplicate", "$", "contains duplicate canonical values"); + } + return sorted; +} + +// src/graph.ts +var OH_GRAPH_FORMAT_VERSION_V1 = 1; +var OH_GRAPH_LIMITS_V1 = Object.freeze({ + changesPerOperation: 8192, + dependenciesPerRecord: 4096, + recordBytes: 1024 * 1024, + recordsPerSnapshot: 65536 +}); +var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [ + "activity", + "assertion", + "context", + "dependency-manifest", + "edition", + "entity", + "evidence", + "identity-operation", + "inquiry", + "inquiry-event", + "review-decision", + "rights-decision", + "schema", + "shape", + "statement", + "type-membership", + "view", + "vocabulary" +]; +function recordKey(value) { + return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function createKnowledgeGraphRecordV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies)) + throw new TypeError("Invalid graph record input."); + const key = recordKey(input.key); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind); + if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) + throw new TypeError("Invalid graph record identity."); + const dependencies = input.dependencies.map(recordKey); + if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) { + throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive."); + } + const valueJson = canonicalJson(input.value); + if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) { + throw new RangeError("Graph record value exceeds its canonical byte limit."); + } + const payload = { dependencies, key, kind, v: 1, value: input.value }; + return { ...payload, recordSha256: canonicalSha256(payload) }; +} +function parseKnowledgeGraphRecordV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256")) + return null; + const recordSha256 = parseSha256Hex(value.recordSha256); + const { recordSha256: _digest, ...input } = value; + try { + const created = createKnowledgeGraphRecordV1(input); + return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null; + } catch { + return null; + } +} +function knowledgeGraphRecordRefV1(record) { + return { + dependencies: record.dependencies, + key: record.key, + kind: record.kind, + sha256: record.recordSha256, + v: 1 + }; +} +function changeKey(change) { + return change.kind === "put" ? change.record.key : change.key; +} +function canonicalKnowledgeGraphChangesV1(changes) { + const normalized = []; + for (const change of changes) { + if (!isPlainRecord(change) || change.v !== 1) + throw new TypeError("Invalid graph change."); + if (change.kind === "put") { + const record = parseKnowledgeGraphRecordV1(change.record); + if (record === null) + throw new TypeError("Invalid graph record in change."); + normalized.push({ kind: "put", record, v: 1 }); + } else if (change.kind === "tombstone") { + const key = recordKey(change.key); + const priorSha256 = parseSha256Hex(change.priorSha256); + if (key === null || priorSha256 === null) + throw new TypeError("Invalid graph tombstone."); + normalized.push({ key, kind: "tombstone", priorSha256, v: 1 }); + } else + throw new TypeError("Unknown graph change kind."); + } + return sortUnique(normalized, changeKey); +} +function graphRevisionSha256V1(input) { + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + const operationId = safeCode(input.operationId); + const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256); + const recordsSha256 = parseSha256Hex(input.recordsSha256); + const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null; + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) { + throw new TypeError("Invalid graph revision digest input."); + } + return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 }); +} + +// src/ontology.ts +var OH_ONTOLOGY_VERSION_V1 = "1.0.0"; +var OH_CONTRACT_ID_V1 = "oh.ontology.v1"; +var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({ + dimensions: 64, + listValues: 256, + qualifiers: 128, + statementBytes: 256 * 1024, + textBytes: 64 * 1024 +}); + +// src/schema.ts +var OH_SCHEMA_FORMAT_VERSION_V1 = 1; + +// src/contract.ts +var manifestPayload = Object.freeze({ + contractId: OH_CONTRACT_ID_V1, + graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1, + ontologyVersion: OH_ONTOLOGY_VERSION_V1, + recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1, + v: 1 +}); +var OH_CONTRACT_MANIFEST_V1 = Object.freeze({ + ...manifestPayload, + contractSha256: canonicalSha256(manifestPayload) +}); +class OhRecordCodecRegistry { + #codecs = new Map; + #sealed = false; + register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); + if (this.#codecs.has(codec.kind)) + throw new TypeError(`A codec is already registered for ${codec.kind}.`); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); + return this; + } + parse(kind, value) { + const codec = this.#codecs.get(kind); + if (codec !== undefined) + return codec.parse(value); + try { + canonicalJson(value); + return value; + } catch { + return null; + } + } + has(kind) { + return this.#codecs.has(kind); + } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } +} + +// src/projection.ts +var OH_PROJECTION_FORMAT_VERSION_V1 = 1; +var OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1"; +var OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1"; +var OH_PROJECTION_LIMITS_V1 = Object.freeze({ + arity: 32, + atomBytes: 16 * 1024, + derivedTuples: 262144, + facts: 262144, + literalsPerRule: 64, + proofDepth: 128, + proofNodes: 4096, + queryLiterals: 64, + queryMatches: 262144, + queryResults: 65536, + relations: 4096, + resultBytes: 16 * 1024 * 1024, + rounds: 1024, + rules: 1024, + sourcesPerFact: 64, + totalProofNodes: 65536, + variables: 256, + workUnits: 16777216 +}); +var recordFactExtractorPayloadV1 = { + factPackId: "oh.record-facts", + factPackRevision: 1, + relations: ["oh.dependency", "oh.record"], + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 +}; +var OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({ + ...recordFactExtractorPayloadV1, + extractorSha256: canonicalSha256(recordFactExtractorPayloadV1) +}); +function nonnegativeInteger(value) { + return Number.isSafeInteger(value) && value >= 0 ? value : null; +} +function positiveInteger(value, maximum = Number.MAX_SAFE_INTEGER) { + return Number.isSafeInteger(value) && value >= 1 && value <= maximum ? value : null; +} +function projectionName(value, maximumLength = 128) { + return safeCode(value, maximumLength); +} +function compareCanonical(left, right) { + const leftKey = canonicalJson(left); + const rightKey = canonicalJson(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; +} +function compareProjectionFacts(left, right) { + return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]); +} +var INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom"); +function atom(value) { + if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") { + return INVALID_PROJECTION_ATOM; + } + try { + const encoded = canonicalJson(value); + return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes ? value : INVALID_PROJECTION_ATOM; + } catch { + return INVALID_PROJECTION_ATOM; + } +} +function tuple(value) { + if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity) + return null; + const parsed = value.map(atom); + return parsed.some((item) => item === INVALID_PROJECTION_ATOM) ? null : parsed; +} +function parseRecordRef(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"]) || value.v !== 1 || !Array.isArray(value.dependencies)) + return null; + const key = safeCode(value.key, 512); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind); + const sha256 = parseSha256Hex(value.sha256); + const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512)); + if (key === null || kind === undefined || sha256 === null || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord || dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) + return null; + return { dependencies, key, kind, sha256, v: 1 }; +} +function createOhProjectionSnapshotV1(input) { + const spaceId = projectionName(input.spaceId); + const generation = nonnegativeInteger(input.head.generation); + const sequence = nonnegativeInteger(input.head.sequence); + const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256); + const graphRevisionSha256 = input.head.graphRevisionSha256 === null ? null : parseSha256Hex(input.head.graphRevisionSha256); + const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256); + if (spaceId === null || generation === null || sequence === null || generation !== sequence || input.head.operationSha256 !== null && operationSha256 === null || input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid projection snapshot head."); + } + const records = input.records.map(parseKnowledgeGraphRecordV1); + if (records.some((record) => record === null)) + throw new TypeError("Invalid record in projection snapshot."); + const recordRefs = sortUnique(records.map(knowledgeGraphRecordRefV1), (reference) => reference.key); + const recordsSha256 = canonicalSha256(recordRefs); + if (recordsSha256 !== declaredRecordsSha256) { + throw new TypeError("Projection snapshot records do not reproduce the declared head."); + } + const keys = new Set(recordRefs.map((reference) => reference.key)); + if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) { + throw new TypeError("Projection snapshot has a missing record dependency."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return { ...payload, snapshotSha256: canonicalSha256(payload) }; +} +function parseOhProjectionSnapshotV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "generation", + "graphRevisionSha256", + "operationSha256", + "recordRefs", + "recordsSha256", + "sequence", + "snapshotSha256", + "spaceId", + "v" + ]) || value.v !== 1 || !Array.isArray(value.recordRefs) || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const generation = nonnegativeInteger(value.generation); + const sequence = nonnegativeInteger(value.sequence); + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + const spaceId = projectionName(value.spaceId); + const recordRefs = value.recordRefs.map(parseRecordRef); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null || value.graphRevisionSha256 !== null && graphRevisionSha256 === null || value.operationSha256 !== null && operationSha256 === null || sequence === 0 !== (operationSha256 === null) || sequence === 0 !== (graphRevisionSha256 === null) || recordRefs.some((reference) => reference === null)) + return null; + const refs = recordRefs; + if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256) + return null; + const keys = new Set(refs.map((reference) => reference.key)); + if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) + return null; + const payload = { + contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs: refs, + recordsSha256, + sequence, + spaceId, + v: 1 + }; + return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null; +} +function createOhProjectionFactV1(input) { + const relation = projectionName(input.relation); + const parsedTuple = tuple(input.tuple); + if (relation === null || parsedTuple === null || input.sources.length < 1 || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) { + throw new TypeError("Invalid projection fact."); + } + const sources = input.sources.map((source) => { + if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) { + throw new TypeError("Invalid projection fact source."); + } + const key = safeCode(source.key, 512); + const recordSha256 = parseSha256Hex(source.recordSha256); + if (key === null || recordSha256 === null) + throw new TypeError("Invalid projection fact source."); + return { key, recordSha256, v: 1 }; + }).sort(compareCanonical); + if (!orderedUnique(sources, (source) => source.key)) { + throw new TypeError("Projection fact sources must have unique record keys."); + } + const payload = { relation, sources, tuple: parsedTuple, v: 1 }; + return { ...payload, factSha256: canonicalSha256(payload) }; +} +function parseOhProjectionFactV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"]) || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple)) + return null; + const factSha256 = parseSha256Hex(value.factSha256); + try { + const fact = createOhProjectionFactV1({ + relation: value.relation, + sources: value.sources, + tuple: value.tuple + }); + return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null; + } catch { + return null; + } +} +function mergeProjectionFacts(facts) { + const grouped = new Map; + for (const fact of facts) { + const identity = canonicalJson([fact.relation, fact.tuple]); + let group = grouped.get(identity); + if (group === undefined) { + group = { relation: fact.relation, sources: new Map, tuple: fact.tuple }; + grouped.set(identity, group); + } + for (const source of fact.sources) { + const existing = group.sources.get(source.key); + if (existing !== undefined && existing.recordSha256 !== source.recordSha256) { + throw new TypeError("One fact source key is bound to multiple record digests."); + } + group.sources.set(source.key, source); + } + } + return [...grouped.values()].map((group) => createOhProjectionFactV1({ + relation: group.relation, + sources: [...group.sources.values()], + tuple: group.tuple + })).sort(compareProjectionFacts); +} +function createOhProjectionDatasetV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const extractorSha256 = parseSha256Hex(input.extractorSha256); + const factPackId = projectionName(input.factPackId); + const factPackRevision = positiveInteger(input.factPackRevision); + if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null || input.facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new TypeError("Invalid projection dataset."); + const parsedFacts = input.facts.map(parseOhProjectionFactV1); + if (parsedFacts.some((fact) => fact === null)) + throw new TypeError("Invalid fact in projection dataset."); + const facts = mergeProjectionFacts(parsedFacts); + if (facts.length > OH_PROJECTION_LIMITS_V1.facts) + throw new RangeError("Projection dataset has too many facts."); + const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256])); + for (const fact of facts) { + for (const source of fact.sources) { + if (refs.get(source.key) !== source.recordSha256) { + throw new TypeError("Projection fact source is not present at the exact input snapshot."); + } + } + } + const factPackPayload = { + extractorSha256, + factPackId, + factPackRevision, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + const factPackSha256 = canonicalSha256(factPackPayload); + const factsSha256 = canonicalSha256(facts); + const payload = { + extractorSha256, + factPackId, + factPackRevision, + factPackSha256, + facts, + factsSha256, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, datasetSha256: canonicalSha256(payload) }; +} +function parseOhProjectionDatasetV1(value, snapshot) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "datasetSha256", + "extractorSha256", + "factPackId", + "factPackRevision", + "factPackSha256", + "facts", + "factsSha256", + "snapshotSha256", + "v" + ]) || value.v !== 1 || !Array.isArray(value.facts)) + return null; + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256); + const declaredFactsSha256 = parseSha256Hex(value.factsSha256); + try { + const dataset = createOhProjectionDatasetV1({ + extractorSha256: value.extractorSha256, + factPackId: value.factPackId, + factPackRevision: value.factPackRevision, + facts: value.facts, + snapshot + }); + return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256 && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256 && dataset.datasetSha256 === datasetSha256 ? dataset : null; + } catch { + return null; + } +} +function ohProjectionVariableV1(name) { + const parsed = projectionName(name); + if (parsed === null) + throw new TypeError("Invalid projection variable name."); + return { kind: "variable", name: parsed, v: 1 }; +} +function ohProjectionConstantV1(value) { + const parsed = atom(value); + if (parsed === INVALID_PROJECTION_ATOM) + throw new TypeError("Invalid projection constant."); + return { kind: "constant", v: 1, value: parsed }; +} +function createOhProjectionLiteralV1(input) { + const relation = projectionName(input.relation); + if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) { + throw new TypeError("Invalid projection literal."); + } + const terms = input.terms.map((term) => parseOhProjectionTermV1(term)); + if (terms.some((term) => term === null)) + throw new TypeError("Invalid term in projection literal."); + return { relation, terms, v: 1 }; +} +function parseOhProjectionTermV1(value) { + if (!isPlainRecord(value) || value.v !== 1) + return null; + if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) { + const name = projectionName(value.name); + return name === null ? null : { kind: "variable", name, v: 1 }; + } + if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) { + const parsed = atom(value.value); + return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed }; + } + return null; +} +function parseOhProjectionLiteralV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"]) || value.v !== 1 || !Array.isArray(value.terms)) + return null; + try { + return createOhProjectionLiteralV1({ + relation: value.relation, + terms: value.terms + }); + } catch { + return null; + } +} +function literalVariables(literal) { + return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []); +} +function createOhProjectionRuleV1(input) { + const ruleId = projectionName(input.ruleId); + const head = parseOhProjectionLiteralV1(input.head); + if (ruleId === null || head === null || input.body.length < 1 || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule) + throw new TypeError("Invalid projection rule."); + const body = input.body.map(parseOhProjectionLiteralV1); + if (body.some((literal) => literal === null)) + throw new TypeError("Invalid body literal in projection rule."); + const bound = new Set(body.flatMap(literalVariables)); + if (literalVariables(head).some((variable) => !bound.has(variable)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projection rule head variable must be bound in its body."); + } + const payload = { body, head, ruleId, v: 1 }; + return { ...payload, ruleSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRuleV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"]) || value.v !== 1 || !Array.isArray(value.body)) + return null; + const ruleSha256 = parseSha256Hex(value.ruleSha256); + try { + const rule = createOhProjectionRuleV1({ + body: value.body, + head: value.head, + ruleId: value.ruleId + }); + return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null; + } catch { + return null; + } +} +function createOhProjectionRulePackV1(input) { + const rulePackId = projectionName(input.rulePackId); + const rulePackRevision = positiveInteger(input.rulePackRevision); + if (rulePackId === null || rulePackRevision === null || input.rules.length < 1 || input.rules.length > OH_PROJECTION_LIMITS_V1.rules) + throw new TypeError("Invalid projection rule pack."); + const parsedRules = input.rules.map(parseOhProjectionRuleV1); + if (parsedRules.some((rule) => rule === null)) + throw new TypeError("Invalid rule in projection rule pack."); + const rules = sortUnique(parsedRules, (rule) => rule.ruleId); + const rulesSha256 = canonicalSha256(rules); + const payload = { + rulePackId, + rulePackRevision, + rules, + rulesSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 + }; + return { ...payload, rulePackSha256: canonicalSha256(payload) }; +} +function parseOhProjectionRulePackV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "rulePackId", + "rulePackRevision", + "rulePackSha256", + "rules", + "rulesSha256", + "semantics", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules)) + return null; + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const rulesSha256 = parseSha256Hex(value.rulesSha256); + try { + const pack = createOhProjectionRulePackV1({ + rulePackId: value.rulePackId, + rulePackRevision: value.rulePackRevision, + rules: value.rules + }); + return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null; + } catch { + return null; + } +} +function createOhProjectionQueryV1(input) { + const queryId = projectionName(input.queryId); + const limit = positiveInteger(input.limit ?? 1000, OH_PROJECTION_LIMITS_V1.queryResults); + if (queryId === null || limit === null || input.find.length < 1 || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1 || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals) + throw new TypeError("Invalid projection query."); + const find = input.find.map((name) => projectionName(name)); + const where = input.where.map(parseOhProjectionLiteralV1); + if (find.some((name) => name === null) || !orderedUnique([...find].sort(), String) || where.some((literal) => literal === null)) + throw new TypeError("Invalid projection query variables or literals."); + const bound = new Set(where.flatMap(literalVariables)); + if (find.some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projected query variable must be bound in the query body."); + } + const payload = { + find, + limit, + queryId, + where, + v: 1 + }; + return { ...payload, querySha256: canonicalSha256(payload) }; +} +function parseOhProjectionQueryV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"]) || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where)) + return null; + const querySha256 = parseSha256Hex(value.querySha256); + try { + const query = createOhProjectionQueryV1({ + find: value.find, + limit: value.limit, + queryId: value.queryId, + where: value.where + }); + return querySha256 !== null && query.querySha256 === querySha256 ? query : null; + } catch { + return null; + } +} +function createOhProjectionIdentityV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const query = parseOhProjectionQueryV1(input.query); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + if (snapshot === null || dataset === null || query === null || rulePack === null) { + throw new TypeError("Invalid projection identity input."); + } + const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256); + if (engine === null) + throw new TypeError("Invalid projection engine identity."); + const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 }; + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + datasetSha256: dataset.datasetSha256, + engineSha256: canonicalSha256({ engine, v: 1 }), + evaluationSha256: canonicalSha256(evaluation), + querySha256: query.querySha256, + rulePackSha256: rulePack.rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256: snapshot.snapshotSha256, + v: 1 + }; + return { ...payload, projectionSha256: canonicalSha256(payload) }; +} +function parseOhProjectionIdentityV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "contractSha256", + "datasetSha256", + "engineSha256", + "evaluationSha256", + "projectionSha256", + "querySha256", + "rulePackSha256", + "semantics", + "snapshotSha256", + "v" + ]) || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1) + return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const engineSha256 = parseSha256Hex(value.engineSha256); + const evaluationSha256 = parseSha256Hex(value.evaluationSha256); + const projectionSha256 = parseSha256Hex(value.projectionSha256); + const querySha256 = parseSha256Hex(value.querySha256); + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null || querySha256 === null || rulePackSha256 === null || snapshotSha256 === null) + return null; + const payload = { + contractSha256, + datasetSha256, + engineSha256, + evaluationSha256, + querySha256, + rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256, + v: 1 + }; + return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null; +} +function invalidationForOhProjectionV1(previous, next) { + const parsedPrevious = parseOhProjectionIdentityV1(previous); + const parsedNext = parseOhProjectionIdentityV1(next); + if (parsedPrevious === null || parsedNext === null) + throw new TypeError("Invalid projection identity."); + if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256) + return { kind: "reusable", v: 1 }; + const reasons = []; + if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256) + reasons.push("snapshot-changed"); + if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256) + reasons.push("dataset-changed"); + if (parsedPrevious.engineSha256 !== parsedNext.engineSha256) + reasons.push("engine-changed"); + if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256) + reasons.push("evaluation-changed"); + if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256) + reasons.push("rule-pack-changed"); + if (parsedPrevious.querySha256 !== parsedNext.querySha256) + reasons.push("query-changed"); + return { kind: "full-rebuild", reasons, v: 1 }; +} +function tupleKey(value) { + return canonicalJson(value); +} +function referenceKey(reference) { + return canonicalJson([reference.relation, reference.tuple]); +} +function relationTuples(relations, relation) { + return [...relations.get(relation)?.values() ?? []].sort((left, right) => compareCanonical(left.tuple, right.tuple)); +} +function setArity(arities, relation, arity) { + const existing = arities.get(relation); + if (existing !== undefined && existing !== arity) { + throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`); + } + arities.set(relation, arity); + if (arities.size > OH_PROJECTION_LIMITS_V1.relations) + throw new RangeError("Projection uses too many relations."); +} +function validateProgramArities(dataset, rulePack, query) { + const arities = new Map; + for (const fact of dataset.facts) + setArity(arities, fact.relation, fact.tuple.length); + for (const rule of rulePack.rules) { + setArity(arities, rule.head.relation, rule.head.terms.length); + for (const literal of rule.body) + setArity(arities, literal.relation, literal.terms.length); + } + for (const literal of query.where) + setArity(arities, literal.relation, literal.terms.length); +} +function sameAtom(left, right) { + return left === right; +} +function unifyLiteral(literal, state, binding) { + const next = new Map(binding); + for (let index = 0;index < literal.terms.length; index += 1) { + const term = literal.terms[index]; + const value = state.tuple[index]; + if (term.kind === "constant") { + if (!sameAtom(term.value, value)) + return null; + continue; + } + if (next.has(term.name)) { + if (!sameAtom(next.get(term.name), value)) + return null; + } else + next.set(term.name, value); + } + return next; +} +function consumeWorkUnit(budget) { + if (budget.units >= budget.maximum) + throw new RangeError("Projection exceeds its work-unit bound."); + budget.units += 1; +} +function matchBody(relations, body, maximumMatches, work) { + let matches = [{ binding: new Map, premises: [] }]; + for (const literal of body) { + const next = []; + const candidates = relationTuples(relations, literal.relation); + for (const match of matches) { + for (const candidate of candidates) { + consumeWorkUnit(work); + const binding = unifyLiteral(literal, candidate, match.binding); + if (binding === null) + continue; + next.push({ binding, premises: [...match.premises, { + relation: literal.relation, + tuple: candidate.tuple + }] }); + if (next.length > maximumMatches) + throw new RangeError("Projection join exceeds its match bound."); + } + } + matches = next; + if (matches.length === 0) + break; + } + return matches; +} +function instantiateHead(head, binding) { + return head.terms.map((term) => term.kind === "constant" ? term.value : binding.get(term.name)); +} +function canonicalWitness(witness) { + if (witness.kind === "fact") + return canonicalJson(witness); + return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 }); +} +function materializeNaive(input) { + const relations = new Map; + for (const fact of input.dataset.facts) { + let relation = relations.get(fact.relation); + if (relation === undefined) { + relation = new Map; + relations.set(fact.relation, relation); + } + relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } }); + } + let derivedFacts = 0; + let rounds = 0; + while (true) { + const candidates = new Map; + for (const rule of input.rulePack.rules) { + for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) { + const derivedTuple = instantiateHead(rule.head, match.binding); + const relation = relations.get(rule.head.relation); + const key = tupleKey(derivedTuple); + if (relation?.has(key) === true) + continue; + const state = { + tuple: derivedTuple, + witness: { kind: "derived", premises: match.premises, rule } + }; + const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple }); + const existing = candidates.get(identity); + if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) { + candidates.set(identity, { relation: rule.head.relation, state }); + } + } + } + if (candidates.size === 0) + break; + if (rounds >= input.maximumRounds) + throw new RangeError("Projection exceeds its evaluation round bound."); + if (derivedFacts + candidates.size > input.maximumDerivedTuples) { + throw new RangeError("Projection exceeds its derived tuple bound."); + } + const ordered = [...candidates.values()].sort((left, right) => compareCanonical([left.relation, left.state.tuple], [right.relation, right.state.tuple])); + for (const candidate of ordered) { + let relation = relations.get(candidate.relation); + if (relation === undefined) { + relation = new Map; + relations.set(candidate.relation, relation); + } + relation.set(tupleKey(candidate.state.tuple), candidate.state); + } + derivedFacts += candidates.size; + rounds += 1; + } + return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds }; +} +function boundedOption(value, fallback, maximum, label) { + const parsed = positiveInteger(value ?? fallback, maximum); + if (parsed === null) + throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`); + return parsed; +} +function resolveEvaluationOptions(options) { + const resolved = { + maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"), + maximumProofDepth: boundedOption(options.maximumProofDepth, 32, OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"), + maximumProofNodes: boundedOption(options.maximumProofNodes, 1024, OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"), + maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"), + maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"), + maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, "maximumTotalProofNodes"), + maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits") + }; + if (resolved.maximumResultBytes < 64 * 1024) { + throw new RangeError("maximumResultBytes must be at least 65536."); + } + return resolved; +} +function reserveResultBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function reserveProofNode(budget, options, envelope) { + if (budget.nodes >= options.maximumProofNodes || budget.result.nodes >= options.maximumTotalProofNodes || !reserveResultBytes(budget.result, envelope)) + return false; + budget.nodes += 1; + budget.result.nodes += 1; + return true; +} +function proofForReference(relations, reference, budget, options, depth, visiting) { + if (depth >= options.maximumProofDepth) { + const proof = { + kind: "truncated", + reason: "depth", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const identity = referenceKey(reference); + if (visiting.has(identity)) { + const proof = { + kind: "truncated", + reason: "cycle", + relation: reference.relation, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple)); + if (state === undefined) + throw new Error("Projection proof references a tuple outside the materialized result."); + if (state.witness.kind === "fact") { + const proof = { + kind: "fact", + relation: reference.relation, + sources: state.witness.sources, + tuple: reference.tuple, + v: 1 + }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const envelope = { + kind: "derived", + premises: [], + premisesTruncated: false, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + if (!reserveProofNode(budget, options, envelope)) + return null; + visiting.add(identity); + try { + const premises = []; + let premisesTruncated = false; + for (const premise of state.witness.premises) { + const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting); + if (proof === null) { + premisesTruncated = true; + break; + } + premises.push(proof); + } + return { + kind: "derived", + premises, + premisesTruncated, + relation: reference.relation, + ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, + tuple: reference.tuple, + v: 1 + }; + } finally { + visiting.delete(identity); + } +} +function proofIsTruncated(proof) { + return proof.kind === "truncated" || proof.kind === "derived" && (proof.premisesTruncated || proof.premises.some(proofIsTruncated)); +} +function reserveProjectionParseBytes(budget, value) { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) + return false; + budget.bytes += bytes; + return true; +} +function parseProjectionFactSource(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) { + return null; + } + const key = safeCode(value.key, 512); + const recordSha256 = parseSha256Hex(value.recordSha256); + return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 }; +} +function parseProjectionProofWithBudget(value, budget, depth) { + if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes || !isPlainRecord(value) || value.v !== 1) + return null; + const relation = projectionName(value.relation); + const parsedTuple = tuple(value.tuple); + if (relation === null || parsedTuple === null) + return null; + if (value.kind === "fact") { + if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"]) || !Array.isArray(value.sources) || value.sources.length < 1 || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) + return null; + const sources = value.sources.map(parseProjectionFactSource); + if (sources.some((source) => source === null)) + return null; + const parsedSources = sources; + if (!orderedUnique(parsedSources, (source) => source.key)) + return null; + const proof = { + kind: "fact", + relation, + sources: parsedSources, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind === "truncated") { + if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"]) || value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes") + return null; + const reason = value.reason; + const proof = { + kind: "truncated", + reason, + relation, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, proof)) + return null; + budget.nodes += 1; + return proof; + } + if (value.kind !== "derived" || !hasExactKeys(value, [ + "kind", + "premises", + "premisesTruncated", + "relation", + "ruleId", + "ruleSha256", + "tuple", + "v" + ]) || !Array.isArray(value.premises) || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule || typeof value.premisesTruncated !== "boolean") + return null; + const ruleId = projectionName(value.ruleId); + const ruleSha256 = parseSha256Hex(value.ruleSha256); + if (ruleId === null || ruleSha256 === null || !value.premisesTruncated && value.premises.length === 0 || value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule) { + return null; + } + const skeleton = { + kind: "derived", + premises: [], + premisesTruncated: value.premisesTruncated, + relation, + ruleId, + ruleSha256, + tuple: parsedTuple, + v: 1 + }; + if (!reserveProjectionParseBytes(budget, skeleton)) + return null; + budget.nodes += 1; + const premises = []; + for (const premise of value.premises) { + const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1); + if (parsed === null) + return null; + premises.push(parsed); + } + return { ...skeleton, premises }; +} +function parseOhProjectionProofV1(value) { + try { + const budget = { + bytes: 0, + maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes, + maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth, + maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes, + nodes: 0 + }; + const proof = parseProjectionProofWithBudget(value, budget, 0); + return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null; + } catch { + return null; + } +} +function parseProjectionEvaluation(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "maximumDerivedTuples", + "maximumProofDepth", + "maximumProofNodes", + "maximumResultBytes", + "maximumRounds", + "maximumTotalProofNodes", + "maximumWorkUnits", + "v" + ]) || value.v !== 1) + return null; + const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples); + const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth); + const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes); + const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes); + const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds); + const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes); + const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits); + if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null || maximumTotalProofNodes === null || maximumWorkUnits === null) + return null; + return { + maximumDerivedTuples, + maximumProofDepth, + maximumProofNodes, + maximumResultBytes, + maximumRounds, + maximumTotalProofNodes, + maximumWorkUnits, + v: 1 + }; +} +function parseProjectionResultRow(value, evaluation, resultBudget) { + if (!isPlainRecord(value) || !hasExactKeys(value, ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1 || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals || typeof value.proofsTruncated !== "boolean") + return null; + const values = tuple(value.values); + const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches); + if (values === null || supportCount === null || !value.proofsTruncated && value.proofs.length === 0) + return null; + if (!reserveProjectionParseBytes(resultBudget, { + proofs: [], + proofsTruncated: value.proofsTruncated, + supportCount, + values, + v: 1 + })) + return null; + const before = resultBudget.nodes; + resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes); + const proofs = []; + for (const proof of value.proofs) { + const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0); + if (parsed === null) + return null; + proofs.push(parsed); + } + resultBudget.maximumNodes = evaluation.maximumTotalProofNodes; + const containsTruncation = proofs.some(proofIsTruncated); + if (!value.proofsTruncated && containsTruncation || value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals && !containsTruncation) + return null; + return { + nodes: resultBudget.nodes - before, + row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 } + }; +} +function parseOhProjectionResultV1(value, expectedProjectionSha256) { + try { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "authority", + "cache", + "engine", + "evaluation", + "identity", + "resultSha256", + "rows", + "stats", + "v" + ]) || value.v !== 1 || value.authority !== "derived" || !isPlainRecord(value.cache) || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild" || value.cache.v !== 1 || !Array.isArray(value.rows) || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats) || !hasExactKeys(value.stats, [ + "baseFacts", + "derivedFacts", + "proofNodes", + "proofsTruncated", + "queryMatches", + "relations", + "rounds", + "truncated", + "truncationReasons", + "v", + "workUnits" + ]) || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons) || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean") + return null; + const engine = safeCode(value.engine, 256); + const evaluation = parseProjectionEvaluation(value.evaluation); + const identity = parseOhProjectionIdentityV1(value.identity); + const resultSha256 = parseSha256Hex(value.resultSha256); + const expected = expectedProjectionSha256 === undefined ? undefined : parseSha256Hex(expectedProjectionSha256); + if (engine === null || evaluation === null || identity === null || resultSha256 === null || expectedProjectionSha256 !== undefined && expected === null || expected !== undefined && identity.projectionSha256 !== expected || identity.engineSha256 !== canonicalSha256({ engine, v: 1 }) || identity.evaluationSha256 !== canonicalSha256(evaluation)) + return null; + const baseFacts = nonnegativeInteger(value.stats.baseFacts); + const derivedFacts = nonnegativeInteger(value.stats.derivedFacts); + const proofNodes = nonnegativeInteger(value.stats.proofNodes); + const queryMatches = nonnegativeInteger(value.stats.queryMatches); + const relations = nonnegativeInteger(value.stats.relations); + const rounds = nonnegativeInteger(value.stats.rounds); + const workUnits = nonnegativeInteger(value.stats.workUnits); + if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations || rounds === null || rounds > evaluation.maximumRounds || workUnits === null || workUnits > evaluation.maximumWorkUnits || relations > baseFacts + derivedFacts || rounds > derivedFacts || rounds === 0 !== (derivedFacts === 0) || queryMatches > workUnits) + return null; + const truncationReasons = value.stats.truncationReasons; + if (truncationReasons.length > 2 || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x") || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes") || value.stats.truncated !== truncationReasons.length > 0) + return null; + const budget = { + bytes: 0, + maximumBytes: evaluation.maximumResultBytes, + maximumDepth: evaluation.maximumProofDepth, + maximumNodes: evaluation.maximumTotalProofNodes, + nodes: 0 + }; + const rows = []; + let supportCount = 0; + for (const row of value.rows) { + const parsed = parseProjectionResultRow(row, evaluation, budget); + if (parsed === null) + return null; + rows.push(parsed.row); + supportCount += parsed.row.supportCount; + if (supportCount > queryMatches) + return null; + } + if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated) || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches)) + return null; + const reasons = truncationReasons; + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine, + evaluation, + identity, + rows, + stats: { + baseFacts, + derivedFacts, + proofNodes, + proofsTruncated: value.stats.proofsTruncated, + queryMatches, + relations, + rounds, + truncated: value.stats.truncated, + truncationReasons: reasons, + v: 1, + workUnits + }, + v: 1 + }; + const serialized = canonicalJson(payload); + return utf8ByteLength(serialized) <= evaluation.maximumResultBytes && sha256Hex(serialized) === resultSha256 ? { ...payload, resultSha256 } : null; + } catch { + return null; + } +} +function buildProjectionResult(input) { + const matches = matchBody(input.materialized.relations, input.query.where, OH_PROJECTION_LIMITS_V1.queryMatches, input.work); + const byValues = new Map; + for (const match of matches) { + const values = input.query.find.map((name) => match.binding.get(name)); + const key = tupleKey(values); + const existing = byValues.get(key); + if (existing === undefined) + byValues.set(key, { match, supportCount: 1 }); + else + byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0 ? match : existing.match, supportCount: existing.supportCount + 1 }); + } + const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); + const resultBudget = { + bytes: 0, + maximumBytes: input.options.maximumResultBytes - 64 * 1024, + nodes: 0 + }; + const rows = []; + let resultBytesTruncated = false; + for (const [key, support] of ordered.slice(0, input.query.limit)) { + const values = JSON.parse(key); + if (!reserveResultBytes(resultBudget, { + proofs: [], + proofsTruncated: false, + supportCount: support.supportCount, + values, + v: 1 + })) { + resultBytesTruncated = true; + break; + } + const budget = { nodes: 0, result: resultBudget }; + const proofs = []; + for (const premise of support.match.premises) { + const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set); + if (proof === null) + break; + proofs.push(proof); + } + const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated); + rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 }); + } + const queryLimitTruncated = ordered.length > input.query.limit; + const truncationReasons = [ + ...queryLimitTruncated ? ["query-limit"] : [], + ...resultBytesTruncated ? ["result-bytes"] : [] + ]; + const truncated = truncationReasons.length > 0; + const identity = createOhProjectionIdentityV1({ + dataset: input.dataset, + query: input.query, + engine: input.engine, + options: input.options, + rulePack: input.rulePack, + snapshot: input.snapshot + }); + const payload = { + authority: "derived", + cache: { strategy: "full-rebuild", v: 1 }, + engine: input.engine, + evaluation: { ...input.options, v: 1 }, + identity, + rows, + stats: { + baseFacts: input.materialized.baseFacts, + derivedFacts: input.materialized.derivedFacts, + proofNodes: resultBudget.nodes, + proofsTruncated: rows.some((row) => row.proofsTruncated), + queryMatches: matches.length, + relations: input.materialized.relations.size, + rounds: input.materialized.rounds, + truncated, + truncationReasons, + v: 1, + workUnits: input.work.units + }, + v: 1 + }; + const serialized = canonicalJson(payload); + if (utf8ByteLength(serialized) > input.options.maximumResultBytes) { + throw new RangeError("Projection result exceeds its canonical byte bound."); + } + return { ...payload, resultSha256: sha256Hex(serialized) }; +} +function evaluateOhProjectionV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + if (snapshot === null || dataset === null || rulePack === null || query === null) { + throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const materialized = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + return buildProjectionResult({ + dataset, + engine: OH_PROJECTION_INTERNAL_ENGINE_V1, + materialized, + options, + query, + rulePack, + snapshot, + work + }); +} +function evaluateOhProjectionWithMaterializerV1(input) { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + const engine = safeCode(input.engine, 256); + if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) { + throw new TypeError("Invalid projection adapter input."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const witnessMaterialization = materializeNaive({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + rulePack, + work + }); + const external = input.materialize({ + dataset, + maximumDerivedTuples: options.maximumDerivedTuples, + maximumRounds: options.maximumRounds, + query, + rulePack + }); + const externalCanonical = new Map; + for (const [relationName, tuples] of external.relationFacts) { + const relation = projectionName(relationName); + if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) { + throw new TypeError("Projection adapter returned an invalid relation."); + } + const parsed = tuples.map(tuple); + if (parsed.some((value) => value === null)) + throw new TypeError("Projection adapter returned an invalid tuple."); + const keys = []; + for (const value of parsed) { + if (value === null) + throw new TypeError("Projection adapter returned an invalid tuple."); + keys.push(tupleKey(value)); + } + externalCanonical.set(relation, [...new Set(keys)].sort()); + } + const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) => [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()])); + const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort(); + for (const relation of relationNames) { + if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) { + throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`); + } + } + return buildProjectionResult({ + dataset, + engine, + materialized: witnessMaterialization, + options, + query, + rulePack, + snapshot, + work + }); +} +function createOhProjectionRecordFactsV1(records, options = {}) { + if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) + throw new RangeError("Too many records for projection facts."); + const parsedRecords = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0).map((candidate) => { + const record = parseKnowledgeGraphRecordV1(candidate); + if (record === null) + throw new TypeError("Invalid graph record for projection facts."); + return record; + }); + let projectedFactCount = 0; + for (const record of parsedRecords) { + if (options.includeRecords !== false) + projectedFactCount += 1; + if (options.includeDependencies !== false) + projectedFactCount += record.dependencies.length; + if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("Structural projection exceeds its fact bound."); + } + } + const facts = []; + for (const record of parsedRecords) { + const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 }]; + if (options.includeRecords !== false) { + facts.push(createOhProjectionFactV1({ + relation: "oh.record", + sources: source, + tuple: [record.key, record.kind, record.recordSha256] + })); + } + if (options.includeDependencies !== false) { + for (const dependency of record.dependencies) { + facts.push(createOhProjectionFactV1({ + relation: "oh.dependency", + sources: source, + tuple: [record.key, dependency] + })); + } + } + } + return facts.sort(compareProjectionFacts); +} +function isOhProjectionRecordKindV1(value) { + return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value); +} + +// src/projection-suss.ts +var OH_PROJECTION_SUSS_VERSION_V1 = "0.20.0"; +var OH_PROJECTION_SUSS_ENGINE_V1 = "suss.datalog.v0-20-0.equivalence"; +function encodeAtom(value) { + return canonicalJson(value); +} +function decodeAtom(value) { + if (typeof value !== "string") + throw new TypeError("Suss returned a non-encoded projection atom."); + const decoded = parseCanonicalJson(value, 16 * 1024); + if (decoded !== null && typeof decoded === "object") { + throw new TypeError("Suss returned a non-atomic projection value."); + } + return decoded; +} +function sussTerm(term) { + return term.kind === "constant" ? constant(encodeAtom(term.value)) : variable(term.name); +} +function sussLiteral(literal) { + return lit(literal.relation, ...literal.terms.map(sussTerm)); +} +function sussRule(input) { + return rule(input.head.relation, input.head.terms.map(sussTerm), input.body.map(sussLiteral), input.ruleId); +} +function projectionDomainSize(dataset, rulePack, query) { + const atoms = new Set; + for (const fact of dataset.facts) + for (const value of fact.tuple) + atoms.add(encodeAtom(value)); + const addTerms = (literal) => { + for (const term of literal.terms) + if (term.kind === "constant") + atoms.add(encodeAtom(term.value)); + }; + for (const item of rulePack.rules) { + addTerms(item.head); + for (const literal of item.body) + addTerms(literal); + } + for (const literal of query.where) + addTerms(literal); + return atoms.size; +} +function assertConservativeOutputBound(input) { + const domainSize = BigInt(projectionDomainSize(input.dataset, input.rulePack, input.query)); + const heads = new Map; + for (const item of input.rulePack.rules) + heads.set(item.head.relation, item.head.terms.length); + const baseCounts = new Map; + for (const fact of input.dataset.facts) { + if (heads.has(fact.relation)) + baseCounts.set(fact.relation, (baseCounts.get(fact.relation) ?? 0) + 1); + } + let possible = 0n; + const maximum = BigInt(input.maximumDerivedTuples); + for (const [relation, arity] of heads) { + const relationSpace = domainSize ** BigInt(arity); + const existing = BigInt(baseCounts.get(relation) ?? 0); + possible += relationSpace > existing ? relationSpace - existing : 0n; + if (possible > maximum) { + throw new RangeError("The Suss equivalence adapter cannot prove the requested derived-tuple bound before evaluation; use the bounded Oh evaluator."); + } + } +} +function evaluateOhProjectionWithSussV1(input) { + return evaluateOhProjectionWithMaterializerV1({ + dataset: input.dataset, + engine: OH_PROJECTION_SUSS_ENGINE_V1, + ...input.options === undefined ? {} : { options: input.options }, + query: input.query, + rulePack: input.rulePack, + snapshot: input.snapshot, + materialize: (program) => { + assertConservativeOutputBound(program); + const database = new Database; + for (const fact of program.dataset.facts) { + database.add(fact.relation, fact.tuple.map(encodeAtom)); + } + evaluate(database, program.rulePack.rules.map(sussRule)); + const relationFacts = new Map; + for (const relation of database.relationNames()) { + relationFacts.set(relation, database.facts(relation).map((values) => values.map(decodeAtom))); + } + return { relationFacts }; + } + }); +} +export { + evaluateOhProjectionWithSussV1, + OH_PROJECTION_SUSS_VERSION_V1, + OH_PROJECTION_SUSS_ENGINE_V1 +}; diff --git a/dist/projection.d.ts b/dist/projection.d.ts new file mode 100644 index 0000000..62b04a0 --- /dev/null +++ b/dist/projection.d.ts @@ -0,0 +1,315 @@ +import { type JsonPrimitive, type Sha256Hex } from "./canonical"; +import { type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordRefV1, type KnowledgeGraphRecordV1 } from "./graph"; +export declare const OH_PROJECTION_FORMAT_VERSION_V1: 1; +export declare const OH_PROJECTION_SEMANTICS_V1: "oh.projection.positive-datalog.v1"; +export declare const OH_PROJECTION_INTERNAL_ENGINE_V1: "oh.naive.positive.v1"; +export declare const OH_PROJECTION_LIMITS_V1: Readonly<{ + arity: 32; + atomBytes: number; + derivedTuples: 262144; + facts: 262144; + literalsPerRule: 64; + proofDepth: 128; + proofNodes: 4096; + queryLiterals: 64; + queryMatches: 262144; + queryResults: 65536; + relations: 4096; + resultBytes: number; + rounds: 1024; + rules: 1024; + sourcesPerFact: 64; + totalProofNodes: 65536; + variables: 256; + workUnits: 16777216; +}>; +export type OhProjectionAtomV1 = JsonPrimitive; +export type OhProjectionSnapshotV1 = Readonly<{ + contractSha256: Sha256Hex; + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordRefs: readonly KnowledgeGraphRecordRefV1[]; + recordsSha256: Sha256Hex; + sequence: number; + snapshotSha256: Sha256Hex; + spaceId: string; + v: 1; +}>; +export type OhProjectionFactSourceV1 = Readonly<{ + key: string; + recordSha256: Sha256Hex; + v: 1; +}>; +export type OhProjectionFactV1 = Readonly<{ + factSha256: Sha256Hex; + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}>; +export type OhProjectionDatasetV1 = Readonly<{ + datasetSha256: Sha256Hex; + extractorSha256: Sha256Hex; + factPackId: string; + factPackRevision: number; + factPackSha256: Sha256Hex; + facts: readonly OhProjectionFactV1[]; + factsSha256: Sha256Hex; + snapshotSha256: Sha256Hex; + v: 1; +}>; +export declare const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1: Readonly<{ + extractorSha256: Sha256Hex; + factPackId: "oh.record-facts"; + factPackRevision: 1; + relations: readonly ["oh.dependency", "oh.record"]; + semantics: "oh.projection.positive-datalog.v1"; + v: 1; +}>; +export type OhProjectionTermV1 = Readonly<{ + kind: "constant"; + v: 1; + value: OhProjectionAtomV1; +}> | Readonly<{ + kind: "variable"; + name: string; + v: 1; +}>; +export type OhProjectionLiteralV1 = Readonly<{ + relation: string; + terms: readonly OhProjectionTermV1[]; + v: 1; +}>; +export type OhProjectionRuleV1 = Readonly<{ + body: readonly OhProjectionLiteralV1[]; + head: OhProjectionLiteralV1; + ruleId: string; + ruleSha256: Sha256Hex; + v: 1; +}>; +export type OhProjectionRulePackV1 = Readonly<{ + rulePackId: string; + rulePackRevision: number; + rulePackSha256: Sha256Hex; + rules: readonly OhProjectionRuleV1[]; + rulesSha256: Sha256Hex; + semantics: typeof OH_PROJECTION_SEMANTICS_V1; + v: 1; +}>; +export type OhProjectionQueryV1 = Readonly<{ + find: readonly string[]; + limit: number; + queryId: string; + querySha256: Sha256Hex; + where: readonly OhProjectionLiteralV1[]; + v: 1; +}>; +export type OhProjectionIdentityV1 = Readonly<{ + contractSha256: Sha256Hex; + datasetSha256: Sha256Hex; + engineSha256: Sha256Hex; + evaluationSha256: Sha256Hex; + projectionSha256: Sha256Hex; + querySha256: Sha256Hex; + rulePackSha256: Sha256Hex; + semantics: typeof OH_PROJECTION_SEMANTICS_V1; + snapshotSha256: Sha256Hex; + v: 1; +}>; +export type OhProjectionProofV1 = Readonly<{ + kind: "fact"; + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}> | Readonly<{ + kind: "derived"; + premises: readonly OhProjectionProofV1[]; + premisesTruncated: boolean; + relation: string; + ruleId: string; + ruleSha256: Sha256Hex; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}> | Readonly<{ + kind: "truncated"; + reason: "cycle" | "depth" | "nodes"; + relation: string; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}>; +export type OhProjectionResultRowV1 = Readonly<{ + proofs: readonly OhProjectionProofV1[]; + proofsTruncated: boolean; + supportCount: number; + values: readonly OhProjectionAtomV1[]; + v: 1; +}>; +export type OhProjectionResultV1 = Readonly<{ + authority: "derived"; + cache: Readonly<{ + strategy: "full-rebuild"; + v: 1; + }>; + engine: string; + evaluation: Readonly<{ + maximumDerivedTuples: number; + maximumProofDepth: number; + maximumProofNodes: number; + maximumResultBytes: number; + maximumRounds: number; + maximumTotalProofNodes: number; + maximumWorkUnits: number; + v: 1; + }>; + identity: OhProjectionIdentityV1; + resultSha256: Sha256Hex; + rows: readonly OhProjectionResultRowV1[]; + stats: Readonly<{ + baseFacts: number; + derivedFacts: number; + proofNodes: number; + queryMatches: number; + relations: number; + rounds: number; + proofsTruncated: boolean; + truncated: boolean; + truncationReasons: readonly ("query-limit" | "result-bytes")[]; + workUnits: number; + v: 1; + }>; + v: 1; +}>; +export type OhProjectionEvaluationOptionsV1 = Readonly<{ + maximumDerivedTuples?: number; + maximumProofDepth?: number; + maximumProofNodes?: number; + maximumResultBytes?: number; + maximumRounds?: number; + maximumTotalProofNodes?: number; + maximumWorkUnits?: number; +}>; +export type OhProjectionInvalidationReasonV1 = "dataset-changed" | "engine-changed" | "evaluation-changed" | "query-changed" | "rule-pack-changed" | "snapshot-changed"; +export type OhProjectionInvalidationV1 = Readonly<{ + kind: "reusable"; + v: 1; +}> | Readonly<{ + kind: "full-rebuild"; + reasons: readonly OhProjectionInvalidationReasonV1[]; + v: 1; +}>; +type ProjectionHeadInputV1 = Readonly<{ + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordsSha256: Sha256Hex; + sequence: number; +}>; +export declare function createOhProjectionSnapshotV1(input: Readonly<{ + head: ProjectionHeadInputV1; + records: readonly KnowledgeGraphRecordV1[]; + spaceId: string; +}>): OhProjectionSnapshotV1; +export declare function parseOhProjectionSnapshotV1(value: unknown): OhProjectionSnapshotV1 | null; +export declare function createOhProjectionFactV1(input: Readonly<{ + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; +}>): OhProjectionFactV1; +export declare function parseOhProjectionFactV1(value: unknown): OhProjectionFactV1 | null; +export declare function createOhProjectionDatasetV1(input: Readonly<{ + extractorSha256: Sha256Hex; + factPackId: string; + factPackRevision: number; + facts: readonly OhProjectionFactV1[]; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionDatasetV1; +export declare function parseOhProjectionDatasetV1(value: unknown, snapshot: OhProjectionSnapshotV1): OhProjectionDatasetV1 | null; +export declare function ohProjectionVariableV1(name: string): OhProjectionTermV1; +export declare function ohProjectionConstantV1(value: OhProjectionAtomV1): OhProjectionTermV1; +export declare function createOhProjectionLiteralV1(input: Readonly<{ + relation: string; + terms: readonly OhProjectionTermV1[]; +}>): OhProjectionLiteralV1; +export declare function parseOhProjectionTermV1(value: unknown): OhProjectionTermV1 | null; +export declare function parseOhProjectionLiteralV1(value: unknown): OhProjectionLiteralV1 | null; +export declare function createOhProjectionRuleV1(input: Readonly<{ + body: readonly OhProjectionLiteralV1[]; + head: OhProjectionLiteralV1; + ruleId: string; +}>): OhProjectionRuleV1; +export declare function parseOhProjectionRuleV1(value: unknown): OhProjectionRuleV1 | null; +export declare function createOhProjectionRulePackV1(input: Readonly<{ + rulePackId: string; + rulePackRevision: number; + rules: readonly OhProjectionRuleV1[]; +}>): OhProjectionRulePackV1; +export declare function parseOhProjectionRulePackV1(value: unknown): OhProjectionRulePackV1 | null; +export declare function createOhProjectionQueryV1(input: Readonly<{ + find: readonly string[]; + limit?: number; + queryId: string; + where: readonly OhProjectionLiteralV1[]; +}>): OhProjectionQueryV1; +export declare function parseOhProjectionQueryV1(value: unknown): OhProjectionQueryV1 | null; +export declare function createOhProjectionIdentityV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + engine?: string; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionIdentityV1; +export declare function parseOhProjectionIdentityV1(value: unknown): OhProjectionIdentityV1 | null; +export declare function invalidationForOhProjectionV1(previous: OhProjectionIdentityV1, next: OhProjectionIdentityV1): OhProjectionInvalidationV1; +/** + * Parses one untrusted proof tree under the public hard depth, node, and byte + * ceilings. Cached result envelopes should normally be parsed as a whole with + * `parseOhProjectionResultV1`, which also applies their smaller declared limits. + */ +export declare function parseOhProjectionProofV1(value: unknown): OhProjectionProofV1 | null; +/** + * Parses a cached projection result as untrusted data. It verifies exact keys, + * all aggregate and declared bounds, proof truncation markers, canonical row + * order, engine/evaluation identity links, and `resultSha256`. Pass the + * projection digest requested from a cache to reject identity substitution. + */ +export declare function parseOhProjectionResultV1(value: unknown, expectedProjectionSha256?: Sha256Hex): OhProjectionResultV1 | null; +export declare function evaluateOhProjectionV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1; +/** + * Internal adapter seam. It is exported for package-owned optional engines, + * not as authority: callers receive the same derived-only result envelope. + */ +export declare function evaluateOhProjectionWithMaterializerV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + engine: string; + materialize: (program: Readonly<{ + dataset: OhProjectionDatasetV1; + maximumDerivedTuples: number; + maximumRounds: number; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + }>) => Readonly<{ + relationFacts: ReadonlyMap; + }>; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1; +export type OhProjectionRecordFactOptionsV1 = Readonly<{ + includeDependencies?: boolean; + includeRecords?: boolean; +}>; +/** Builds the stable structural fact layer shared by every domain fact pack. */ +export declare function createOhProjectionRecordFactsV1(records: readonly KnowledgeGraphRecordV1[], options?: OhProjectionRecordFactOptionsV1): readonly OhProjectionFactV1[]; +export declare function isOhProjectionRecordKindV1(value: unknown): value is KnowledgeGraphRecordKindV1; +export {}; +//# sourceMappingURL=projection.d.ts.map \ No newline at end of file diff --git a/dist/projection.d.ts.map b/dist/projection.d.ts.map new file mode 100644 index 0000000..eebcc9e --- /dev/null +++ b/dist/projection.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAErB,OAAO,EAKL,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC5B,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,+BAA+B,EAAG,CAAU,CAAC;AAC1D,eAAO,MAAM,0BAA0B,EAAG,mCAA4C,CAAC;AACvF,eAAO,MAAM,gCAAgC,EAAG,sBAA+B,CAAC;AAEhF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;EAmBlC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,cAAc,EAAE,SAAS,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACjD,aAAa,EAAE,SAAS,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,SAAS,CAAC;IACxB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,UAAU,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC7C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,aAAa,EAAE,SAAS,CAAC;IACzB,eAAe,EAAE,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,SAAS,CAAC;IAC1B,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,WAAW,EAAE,SAAS,CAAC;IACvB,cAAc,EAAE,SAAS,CAAC;IAC1B,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAUH,eAAO,MAAM,sCAAsC;;;;;;;EAGjD,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC,GAC/D,QAAQ,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,IAAI,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,CAAC;IACtB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,SAAS,CAAC;IAC1B,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,WAAW,EAAE,SAAS,CAAC;IACvB,SAAS,EAAE,OAAO,0BAA0B,CAAC;IAC7C,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,SAAS,CAAC;IACvB,KAAK,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACxC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,cAAc,EAAE,SAAS,CAAC;IAC1B,aAAa,EAAE,SAAS,CAAC;IACzB,YAAY,EAAE,SAAS,CAAC;IACxB,gBAAgB,EAAE,SAAS,CAAC;IAC5B,gBAAgB,EAAE,SAAS,CAAC;IAC5B,WAAW,EAAE,SAAS,CAAC;IACvB,cAAc,EAAE,SAAS,CAAC;IAC1B,SAAS,EAAE,OAAO,0BAA0B,CAAC;IAC7C,cAAc,EAAE,SAAS,CAAC;IAC1B,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC7C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GACA,QAAQ,CAAC;IACT,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,CAAC;IACtB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GACA,QAAQ,CAAC;IACT,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEL,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvC,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACtC,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,QAAQ,CAAC;QAAE,QAAQ,EAAE,cAAc,CAAC;QAAC,CAAC,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,QAAQ,CAAC;QACnB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,gBAAgB,EAAE,MAAM,CAAC;QACzB,CAAC,EAAE,CAAC,CAAC;KACN,CAAC,CAAC;IACH,QAAQ,EAAE,sBAAsB,CAAC;IACjC,YAAY,EAAE,SAAS,CAAC;IACxB,IAAI,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACzC,KAAK,EAAE,QAAQ,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,OAAO,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC;QACnB,iBAAiB,EAAE,SAAS,CAAC,aAAa,GAAG,cAAc,CAAC,EAAE,CAAC;QAC/D,SAAS,EAAE,MAAM,CAAC;QAClB,CAAC,EAAE,CAAC,CAAC;KACN,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,MAAM,gCAAgC,GACxC,iBAAiB,GACjB,gBAAgB,GAChB,oBAAoB,GACpB,eAAe,GACf,mBAAmB,GACnB,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAClC,QAAQ,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,GACpC,QAAQ,CAAC;IACT,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,SAAS,gCAAgC,EAAE,CAAC;IACrD,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEL,KAAK,qBAAqB,GAAG,QAAQ,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,SAAS,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AA8DH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC3D,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,GAAG,sBAAsB,CA0C1B;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,GAAG,IAAI,CA6BzF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC7C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACtC,CAAC,GAAG,kBAAkB,CAqBtB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAWjF;AAwBD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC1D,eAAe,EAAE,SAAS,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,GAAG,qBAAqB,CA0BzB;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EACvD,QAAQ,EAAE,sBAAsB,GAAG,qBAAqB,GAAG,IAAI,CAkBhE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAIvE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAIpF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACtC,CAAC,GAAG,qBAAqB,CAQzB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAWjF;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CASvF;AAMD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACvD,IAAI,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,GAAG,kBAAkB,CActB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAWjF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACtC,CAAC,GAAG,sBAAsB,CAY1B;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,GAAG,IAAI,CAazF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACxD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACzC,CAAC,GAAG,mBAAmB,CAiBvB;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAWnF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC3D,OAAO,EAAE,qBAAqB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,+BAA+B,CAAC;IAC1C,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,GAAG,sBAAsB,CAuB1B;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,GAAG,IAAI,CAqBzF;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,sBAAsB,EAC5E,IAAI,EAAE,sBAAsB,GAAG,0BAA0B,CAa1D;AA2WD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAWnF;AAoDD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EACtD,wBAAwB,CAAC,EAAE,SAAS,GAAG,oBAAoB,GAAG,IAAI,CAiFnE;AA0ED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACrD,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,CAAC,EAAE,+BAA+B,CAAC;IAC1C,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,GAAG,oBAAoB,CAexB;AAED;;;GAGG;AACH,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,QAAQ,CAAC;IACrE,OAAO,EAAE,qBAAqB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC;QAC9B,OAAO,EAAE,qBAAqB,CAAC;QAC/B,oBAAoB,EAAE,MAAM,CAAC;QAC7B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,mBAAmB,CAAC;QAC3B,QAAQ,EAAE,sBAAsB,CAAC;KAClC,CAAC,KAAK,QAAQ,CAAC;QAAE,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IACzF,OAAO,CAAC,EAAE,+BAA+B,CAAC;IAC1C,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAAC,GAAG,oBAAoB,CAyCxB;AAED,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC,CAAC;AAEH,gFAAgF;AAChF,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,SAAS,sBAAsB,EAAE,EACxF,OAAO,GAAE,+BAAoC,GAAG,SAAS,kBAAkB,EAAE,CAgC9E;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAE9F"} \ No newline at end of file diff --git a/dist/sdk.js b/dist/sdk.js index c3d45ee..a351bca 100644 --- a/dist/sdk.js +++ b/dist/sdk.js @@ -1024,10 +1024,13 @@ function parseOhContractManifestV1(value) { class OhRecordCodecRegistry { #codecs = new Map; + #sealed = false; register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } parse(kind, value) { @@ -1044,6 +1047,27 @@ class OhRecordCodecRegistry { has(kind) { return this.#codecs.has(kind); } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } } // src/operation.ts @@ -1346,6 +1370,573 @@ async function searchOhV1(input) { import { mkdirSync } from "fs"; import { dirname } from "path"; +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} + // src/sqlite/driver.ts import { existsSync } from "fs"; import { Database } from "bun:sqlite"; @@ -1407,9 +1998,22 @@ function withImmediateTransaction(database, work) { throw error; } } +function withReadTransaction(database, work) { + database.exec("BEGIN"); + try { + const result = work(); + database.exec("COMMIT"); + return result; + } catch (error) { + try { + database.exec("ROLLBACK"); + } catch {} + throw error; + } +} // src/sqlite/migrations.ts -var OH_SQLITE_SCHEMA_VERSION = 1; +var OH_SQLITE_SCHEMA_VERSION = 2; var OH_SQLITE_MIGRATIONS = Object.freeze([ Object.freeze({ name: "0001_oh_core", @@ -1518,6 +2122,32 @@ CREATE VIRTUAL TABLE oh_search_fts USING fts5( CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence); CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key); CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key); +` + }), + Object.freeze({ + name: "0002_store_realms", + version: 2, + sql: ` +CREATE TABLE oh_space_bindings ( + space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id), + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64), + binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64), + binding_json TEXT NOT NULL CHECK(json_valid(binding_json)), + created_at TEXT NOT NULL +) STRICT; + +CREATE TABLE oh_space_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64), + prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64), + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64), + receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json)) +) STRICT; ` }) ]); @@ -1554,27 +2184,51 @@ function applyOhSqliteMigrations(database) { } // src/sqlite/store.ts -var EMPTY_RECORDS_SHA256 = canonicalSha256([]); - -class OhConflictError extends Error { - constructor(message) { - super(message); - this.name = "OhConflictError"; +var EMPTY_RECORDS_SHA2562 = canonicalSha256([]); +var OPERATION_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +var BINDING_COLUMNS = `space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json`; +var PURGE_COLUMNS = `space_id, binding_sha256, prior_operation_sha256, prior_sequence, + purged_at, receipt_sha256, receipt_json`; +function parseStoredOperationRow(row, expected = {}) { + let value; + try { + value = JSON.parse(row.operation_json); + } catch { + throw new OhIntegrityError("A stored operation is not JSON."); + } + const operation = parseOhOperationV1(value); + if (operation === null || canonicalJson(operation) !== row.operation_json || row.operation_sha256 !== operation.operationSha256 || row.space_id !== operation.spaceId || row.sequence !== operation.sequence || row.operation_id !== operation.operationId || row.parent_operation_sha256 !== operation.parentOperationSha256 || row.graph_revision_sha256 !== operation.graphRevisionSha256 || row.records_sha256 !== operation.recordsSha256 || row.instant !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) { + throw new OhIntegrityError("Stored operation columns do not match their canonical envelope."); } + return operation; } - -class OhIntegrityError extends Error { - constructor(message) { - super(message); - this.name = "OhIntegrityError"; +function parseStoredBindingRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.binding_json); + } catch { + throw new OhIntegrityError("A store binding is not JSON."); + } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== row.binding_json || binding.spaceId !== expectedSpaceId || row.space_id !== binding.spaceId || row.realm_id !== binding.realmId || row.profile_id !== binding.profile.profileId || row.profile_kind !== binding.profile.profileKind || row.profile_sha256 !== binding.profile.profileSha256 || row.binding_sha256 !== binding.bindingSha256) { + throw new OhIntegrityError("Stored binding columns do not match their canonical envelope."); } + return binding; } - -class OhDependencyError extends Error { - constructor(message) { - super(message); - this.name = "OhDependencyError"; +function parseStoredPurgeRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.receipt_json); + } catch { + throw new OhIntegrityError("A purge receipt is not JSON."); + } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== row.receipt_json || receipt.spaceId !== expectedSpaceId || row.space_id !== receipt.spaceId || row.binding_sha256 !== receipt.bindingSha256 || row.prior_operation_sha256 !== receipt.priorHead.operationSha256 || row.prior_sequence !== receipt.priorHead.sequence || row.purged_at !== receipt.purgedAt || row.receipt_sha256 !== receipt.receiptSha256) { + throw new OhIntegrityError("Stored purge columns do not match their canonical receipt."); } + return receipt; } function parseHead(row) { const operationSha256 = row.head_operation_sha256 === null ? null : parseSha256Hex(row.head_operation_sha256); @@ -1663,6 +2317,12 @@ class OhSqliteStore { if (this.#closed) throw new Error("The Oh store is closed."); } + #assertOperationReplication() { + const binding = this.binding(); + if (binding !== null && !binding.profile.capabilities.operationReplication) { + throw new OhProfileError("This bound store profile forbids operation replication."); + } + } #registerContract() { const manifestJson = canonicalJson(OH_CONTRACT_MANIFEST_V1); this.database.query(`INSERT INTO oh_contracts(contract_id, contract_sha256, manifest_json, created_at) @@ -1674,12 +2334,45 @@ class OhSqliteStore { } ensureSpace() { this.#assertOpen(); - const now = canonicalNow(); - this.database.query(`INSERT INTO oh_spaces( - space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, - records_sha256, sequence, created_at, updated_at - ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA256, now, now); - return this.head(); + return withImmediateTransaction(this.database, () => { + const purged = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (purged !== null) { + throw new OhPurgedSpaceError(parseStoredPurgeRow(purged, this.spaceId)); + } + const now = canonicalNow(); + this.database.query(`INSERT INTO oh_spaces( + space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, + records_sha256, sequence, created_at, updated_at + ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA2562, now, now); + return this.head(); + }); + } + bind(bindingValue) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId) { + throw new OhProfileError("The store binding does not identify this space."); + } + const bindingJson = canonicalJson(binding); + this.database.query(`INSERT INTO oh_space_bindings( + space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, binding.profile.profileSha256, binding.bindingSha256, bindingJson, canonicalNow()); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + throw new OhIntegrityError("The persisted store binding disappeared."); + const persisted = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persisted) !== bindingJson) { + throw new OhProfileError("The space is already bound to a different realm or profile."); + } + return binding; + } + binding() { + this.#assertOpen(); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + return null; + return parseStoredBindingRow(row, this.spaceId); } head() { this.#assertOpen(); @@ -1741,6 +2434,48 @@ class OhSqliteStore { }); return { graphRevisionSha256, records, recordsSha256 }; } + #assertCurrentHeadAuthority(head) { + const summary = this.database.query(`SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_operations WHERE space_id = ?`).get(this.spaceId); + if (summary === null || summary.count !== head.sequence || head.sequence === 0 && (summary.minimum !== null || summary.maximum !== null) || head.sequence > 0 && (summary.minimum !== 1 || summary.maximum !== head.sequence)) { + throw new OhIntegrityError("The operation history does not exactly cover the current space head."); + } + if (head.sequence === 0) + return; + if (head.operationSha256 === null) + throw new OhIntegrityError("A nonempty space head has no operation digest."); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, head.sequence); + if (row === null) + throw new OhIntegrityError("The current space head operation is missing."); + const operation = parseStoredOperationRow(row, { + spaceId: this.spaceId, + operationSha256: head.operationSha256 + }); + if (operation.sequence !== head.sequence || operation.graphRevisionSha256 !== head.graphRevisionSha256 || operation.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The current space head differs from its canonical operation."); + } + } + #assertOperationReachable(operation, head) { + if (operation.sequence < 1 || operation.sequence > head.sequence) { + throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head."); + } + const rows = this.database.query(`SELECT operation_sha256, parent_operation_sha256, sequence + FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence); + if (rows.length !== head.sequence - operation.sequence + 1) { + throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head."); + } + let priorSha256 = operation.parentOperationSha256; + for (let index = 0;index < rows.length; index += 1) { + const row = rows[index]; + if (row === undefined || row.sequence !== operation.sequence + index || row.parent_operation_sha256 !== priorSha256 || index === 0 && row.operation_sha256 !== operation.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain."); + } + priorSha256 = row.operation_sha256; + } + if (priorSha256 !== head.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest."); + } + } #persist(operation) { this.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, @@ -1802,17 +2537,17 @@ class OhSqliteStore { if (changes.length === 0 || changes.length > 8192) throw new TypeError("A commit needs 1 through 8192 changes."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND operation_id = ?").get(this.spaceId, operationId); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operationId); if (duplicate !== null) { - const existing = parseOhOperationV1(JSON.parse(duplicate.operation_json)); - if (existing === null) - throw new OhIntegrityError("The stored idempotent operation is invalid."); + const existing = parseStoredOperationRow(duplicate, { operationId, spaceId: this.spaceId }); + this.#assertOperationReachable(existing, head); if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) { throw new OhConflictError("The operation ID is already bound to different content."); } return existing; } - const head = this.head(); if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) { throw new OhConflictError("The expected head does not match the current space head."); } @@ -1836,18 +2571,25 @@ class OhSqliteStore { } importOperation(value) { this.#assertOpen(); + this.#assertOperationReplication(); const operation = parseOhOperationV1(value); if (operation === null || operation.spaceId !== this.spaceId) throw new OhIntegrityError("Invalid imported operation."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE operation_sha256 = ?").get(operation.operationSha256); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`).get(operation.operationSha256); if (duplicate !== null) { - if (canonicalJson(JSON.parse(duplicate.operation_json)) !== canonicalJson(operation)) { + const existing = parseStoredOperationRow(duplicate, { + operationSha256: operation.operationSha256, + spaceId: this.spaceId + }); + this.#assertOperationReachable(existing, head); + if (canonicalJson(existing) !== canonicalJson(operation)) { throw new OhIntegrityError("An operation digest is bound to different bytes."); } return { imported: false, operation }; } - const head = this.head(); if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) { throw new OhConflictError("The imported operation does not extend the local head."); } @@ -1861,15 +2603,117 @@ class OhSqliteStore { } exportOperations(afterSequence = 0, limit = 1000) { this.#assertOpen(); + this.#assertOperationReplication(); if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) throw new RangeError("afterSequence must be nonnegative."); const boundedLimit = normalizeLimit(limit); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?").all(this.spaceId, afterSequence, boundedLimit); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null || canonicalJson(operation) !== row.operation_json) - throw new OhIntegrityError("A stored operation is invalid."); - return operation; + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`).all(this.spaceId, afterSequence, boundedLimit); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + } + #headAt(reference) { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) + throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) + return emptyOhHeadV1(); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, parsed.sequence); + if (row === null) + throw new OhConflictError("The requested head is not present in this space."); + const operation = parseStoredOperationRow(row, { spaceId: this.spaceId }); + if (operation.spaceId !== this.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + snapshotAtHead(options = {}) { + this.#assertOpen(); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + return withReadTransaction(this.database, () => { + const current = this.head(); + const target = options.head === undefined ? current : this.#headAt(options.head); + if (target.sequence > current.sequence) + throw new OhConflictError("The requested head is ahead of this space."); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, target.sequence); + const operations = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + const snapshot = replayOhOperationsV1(this.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Operation replay does not reproduce the requested head."); + } + return snapshot; + }); + } + changesSince(fromValue, options = {}) { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) + throw new TypeError("Invalid change-feed cursor."); + const limit = normalizeLimit(options.limit, 100, 1000); + return withReadTransaction(this.database, () => { + const current = this.head(); + const fromHead = this.#headAt(from); + const through = options.through === undefined ? current : this.#headAt(options.through); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one local history prefix."); + } + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? AND sequence <= ? + ORDER BY sequence LIMIT ?`).all(this.spaceId, fromHead.sequence, through.sequence, limit + 1); + const parsed = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (parsed.length > limit + 1) + throw new OhIntegrityError("The change feed exceeded its requested page bound."); + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior = fromHead; + for (const operation of parsed) { + if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { + from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, + operations, + through, + to, + v: 1 + }; + }); + } + exportDependencyClosure(input) { + const binding = parseOhStoreBindingV1(input.binding); + if (binding === null || binding.spaceId !== this.spaceId || canonicalJson(this.binding()) !== canonicalJson(binding)) { + throw new OhProfileError("Dependency closure export requires the exact persisted store binding."); + } + if (!binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This store profile does not permit dependency-closure export."); + } + const snapshot = this.snapshotAtHead({ + ...input.head === undefined ? {} : { head: input.head }, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords } + }); + return createOhDependencyClosureV1({ + binding, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }, + roots: input.roots, + snapshot }); } get(key) { @@ -1915,13 +2759,9 @@ class OhSqliteStore { } log(limit = 50) { this.#assertOpen(); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence DESC LIMIT ?").all(this.spaceId, normalizeLimit(limit)); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null) - throw new OhIntegrityError("The stored operation is invalid."); - return operation; - }); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? ORDER BY sequence DESC LIMIT ?`).all(this.spaceId, normalizeLimit(limit)); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); } searchKeyword(query, limit = 20) { this.#assertOpen(); @@ -1977,36 +2817,27 @@ class OhSqliteStore { if (integrity?.integrity_check !== "ok") throw new OhIntegrityError("SQLite integrity_check failed."); const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0; - const operations = storedCount <= 1000 ? this.exportOperations(0, 1000) : this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence").all(this.spaceId).map((row) => { - let value; - try { - value = JSON.parse(row.operation_json); - } catch { - throw new OhIntegrityError("A stored operation is not JSON."); - } - if (canonicalJson(value) !== row.operation_json) - throw new OhIntegrityError("A stored operation is not canonical JSON."); - const parsed = parseOhOperationV1(value); - if (parsed === null) - throw new OhIntegrityError("A stored operation is invalid."); - return parsed; - }); + const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (operations.length !== storedCount) + throw new OhIntegrityError("Operation count changed during verification."); return this.#verifyOperations(operations); } #verifyOperations(operations) { const records = new Map; const materializedBy = new Map; + const operationIds = new Set; let head = { generation: 0, graphRevisionSha256: null, operationSha256: null, - recordsSha256: EMPTY_RECORDS_SHA256, + recordsSha256: EMPTY_RECORDS_SHA2562, sequence: 0, v: 1 }; for (const operation of operations) { - if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) + if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) throw new OhIntegrityError("Operation replay chain is broken."); + operationIds.add(operation.operationId); for (const change of operation.changes) { if (change.kind === "put") { records.set(change.record.key, change.record); @@ -2083,6 +2914,66 @@ class OhSqliteStore { contract() { return { manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION }; } + purgeWorkingSpace(bindingValue, purgedAt = canonicalNow()) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + return withImmediateTransaction(this.database, () => { + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const persistedBinding = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persistedBinding) !== canonicalJson(binding)) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const receipt = createOhSpacePurgeReceiptV1({ binding, priorHead: this.head(), purgedAt }); + this.database.query(`INSERT INTO oh_space_purges(space_id, binding_sha256, + prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + VALUES (?, ?, ?, ?, ?, ?, ?)`).run(this.spaceId, binding.bindingSha256, receipt.priorHead.operationSha256, receipt.priorHead.sequence, receipt.purgedAt, receipt.receiptSha256, canonicalJson(receipt)); + this.database.query("DELETE FROM oh_search_fts WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_search_documents WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_dependencies WHERE space_id = ?").run(this.spaceId); + this.database.query(`DELETE FROM oh_operation_records WHERE operation_sha256 IN + (SELECT operation_sha256 FROM oh_operations WHERE space_id = ?)`).run(this.spaceId); + this.database.query("DELETE FROM oh_sync_outbox WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_sync_state WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_records WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_operations WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_space_bindings WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_spaces WHERE space_id = ?").run(this.spaceId); + const directTables = [ + "oh_spaces", + "oh_space_bindings", + "oh_operations", + "oh_records", + "oh_dependencies", + "oh_search_documents", + "oh_sync_outbox", + "oh_sync_state" + ]; + for (const table of directTables) { + const count = this.database.query(`SELECT count(*) AS count FROM ${table} WHERE space_id = ?`).get(this.spaceId)?.count; + if (count !== 0) + throw new OhIntegrityError(`Space purge left rows in ${table}.`); + } + const operationRecords = this.database.query(`SELECT count(*) AS count + FROM oh_operation_records AS materialized JOIN oh_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ?`).get(this.spaceId)?.count; + const searchRows = this.database.query("SELECT count(*) AS count FROM oh_search_fts WHERE space_id = ?").get(this.spaceId)?.count; + if (operationRecords !== 0 || searchRows !== 0) { + throw new OhIntegrityError("Space purge left derived private payload rows."); + } + const receiptRow = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (receiptRow === null || canonicalJson(parseStoredPurgeRow(receiptRow, this.spaceId)) !== canonicalJson(receipt)) { + throw new OhIntegrityError("The stored purge receipt differs from the requested purge."); + } + return receipt; + }); + } close() { if (this.#closed) return; diff --git a/dist/sqlite/driver.d.ts b/dist/sqlite/driver.d.ts index c1b9671..abe3d20 100644 --- a/dist/sqlite/driver.d.ts +++ b/dist/sqlite/driver.d.ts @@ -2,4 +2,5 @@ import { Database } from "bun:sqlite"; export type OhSqliteDatabase = Database; export declare function openOhSqliteDatabase(path: string): OhSqliteDatabase; export declare function withImmediateTransaction(database: OhSqliteDatabase, work: () => T): T; +export declare function withReadTransaction(database: OhSqliteDatabase, work: () => T): T; //# sourceMappingURL=driver.d.ts.map \ No newline at end of file diff --git a/dist/sqlite/driver.d.ts.map b/dist/sqlite/driver.d.ts.map index 6d1c799..ff9a790 100644 --- a/dist/sqlite/driver.d.ts.map +++ b/dist/sqlite/driver.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/sqlite/driver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;AASxC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAQnE;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAUxF"} \ No newline at end of file +{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/sqlite/driver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;AASxC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAQnE;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAUxF;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAUnF"} \ No newline at end of file diff --git a/dist/sqlite/index.d.ts b/dist/sqlite/index.d.ts index 9a68a84..a2db9b7 100644 --- a/dist/sqlite/index.d.ts +++ b/dist/sqlite/index.d.ts @@ -1,4 +1,5 @@ export * from "./driver"; export * from "./migrations"; +export * from "./port"; export * from "./store"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/sqlite/index.d.ts.map b/dist/sqlite/index.d.ts.map index c8dc181..20425a8 100644 --- a/dist/sqlite/index.d.ts.map +++ b/dist/sqlite/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sqlite/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sqlite/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC"} \ No newline at end of file diff --git a/dist/sqlite/index.js b/dist/sqlite/index.js index a03f774..e226ce4 100644 --- a/dist/sqlite/index.js +++ b/dist/sqlite/index.js @@ -60,6 +60,19 @@ function withImmediateTransaction(database, work) { throw error; } } +function withReadTransaction(database, work) { + database.exec("BEGIN"); + try { + const result = work(); + database.exec("COMMIT"); + return result; + } catch (error) { + try { + database.exec("ROLLBACK"); + } catch {} + throw error; + } +} // src/canonical.ts import { createHash, randomBytes } from "crypto"; @@ -239,7 +252,7 @@ function sortUnique(values, key) { } // src/sqlite/migrations.ts -var OH_SQLITE_SCHEMA_VERSION = 1; +var OH_SQLITE_SCHEMA_VERSION = 2; var OH_SQLITE_MIGRATIONS = Object.freeze([ Object.freeze({ name: "0001_oh_core", @@ -348,6 +361,32 @@ CREATE VIRTUAL TABLE oh_search_fts USING fts5( CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence); CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key); CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key); +` + }), + Object.freeze({ + name: "0002_store_realms", + version: 2, + sql: ` +CREATE TABLE oh_space_bindings ( + space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id), + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64), + binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64), + binding_json TEXT NOT NULL CHECK(json_valid(binding_json)), + created_at TEXT NOT NULL +) STRICT; + +CREATE TABLE oh_space_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64), + prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64), + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64), + receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json)) +) STRICT; ` }) ]); @@ -382,10 +421,6 @@ function applyOhSqliteMigrations(database) { } } } -// src/sqlite/store.ts -import { mkdirSync } from "fs"; -import { dirname } from "path"; - // src/graph.ts var OH_GRAPH_FORMAT_VERSION_V1 = 1; var OH_GRAPH_LIMITS_V1 = Object.freeze({ @@ -1233,10 +1268,13 @@ function parseOhContractManifestV1(value) { class OhRecordCodecRegistry { #codecs = new Map; + #sealed = false; register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } parse(kind, value) { @@ -1253,6 +1291,27 @@ class OhRecordCodecRegistry { has(kind) { return this.#codecs.has(kind); } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } } // src/operation.ts @@ -1321,9 +1380,7 @@ function parseOhOperationV1(value) { return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null; } -// src/sqlite/store.ts -var EMPTY_RECORDS_SHA256 = canonicalSha256([]); - +// src/store.ts class OhConflictError extends Error { constructor(message) { super(message); @@ -1344,6 +1401,601 @@ class OhDependencyError extends Error { this.name = "OhDependencyError"; } } + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} + +// src/sqlite/store.ts +import { mkdirSync } from "fs"; +import { dirname } from "path"; +var EMPTY_RECORDS_SHA2562 = canonicalSha256([]); +var OPERATION_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +var BINDING_COLUMNS = `space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json`; +var PURGE_COLUMNS = `space_id, binding_sha256, prior_operation_sha256, prior_sequence, + purged_at, receipt_sha256, receipt_json`; +function parseStoredOperationRow(row, expected = {}) { + let value; + try { + value = JSON.parse(row.operation_json); + } catch { + throw new OhIntegrityError("A stored operation is not JSON."); + } + const operation = parseOhOperationV1(value); + if (operation === null || canonicalJson(operation) !== row.operation_json || row.operation_sha256 !== operation.operationSha256 || row.space_id !== operation.spaceId || row.sequence !== operation.sequence || row.operation_id !== operation.operationId || row.parent_operation_sha256 !== operation.parentOperationSha256 || row.graph_revision_sha256 !== operation.graphRevisionSha256 || row.records_sha256 !== operation.recordsSha256 || row.instant !== operation.instant || expected.spaceId !== undefined && operation.spaceId !== expected.spaceId || expected.operationId !== undefined && operation.operationId !== expected.operationId || expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256) { + throw new OhIntegrityError("Stored operation columns do not match their canonical envelope."); + } + return operation; +} +function parseStoredBindingRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.binding_json); + } catch { + throw new OhIntegrityError("A store binding is not JSON."); + } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== row.binding_json || binding.spaceId !== expectedSpaceId || row.space_id !== binding.spaceId || row.realm_id !== binding.realmId || row.profile_id !== binding.profile.profileId || row.profile_kind !== binding.profile.profileKind || row.profile_sha256 !== binding.profile.profileSha256 || row.binding_sha256 !== binding.bindingSha256) { + throw new OhIntegrityError("Stored binding columns do not match their canonical envelope."); + } + return binding; +} +function parseStoredPurgeRow(row, expectedSpaceId) { + let value; + try { + value = JSON.parse(row.receipt_json); + } catch { + throw new OhIntegrityError("A purge receipt is not JSON."); + } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== row.receipt_json || receipt.spaceId !== expectedSpaceId || row.space_id !== receipt.spaceId || row.binding_sha256 !== receipt.bindingSha256 || row.prior_operation_sha256 !== receipt.priorHead.operationSha256 || row.prior_sequence !== receipt.priorHead.sequence || row.purged_at !== receipt.purgedAt || row.receipt_sha256 !== receipt.receiptSha256) { + throw new OhIntegrityError("Stored purge columns do not match their canonical receipt."); + } + return receipt; +} function parseHead(row) { const operationSha256 = row.head_operation_sha256 === null ? null : parseSha256Hex(row.head_operation_sha256); const graphRevisionSha256 = row.graph_revision_sha256 === null ? null : parseSha256Hex(row.graph_revision_sha256); @@ -1431,6 +2083,12 @@ class OhSqliteStore { if (this.#closed) throw new Error("The Oh store is closed."); } + #assertOperationReplication() { + const binding = this.binding(); + if (binding !== null && !binding.profile.capabilities.operationReplication) { + throw new OhProfileError("This bound store profile forbids operation replication."); + } + } #registerContract() { const manifestJson = canonicalJson(OH_CONTRACT_MANIFEST_V1); this.database.query(`INSERT INTO oh_contracts(contract_id, contract_sha256, manifest_json, created_at) @@ -1442,12 +2100,45 @@ class OhSqliteStore { } ensureSpace() { this.#assertOpen(); - const now = canonicalNow(); - this.database.query(`INSERT INTO oh_spaces( - space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, - records_sha256, sequence, created_at, updated_at - ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA256, now, now); - return this.head(); + return withImmediateTransaction(this.database, () => { + const purged = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (purged !== null) { + throw new OhPurgedSpaceError(parseStoredPurgeRow(purged, this.spaceId)); + } + const now = canonicalNow(); + this.database.query(`INSERT INTO oh_spaces( + space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, + records_sha256, sequence, created_at, updated_at + ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA2562, now, now); + return this.head(); + }); + } + bind(bindingValue) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId) { + throw new OhProfileError("The store binding does not identify this space."); + } + const bindingJson = canonicalJson(binding); + this.database.query(`INSERT INTO oh_space_bindings( + space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run(this.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, binding.profile.profileSha256, binding.bindingSha256, bindingJson, canonicalNow()); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + throw new OhIntegrityError("The persisted store binding disappeared."); + const persisted = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persisted) !== bindingJson) { + throw new OhProfileError("The space is already bound to a different realm or profile."); + } + return binding; + } + binding() { + this.#assertOpen(); + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) + return null; + return parseStoredBindingRow(row, this.spaceId); } head() { this.#assertOpen(); @@ -1509,6 +2200,48 @@ class OhSqliteStore { }); return { graphRevisionSha256, records, recordsSha256 }; } + #assertCurrentHeadAuthority(head) { + const summary = this.database.query(`SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_operations WHERE space_id = ?`).get(this.spaceId); + if (summary === null || summary.count !== head.sequence || head.sequence === 0 && (summary.minimum !== null || summary.maximum !== null) || head.sequence > 0 && (summary.minimum !== 1 || summary.maximum !== head.sequence)) { + throw new OhIntegrityError("The operation history does not exactly cover the current space head."); + } + if (head.sequence === 0) + return; + if (head.operationSha256 === null) + throw new OhIntegrityError("A nonempty space head has no operation digest."); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, head.sequence); + if (row === null) + throw new OhIntegrityError("The current space head operation is missing."); + const operation = parseStoredOperationRow(row, { + spaceId: this.spaceId, + operationSha256: head.operationSha256 + }); + if (operation.sequence !== head.sequence || operation.graphRevisionSha256 !== head.graphRevisionSha256 || operation.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The current space head differs from its canonical operation."); + } + } + #assertOperationReachable(operation, head) { + if (operation.sequence < 1 || operation.sequence > head.sequence) { + throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head."); + } + const rows = this.database.query(`SELECT operation_sha256, parent_operation_sha256, sequence + FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, operation.sequence, head.sequence); + if (rows.length !== head.sequence - operation.sequence + 1) { + throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head."); + } + let priorSha256 = operation.parentOperationSha256; + for (let index = 0;index < rows.length; index += 1) { + const row = rows[index]; + if (row === undefined || row.sequence !== operation.sequence + index || row.parent_operation_sha256 !== priorSha256 || index === 0 && row.operation_sha256 !== operation.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain."); + } + priorSha256 = row.operation_sha256; + } + if (priorSha256 !== head.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest."); + } + } #persist(operation) { this.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, @@ -1570,17 +2303,17 @@ class OhSqliteStore { if (changes.length === 0 || changes.length > 8192) throw new TypeError("A commit needs 1 through 8192 changes."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND operation_id = ?").get(this.spaceId, operationId); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`).get(this.spaceId, operationId); if (duplicate !== null) { - const existing = parseOhOperationV1(JSON.parse(duplicate.operation_json)); - if (existing === null) - throw new OhIntegrityError("The stored idempotent operation is invalid."); + const existing = parseStoredOperationRow(duplicate, { operationId, spaceId: this.spaceId }); + this.#assertOperationReachable(existing, head); if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) { throw new OhConflictError("The operation ID is already bound to different content."); } return existing; } - const head = this.head(); if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) { throw new OhConflictError("The expected head does not match the current space head."); } @@ -1604,18 +2337,25 @@ class OhSqliteStore { } importOperation(value) { this.#assertOpen(); + this.#assertOperationReplication(); const operation = parseOhOperationV1(value); if (operation === null || operation.spaceId !== this.spaceId) throw new OhIntegrityError("Invalid imported operation."); return withImmediateTransaction(this.database, () => { - const duplicate = this.database.query("SELECT operation_json FROM oh_operations WHERE operation_sha256 = ?").get(operation.operationSha256); + const head = this.head(); + this.#assertCurrentHeadAuthority(head); + const duplicate = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`).get(operation.operationSha256); if (duplicate !== null) { - if (canonicalJson(JSON.parse(duplicate.operation_json)) !== canonicalJson(operation)) { + const existing = parseStoredOperationRow(duplicate, { + operationSha256: operation.operationSha256, + spaceId: this.spaceId + }); + this.#assertOperationReachable(existing, head); + if (canonicalJson(existing) !== canonicalJson(operation)) { throw new OhIntegrityError("An operation digest is bound to different bytes."); } return { imported: false, operation }; } - const head = this.head(); if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) { throw new OhConflictError("The imported operation does not extend the local head."); } @@ -1629,15 +2369,117 @@ class OhSqliteStore { } exportOperations(afterSequence = 0, limit = 1000) { this.#assertOpen(); + this.#assertOperationReplication(); if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) throw new RangeError("afterSequence must be nonnegative."); const boundedLimit = normalizeLimit(limit); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?").all(this.spaceId, afterSequence, boundedLimit); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null || canonicalJson(operation) !== row.operation_json) - throw new OhIntegrityError("A stored operation is invalid."); - return operation; + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`).all(this.spaceId, afterSequence, boundedLimit); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + } + #headAt(reference) { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) + throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) + return emptyOhHeadV1(); + const row = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`).get(this.spaceId, parsed.sequence); + if (row === null) + throw new OhConflictError("The requested head is not present in this space."); + const operation = parseStoredOperationRow(row, { spaceId: this.spaceId }); + if (operation.spaceId !== this.spaceId || operation.sequence !== parsed.sequence || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { + generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256: operation.recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + snapshotAtHead(options = {}) { + this.#assertOpen(); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + return withReadTransaction(this.database, () => { + const current = this.head(); + const target = options.head === undefined ? current : this.#headAt(options.head); + if (target.sequence > current.sequence) + throw new OhConflictError("The requested head is ahead of this space."); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence <= ? ORDER BY sequence`).all(this.spaceId, target.sequence); + const operations = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + const snapshot = replayOhOperationsV1(this.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Operation replay does not reproduce the requested head."); + } + return snapshot; + }); + } + changesSince(fromValue, options = {}) { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) + throw new TypeError("Invalid change-feed cursor."); + const limit = normalizeLimit(options.limit, 100, 1000); + return withReadTransaction(this.database, () => { + const current = this.head(); + const fromHead = this.#headAt(from); + const through = options.through === undefined ? current : this.#headAt(options.through); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one local history prefix."); + } + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? AND sequence <= ? + ORDER BY sequence LIMIT ?`).all(this.spaceId, fromHead.sequence, through.sequence, limit + 1); + const parsed = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (parsed.length > limit + 1) + throw new OhIntegrityError("The change feed exceeded its requested page bound."); + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior = fromHead; + for (const operation of parsed) { + if (operation.sequence !== prior.sequence + 1 || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { + from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, + operations, + through, + to, + v: 1 + }; + }); + } + exportDependencyClosure(input) { + const binding = parseOhStoreBindingV1(input.binding); + if (binding === null || binding.spaceId !== this.spaceId || canonicalJson(this.binding()) !== canonicalJson(binding)) { + throw new OhProfileError("Dependency closure export requires the exact persisted store binding."); + } + if (!binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This store profile does not permit dependency-closure export."); + } + const snapshot = this.snapshotAtHead({ + ...input.head === undefined ? {} : { head: input.head }, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords } + }); + return createOhDependencyClosureV1({ + binding, + ...input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }, + roots: input.roots, + snapshot }); } get(key) { @@ -1683,13 +2525,9 @@ class OhSqliteStore { } log(limit = 50) { this.#assertOpen(); - const rows = this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence DESC LIMIT ?").all(this.spaceId, normalizeLimit(limit)); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null) - throw new OhIntegrityError("The stored operation is invalid."); - return operation; - }); + const rows = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? ORDER BY sequence DESC LIMIT ?`).all(this.spaceId, normalizeLimit(limit)); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); } searchKeyword(query, limit = 20) { this.#assertOpen(); @@ -1745,36 +2583,27 @@ class OhSqliteStore { if (integrity?.integrity_check !== "ok") throw new OhIntegrityError("SQLite integrity_check failed."); const storedCount = this.database.query("SELECT count(*) AS count FROM oh_operations WHERE space_id = ?").get(this.spaceId)?.count ?? 0; - const operations = storedCount <= 1000 ? this.exportOperations(0, 1000) : this.database.query("SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence").all(this.spaceId).map((row) => { - let value; - try { - value = JSON.parse(row.operation_json); - } catch { - throw new OhIntegrityError("A stored operation is not JSON."); - } - if (canonicalJson(value) !== row.operation_json) - throw new OhIntegrityError("A stored operation is not canonical JSON."); - const parsed = parseOhOperationV1(value); - if (parsed === null) - throw new OhIntegrityError("A stored operation is invalid."); - return parsed; - }); + const operations = this.database.query(`SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (operations.length !== storedCount) + throw new OhIntegrityError("Operation count changed during verification."); return this.#verifyOperations(operations); } #verifyOperations(operations) { const records = new Map; const materializedBy = new Map; + const operationIds = new Set; let head = { generation: 0, graphRevisionSha256: null, operationSha256: null, - recordsSha256: EMPTY_RECORDS_SHA256, + recordsSha256: EMPTY_RECORDS_SHA2562, sequence: 0, v: 1 }; for (const operation of operations) { - if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) + if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) throw new OhIntegrityError("Operation replay chain is broken."); + operationIds.add(operation.operationId); for (const change of operation.changes) { if (change.kind === "put") { records.set(change.record.key, change.record); @@ -1851,6 +2680,66 @@ class OhSqliteStore { contract() { return { manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION }; } + purgeWorkingSpace(bindingValue, purgedAt = canonicalNow()) { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + return withImmediateTransaction(this.database, () => { + const row = this.database.query(`SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`).get(this.spaceId); + if (row === null) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const persistedBinding = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persistedBinding) !== canonicalJson(binding)) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const receipt = createOhSpacePurgeReceiptV1({ binding, priorHead: this.head(), purgedAt }); + this.database.query(`INSERT INTO oh_space_purges(space_id, binding_sha256, + prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + VALUES (?, ?, ?, ?, ?, ?, ?)`).run(this.spaceId, binding.bindingSha256, receipt.priorHead.operationSha256, receipt.priorHead.sequence, receipt.purgedAt, receipt.receiptSha256, canonicalJson(receipt)); + this.database.query("DELETE FROM oh_search_fts WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_search_documents WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_dependencies WHERE space_id = ?").run(this.spaceId); + this.database.query(`DELETE FROM oh_operation_records WHERE operation_sha256 IN + (SELECT operation_sha256 FROM oh_operations WHERE space_id = ?)`).run(this.spaceId); + this.database.query("DELETE FROM oh_sync_outbox WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_sync_state WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_records WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_operations WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_space_bindings WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_spaces WHERE space_id = ?").run(this.spaceId); + const directTables = [ + "oh_spaces", + "oh_space_bindings", + "oh_operations", + "oh_records", + "oh_dependencies", + "oh_search_documents", + "oh_sync_outbox", + "oh_sync_state" + ]; + for (const table of directTables) { + const count = this.database.query(`SELECT count(*) AS count FROM ${table} WHERE space_id = ?`).get(this.spaceId)?.count; + if (count !== 0) + throw new OhIntegrityError(`Space purge left rows in ${table}.`); + } + const operationRecords = this.database.query(`SELECT count(*) AS count + FROM oh_operation_records AS materialized JOIN oh_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ?`).get(this.spaceId)?.count; + const searchRows = this.database.query("SELECT count(*) AS count FROM oh_search_fts WHERE space_id = ?").get(this.spaceId)?.count; + if (operationRecords !== 0 || searchRows !== 0) { + throw new OhIntegrityError("Space purge left derived private payload rows."); + } + const receiptRow = this.database.query(`SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`).get(this.spaceId); + if (receiptRow === null || canonicalJson(parseStoredPurgeRow(receiptRow, this.spaceId)) !== canonicalJson(receipt)) { + throw new OhIntegrityError("The stored purge receipt differs from the requested purge."); + } + return receipt; + }); + } close() { if (this.#closed) return; @@ -1858,11 +2747,91 @@ class OhSqliteStore { this.#closed = true; } } + +// src/sqlite/port.ts +class OhSqliteStorePortV1 { + binding; + #authority; + constructor(authority, binding) { + const persisted = authority.bind(binding); + if (canonicalJson(persisted) !== canonicalJson(binding)) { + throw new OhProfileError("The SQLite authority returned a different store binding."); + } + this.#authority = authority; + this.binding = persisted; + } + async head() { + return this.#authority.head(); + } + async snapshot(options = {}) { + return this.#authority.snapshotAtHead(options); + } + async changesSince(from, options = {}) { + return this.#authority.changesSince(from, options); + } + async commit(input) { + return this.#authority.commit(input); + } + async exportDependencyClosure(input) { + return this.#authority.exportDependencyClosure({ binding: this.binding, ...input }); + } + async verify() { + const verified = this.#authority.verifyReplay(); + return { + head: verified.head, + integrity: "verified", + operations: verified.operations, + records: verified.records, + v: 1 + }; + } + async close() { + this.#authority.close(); + } +} +function createOhSqliteStoreAuthorityV1(options = {}) { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) + throw new TypeError("Invalid SQLite store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ + profile, + realmId: options.realmId ?? `realm:${spaceId}`, + spaceId, + v: 1 + }); + const authority = new OhSqliteStore({ + ...options.database === undefined ? {} : { database: options.database }, + ...options.path === undefined ? {} : { path: options.path }, + spaceId + }); + const store = new OhSqliteStorePortV1(authority, binding); + let purge = null; + const host = Object.freeze({ + binding, + purgeWorkingSpace: async (input) => { + if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("This host handle is not bound to a purgeable working profile."); + } + if (purge !== null) + return purge; + purge = authority.purgeWorkingSpace(binding, input.purgedAt); + authority.close(); + return purge; + } + }); + return Object.freeze({ host, store }); +} export { + withReadTransaction, withImmediateTransaction, openOhSqliteDatabase, + createOhSqliteStoreAuthorityV1, applyOhSqliteMigrations, + OhSqliteStorePortV1, OhSqliteStore, + OhPurgedSpaceError, + OhProfileError, OhIntegrityError, OhDependencyError, OhConflictError, diff --git a/dist/sqlite/migrations.d.ts b/dist/sqlite/migrations.d.ts index 9c75854..659b320 100644 --- a/dist/sqlite/migrations.d.ts +++ b/dist/sqlite/migrations.d.ts @@ -1,5 +1,5 @@ import type { OhSqliteDatabase } from "./driver"; -export declare const OH_SQLITE_SCHEMA_VERSION: 1; +export declare const OH_SQLITE_SCHEMA_VERSION: 2; export type OhSqliteMigration = Readonly<{ name: string; sql: string; diff --git a/dist/sqlite/migrations.d.ts.map b/dist/sqlite/migrations.d.ts.map index b822e96..075fd85 100644 --- a/dist/sqlite/migrations.d.ts.map +++ b/dist/sqlite/migrations.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,eAAO,MAAM,wBAAwB,EAAG,CAAU,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzF,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EA8G3D,CAAC;AAEH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAgCxE"} \ No newline at end of file +{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,eAAO,MAAM,wBAAwB,EAAG,CAAU,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzF,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAwI3D,CAAC;AAEH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAgCxE"} \ No newline at end of file diff --git a/dist/sqlite/port.d.ts b/dist/sqlite/port.d.ts new file mode 100644 index 0000000..2a8afc8 --- /dev/null +++ b/dist/sqlite/port.d.ts @@ -0,0 +1,40 @@ +import { type OhChangesPageV1, type OhCommitInputV1, type OhDependencyClosureV1, type OhHeadRefV1, type OhHeadV1, type OhSnapshotV1, type OhStoreAuthorityV1, type OhStoreBindingV1, type OhStoreProfileV1, type OhStoreV1, type OhStoreVerificationV1 } from "../store"; +import type { OhOperationV1 } from "../operation"; +import type { OhSqliteDatabase } from "./driver"; +import { OhSqliteStore } from "./store"; +export type OhSqliteStoreAuthorityOptionsV1 = Readonly<{ + database?: OhSqliteDatabase; + path?: string; + profile?: OhStoreProfileV1; + realmId?: string; + spaceId?: string; +}>; +export declare class OhSqliteStorePortV1 implements OhStoreV1 { + #private; + readonly binding: OhStoreBindingV1; + constructor(authority: OhSqliteStore, binding: OhStoreBindingV1); + head(): Promise; + snapshot(options?: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }>): Promise; + changesSince(from: OhHeadRefV1, options?: Readonly<{ + limit?: number; + through?: OhHeadRefV1; + }>): Promise; + commit(input: OhCommitInputV1): Promise; + exportDependencyClosure(input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): Promise; + verify(): Promise; + close(): Promise; +} +/** + * Binds a Bun SQLite authority to the promise-based store port. Retain the + * returned `host` object in trusted control-plane code; pass only `store` to + * ordinary consumers. + */ +export declare function createOhSqliteStoreAuthorityV1(options?: OhSqliteStoreAuthorityOptionsV1): OhStoreAuthorityV1; +//# sourceMappingURL=port.d.ts.map \ No newline at end of file diff --git a/dist/sqlite/port.d.ts.map b/dist/sqlite/port.d.ts.map new file mode 100644 index 0000000..622f172 --- /dev/null +++ b/dist/sqlite/port.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/sqlite/port.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,YAAY,EAEjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EAErB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC;IACrD,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,qBAAa,mBAAoB,YAAW,SAAS;;IACnD,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAGvB,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,gBAAgB;IASzD,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC;IAIzB,QAAQ,CAAC,OAAO,GAAE,QAAQ,CAAC;QAC/B,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIzB,YAAY,CAChB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,QAAQ,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,CAAM,GAChE,OAAO,CAAC,eAAe,CAAC;IAIrB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;IAItD,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC;QAC5C,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;KAC1B,CAAC,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAI7B,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAMxC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,OAAO,GAAE,+BAAoC,GAC5C,kBAAkB,CA0BpB"} \ No newline at end of file diff --git a/dist/sqlite/store.d.ts b/dist/sqlite/store.d.ts index e0416fb..3dc2da5 100644 --- a/dist/sqlite/store.d.ts +++ b/dist/sqlite/store.d.ts @@ -1,32 +1,11 @@ import { type Sha256Hex } from "../canonical"; import { OH_CONTRACT_MANIFEST_V1 } from "../contract"; -import { type KnowledgeGraphChangeV1, type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "../graph"; +import { type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "../graph"; import { type OhOperationV1 } from "../operation"; +import { OhConflictError, OhDependencyError, OhIntegrityError, OhProfileError, OhPurgedSpaceError, type OhChangesPageV1, type OhCommitInputV1, type OhDependencyClosureV1, type OhHeadRefV1, type OhHeadV1, type OhSnapshotV1, type OhSpacePurgeReceiptV1, type OhStoreBindingV1 } from "../store"; import { type OhSqliteDatabase } from "./driver"; -export declare class OhConflictError extends Error { - constructor(message: string); -} -export declare class OhIntegrityError extends Error { - constructor(message: string); -} -export declare class OhDependencyError extends Error { - constructor(message: string); -} -export type OhHeadV1 = Readonly<{ - generation: number; - graphRevisionSha256: Sha256Hex | null; - operationSha256: Sha256Hex | null; - recordsSha256: Sha256Hex; - sequence: number; - v: 1; -}>; -export type OhCommitInputV1 = Readonly<{ - actorId: string; - changes: readonly KnowledgeGraphChangeV1[]; - expectedHead: Pick; - instant?: string; - operationId: string; -}>; +export { OhConflictError, OhDependencyError, OhIntegrityError, OhProfileError, OhPurgedSpaceError, }; +export type { OhCommitInputV1, OhHeadV1 }; export type OhRecordListOptions = Readonly<{ kind?: KnowledgeGraphRecordKindV1; limit?: number; @@ -56,6 +35,8 @@ export declare class OhSqliteStore { spaceId?: string; }>); ensureSpace(): OhHeadV1; + bind(bindingValue: OhStoreBindingV1): OhStoreBindingV1; + binding(): OhStoreBindingV1 | null; head(): OhHeadV1; commit(input: OhCommitInputV1): OhOperationV1; importOperation(value: unknown): Readonly<{ @@ -63,6 +44,20 @@ export declare class OhSqliteStore { operation: OhOperationV1; }>; exportOperations(afterSequence?: number, limit?: number): readonly OhOperationV1[]; + snapshotAtHead(options?: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }>): OhSnapshotV1; + changesSince(fromValue: OhHeadRefV1, options?: Readonly<{ + limit?: number; + through?: OhHeadRefV1; + }>): OhChangesPageV1; + exportDependencyClosure(input: Readonly<{ + binding: OhStoreBindingV1; + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): OhDependencyClosureV1; get(key: string): KnowledgeGraphRecordV1 | null; list(options?: OhRecordListOptions): readonly KnowledgeGraphRecordV1[]; snapshotRecords(maximum?: number): readonly KnowledgeGraphRecordV1[]; @@ -83,6 +78,8 @@ export declare class OhSqliteStore { manifest: typeof OH_CONTRACT_MANIFEST_V1; sqliteSchemaVersion: number; }>; + /** Host control-plane primitive. Do not expose this method through agent tools. */ + purgeWorkingSpace(bindingValue: OhStoreBindingV1, purgedAt?: string): OhSpacePurgeReceiptV1; close(): void; } //# sourceMappingURL=store.d.ts.map \ No newline at end of file diff --git a/dist/sqlite/store.d.ts.map b/dist/sqlite/store.d.ts.map index 85bedab..6e19c09 100644 --- a/dist/sqlite/store.d.ts.map +++ b/dist/sqlite/store.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC5B,MAAM,UAAU,CAAC;AAElB,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAkD,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjG,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAC5B;AACD,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAC5B;AACD,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,SAAS,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,iBAAiB,CAAC,CAAC;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,CAAC,EAAE,0BAA0B,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,0BAA0B,CAAC;IACjC,YAAY,EAAE,SAAS,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAuEH,qBAAa,aAAa;;IACxB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGb,OAAO,GAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,gBAAgB,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAM;IAiCpG,WAAW,IAAI,QAAQ;IAWvB,IAAI,IAAI,QAAQ;IAsHhB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,aAAa;IAkC7C,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC;IA4B1F,gBAAgB,CAAC,aAAa,SAAI,EAAE,KAAK,SAAO,GAAG,SAAS,aAAa,EAAE;IAc3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAa/C,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,SAAS,sBAAsB,EAAE;IAmB1E,eAAe,CAAC,OAAO,GAAE,MAA8C,GAAG,SAAS,sBAAsB,EAAE;IAoB3G,GAAG,CAAC,KAAK,SAAK,GAAG,SAAS,aAAa,EAAE;IAYzC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,SAAS,uBAAuB,EAAE;IAoB5E,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAY7H,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI;IAahJ,YAAY,IAAI,sBAAsB;IAoGtC,QAAQ,IAAI,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,uBAAuB,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE,CAAC;IAI/F,KAAK,IAAI,IAAI;CAKd"} \ No newline at end of file +{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAML,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC5B,MAAM,UAAU,CAAC;AAElB,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAIL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAKlB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAK1C,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC;AAE1C,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,CAAC,EAAE,0BAA0B,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,0BAA0B,CAAC;IACjC,YAAY,EAAE,SAAS,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAoKH,qBAAa,aAAa;;IACxB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGb,OAAO,GAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,gBAAgB,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAM;IAwCpG,WAAW,IAAI,QAAQ;IAmBvB,IAAI,CAAC,YAAY,EAAE,gBAAgB,GAAG,gBAAgB;IAyBtD,OAAO,IAAI,gBAAgB,GAAG,IAAI;IASlC,IAAI,IAAI,QAAQ;IA0KhB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,aAAa;IAmC7C,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC;IAiC1F,gBAAgB,CAAC,aAAa,SAAI,EAAE,KAAK,SAAO,GAAG,SAAS,aAAa,EAAE;IA8B3E,cAAc,CAAC,OAAO,GAAE,QAAQ,CAAC;QAC/B,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAM,GAAG,YAAY;IAyBtB,YAAY,CACV,SAAS,EAAE,WAAW,EACtB,OAAO,GAAE,QAAQ,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,CAAM,GAChE,eAAe;IA0ClB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC;QACtC,OAAO,EAAE,gBAAgB,CAAC;QAC1B,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;KAC1B,CAAC,GAAG,qBAAqB;IAgB1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAa/C,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,SAAS,sBAAsB,EAAE;IAmB1E,eAAe,CAAC,OAAO,GAAE,MAA8C,GAAG,SAAS,sBAAsB,EAAE;IAoB3G,GAAG,CAAC,KAAK,SAAK,GAAG,SAAS,aAAa,EAAE;IASzC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,SAAS,uBAAuB,EAAE;IAoB5E,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAY7H,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI;IAahJ,YAAY,IAAI,sBAAsB;IA+FtC,QAAQ,IAAI,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,uBAAuB,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE,CAAC;IAI/F,mFAAmF;IACnF,iBAAiB,CAAC,YAAY,EAAE,gBAAgB,EAAE,QAAQ,GAAE,MAAuB,GAAG,qBAAqB;IAiE3G,KAAK,IAAI,IAAI;CAKd"} \ No newline at end of file diff --git a/dist/store.d.ts b/dist/store.d.ts new file mode 100644 index 0000000..bbac4ab --- /dev/null +++ b/dist/store.d.ts @@ -0,0 +1,236 @@ +import { type Sha256Hex } from "./canonical"; +import { OhRecordCodecRegistry } from "./contract"; +export { OhRecordCodecRegistry } from "./contract"; +import { type KnowledgeGraphChangeV1, type KnowledgeGraphRecordKindV1, type KnowledgeGraphRecordV1 } from "./graph"; +import { type OhOperationV1 } from "./operation"; +export declare class OhConflictError extends Error { + constructor(message: string); +} +export declare class OhIntegrityError extends Error { + constructor(message: string); +} +export declare class OhDependencyError extends Error { + constructor(message: string); +} +export declare class OhProfileError extends Error { + constructor(message: string); +} +export type OhHeadV1 = Readonly<{ + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordsSha256: Sha256Hex; + sequence: number; + v: 1; +}>; +export type OhHeadRefV1 = Pick; +export type OhCommitInputV1 = Readonly<{ + actorId: string; + changes: readonly KnowledgeGraphChangeV1[]; + expectedHead: Pick; + instant?: string; + operationId: string; +}>; +export type OhSnapshotV1 = Readonly<{ + head: OhHeadV1; + records: readonly KnowledgeGraphRecordV1[]; + v: 1; +}>; +export type OhChangesPageV1 = Readonly<{ + from: OhHeadRefV1; + hasMore: boolean; + operations: readonly OhOperationV1[]; + through: OhHeadV1; + to: OhHeadRefV1; + v: 1; +}>; +export type OhStoreVerificationV1 = Readonly<{ + head: OhHeadV1; + integrity: "verified"; + operations: number; + records: number; + v: 1; +}>; +export type OhStoreCapabilitiesV1 = Readonly<{ + changesSince: true; + dependencyClosureExport: true; + exactSnapshots: true; + operationReplication: boolean; + semanticBundleCommit: true; + v: 1; + wholeSpacePurge: boolean; +}>; +export type OhStoreProfileV1 = Readonly<{ + applicationProfileSha256: Sha256Hex | null; + capabilities: OhStoreCapabilitiesV1; + profileId: string; + profileKind: "canonical" | "working"; + profileSha256: Sha256Hex; + v: 1; +}>; +export type OhStoreBindingV1 = Readonly<{ + bindingSha256: Sha256Hex; + contractSha256: Sha256Hex; + profile: OhStoreProfileV1; + realmId: string; + spaceId: string; + v: 1; +}>; +export declare const OH_CANONICAL_STORE_PROFILE_V1: Readonly<{ + applicationProfileSha256: Sha256Hex | null; + capabilities: OhStoreCapabilitiesV1; + profileId: string; + profileKind: "canonical" | "working"; + profileSha256: Sha256Hex; + v: 1; +}>; +export declare const OH_WORKING_STORE_PROFILE_V1: Readonly<{ + applicationProfileSha256: Sha256Hex | null; + capabilities: OhStoreCapabilitiesV1; + profileId: string; + profileKind: "canonical" | "working"; + profileSha256: Sha256Hex; + v: 1; +}>; +export type OhDependencyClosureV1 = Readonly<{ + binding: OhStoreBindingV1; + closureSha256: Sha256Hex; + head: OhHeadV1; + records: readonly KnowledgeGraphRecordV1[]; + roots: readonly string[]; + v: 1; +}>; +export declare const OH_DEPENDENCY_CLOSURE_LIMITS_V1: Readonly<{ + bytes: number; + records: 8192; + roots: 1024; +}>; +export type OhSpacePurgeReceiptV1 = Readonly<{ + bindingSha256: Sha256Hex; + priorHead: OhHeadV1; + purgedAt: string; + receiptSha256: Sha256Hex; + spaceId: string; + v: 1; +}>; +export declare class OhPurgedSpaceError extends Error { + readonly receipt: OhSpacePurgeReceiptV1; + constructor(receipt: OhSpacePurgeReceiptV1); +} +export interface OhStoreV1 { + readonly binding: OhStoreBindingV1; + changesSince(from: OhHeadRefV1, options?: Readonly<{ + limit?: number; + through?: OhHeadRefV1; + }>): Promise; + close(): Promise; + commit(input: OhCommitInputV1): Promise; + exportDependencyClosure(input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): Promise; + head(): Promise; + snapshot(options?: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }>): Promise; + verify(): Promise; +} +/** Kept separate so an agent-facing store object never carries deletion authority. */ +export interface OhStoreHostControlV1 { + readonly binding: OhStoreBindingV1; + purgeWorkingSpace(input: Readonly<{ + purgedAt?: string; + }>): Promise; +} +export type OhStoreAuthorityV1 = Readonly<{ + host: OhStoreHostControlV1; + store: OhStoreV1; +}>; +export declare function emptyOhHeadV1(): OhHeadV1; +export declare function parseOhHeadV1(value: unknown): OhHeadV1 | null; +export declare function parseOhHeadRefV1(value: unknown): OhHeadRefV1 | null; +type OhStoreProfileInputV1 = Omit; +export declare function createOhStoreProfileV1(input: OhStoreProfileInputV1): OhStoreProfileV1; +export declare function parseOhStoreProfileV1(value: unknown): OhStoreProfileV1 | null; +export declare function createOhStoreBindingV1(input: Readonly<{ + profile: OhStoreProfileV1; + realmId: string; + spaceId: string; + v: 1; +}>): OhStoreBindingV1; +export declare function parseOhStoreBindingV1(value: unknown): OhStoreBindingV1 | null; +export declare function replayOhOperationsV1(spaceId: string, values: readonly OhOperationV1[], maximumRecords?: number): OhSnapshotV1; +export declare function transitionOhSnapshotV1(input: Readonly<{ + actorId: string; + changes: readonly KnowledgeGraphChangeV1[]; + instant: string; + operationId: string; + snapshot: OhSnapshotV1; + spaceId: string; +}>): Readonly<{ + operation: OhOperationV1; + snapshot: OhSnapshotV1; +}>; +export declare function createOhDependencyClosureV1(input: Readonly<{ + binding: OhStoreBindingV1; + maximumRecords?: number; + roots: readonly string[]; + snapshot: OhSnapshotV1; +}>): OhDependencyClosureV1; +export declare function parseOhDependencyClosureV1(value: unknown): OhDependencyClosureV1 | null; +export declare function verifyOhDependencyClosureV1(value: unknown): Readonly<{ + closure: OhDependencyClosureV1; + ok: true; +}> | Readonly<{ + ok: false; + reason: "invalid-closure"; +}>; +/** + * Strong adoption check. Unlike structural self-verification, this also binds + * the capsule to the exact store binding and head selected by trusted host code. + */ +export declare function verifyOhDependencyClosureAgainstV1(value: unknown, expected: Readonly<{ + binding: OhStoreBindingV1; + head: OhHeadV1; +}>): Readonly<{ + closure: OhDependencyClosureV1; + ok: true; + verification: "expected-authority-and-head"; +}> | Readonly<{ + ok: false; + reason: "binding-mismatch" | "head-mismatch" | "invalid-closure" | "invalid-expectation"; +}>; +export declare function createOhSpacePurgeReceiptV1(input: Readonly<{ + binding: OhStoreBindingV1; + priorHead: OhHeadV1; + purgedAt: string; +}>): OhSpacePurgeReceiptV1; +export declare function parseOhSpacePurgeReceiptV1(value: unknown): OhSpacePurgeReceiptV1 | null; +export type OhSemanticBundleV1 = Readonly<{ + actorId: string; + expectedHead: Pick; + instant: string | null; + operationId: string; + puts: readonly Readonly<{ + dependencies: readonly string[]; + key: string; + kind: KnowledgeGraphRecordKindV1; + v: 1; + value: unknown; + }>[]; + tombstones: readonly Readonly<{ + key: string; + priorSha256: Sha256Hex; + v: 1; + }>[]; + v: 1; +}>; +/** A strict model-facing ingress: every put must have a registered codec. */ +export declare class OhSemanticBundleIngressV1 { + #private; + constructor(store: OhStoreV1, codecs: OhRecordCodecRegistry); + commit(value: unknown): Promise; +} +//# sourceMappingURL=store.d.ts.map \ No newline at end of file diff --git a/dist/store.d.ts.map b/dist/store.d.ts.map new file mode 100644 index 0000000..367ef4f --- /dev/null +++ b/dist/store.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,qBAAqB,EACtB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAQL,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC5B,MAAM,SAAS,CAAC;AACjB,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAErB,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAC5B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAC5B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAC5B;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,SAAS,GAAG,IAAI,CAAC;IACtC,eAAe,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,SAAS,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,iBAAiB,GAAG,UAAU,CAAC,CAAC;AAEzE,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,iBAAiB,CAAC,CAAC;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;IACrC,OAAO,EAAE,QAAQ,CAAC;IAClB,EAAE,EAAE,WAAW,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,YAAY,EAAE,IAAI,CAAC;IACnB,uBAAuB,EAAE,IAAI,CAAC;IAC9B,cAAc,EAAE,IAAI,CAAC;IACrB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,oBAAoB,EAAE,IAAI,CAAC;IAC3B,CAAC,EAAE,CAAC,CAAC;IACL,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,wBAAwB,EAAE,SAAS,GAAG,IAAI,CAAC;IAC3C,YAAY,EAAE,qBAAqB,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;IACrC,aAAa,EAAE,SAAS,CAAC;IACzB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,aAAa,EAAE,SAAS,CAAC;IACzB,cAAc,EAAE,SAAS,CAAC;IAC1B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,eAAO,MAAM,6BAA6B;8BAjBd,SAAS,GAAG,IAAI;kBAC5B,qBAAqB;eACxB,MAAM;iBACJ,WAAW,GAAG,SAAS;mBACrB,SAAS;OACrB,CAAC;EA0BJ,CAAC;AAEH,eAAO,MAAM,2BAA2B;8BAjCZ,SAAS,GAAG,IAAI;kBAC5B,qBAAqB;eACxB,MAAM;iBACJ,WAAW,GAAG,SAAS;mBACrB,SAAS;OACrB,CAAC;EA0CJ,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,SAAS,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,eAAO,MAAM,+BAA+B;;;;EAI1C,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,aAAa,EAAE,SAAS,CAAC;IACzB,SAAS,EAAE,QAAQ,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,SAAS,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;gBAE5B,OAAO,EAAE,qBAAqB;CAK3C;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,YAAY,CACV,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,QAAQ,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,CAAC,GAC5D,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACvD,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC;QACtC,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;KAC1B,CAAC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC;QAC1B,IAAI,CAAC,EAAE,WAAW,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3B,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC1C;AAED,sFAAsF;AACtF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC3F;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,KAAK,EAAE,SAAS,CAAC;CAClB,CAAC,CAAC;AAIH,wBAAgB,aAAa,IAAI,QAAQ,CASxC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAoB7D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAYnE;AAcD,KAAK,qBAAqB,GAAG,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;AAErE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,CAsBrF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACrD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,GAAG,gBAAgB,CAUpB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAY7E;AAcD,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,cAAc,GAAE,MAA8C,GAC7D,YAAY,CA4Cd;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC,CAsDlE;AAsCD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC1D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,YAAY,CAAC;CACxB,CAAC,GAAG,qBAAqB,CAwBzB;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAyBvF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GACtD,QAAQ,CAAC;IAAE,OAAO,EAAE,qBAAqB,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,GACtD,QAAQ,CAAC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAGrD;AAED;;;GAGG;AACH,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACpF,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC,GACE,QAAQ,CAAC;IAAE,OAAO,EAAE,qBAAqB,CAAC;IAAC,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,6BAA6B,CAAA;CAAE,CAAC,GACnG,QAAQ,CAAC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,GAAG,eAAe,GAAG,iBAAiB,GAAG,qBAAqB,CAAA;CAAE,CAAC,CASpH;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC1D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,EAAE,QAAQ,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,GAAG,qBAAqB,CAYzB;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAavF;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,iBAAiB,CAAC,CAAC;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,QAAQ,CAAC;QACtB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;QAChC,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,0BAA0B,CAAC;QACjC,CAAC,EAAE,CAAC,CAAC;QACL,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC,EAAE,CAAC;IACL,UAAU,EAAE,SAAS,QAAQ,CAAC;QAC5B,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,EAAE,SAAS,CAAC;QACvB,CAAC,EAAE,CAAC,CAAC;KACN,CAAC,EAAE,CAAC;IACL,CAAC,EAAE,CAAC,CAAC;CACN,CAAC,CAAC;AAEH,6EAA6E;AAC7E,qBAAa,yBAAyB;;gBAIxB,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB;IAKrD,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC;CA6CrD"} \ No newline at end of file diff --git a/dist/store.js b/dist/store.js new file mode 100644 index 0000000..18e912a --- /dev/null +++ b/dist/store.js @@ -0,0 +1,996 @@ +// src/canonical.ts +import { createHash, randomBytes } from "node:crypto"; + +class OhValidationError extends Error { + code; + path; + constructor(code, path, message) { + super(`${path}: ${message}`); + this.name = "OhValidationError"; + this.code = code; + this.path = path; + } +} +function isPlainRecord(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return false; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} +function hasExactKeys(value, keys) { + const actual = Object.keys(value); + return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)); +} +function assertUnicodeScalarString(value, path) { + for (let index = 0;index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(index + 1); + if (!(next >= 56320 && next <= 57343)) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + index += 1; + } else if (code >= 56320 && code <= 57343) { + throw new OhValidationError("invalid-unicode", path, "contains an unpaired surrogate"); + } + } +} +function encodeCanonical(value, path, ancestors) { + if (value === null || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "string") { + assertUnicodeScalarString(value, path); + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new OhValidationError("non-json-number", path, "must be finite"); + } + if (Object.is(value, -0)) { + throw new OhValidationError("noncanonical-number", path, "negative zero is not canonical"); + } + return JSON.stringify(value); + } + if (typeof value !== "object" || value === null) { + throw new OhValidationError("non-json-value", path, `cannot encode ${typeof value}`); + } + if (ancestors.has(value)) { + throw new OhValidationError("cycle", path, "contains a cycle"); + } + ancestors.add(value); + try { + if (Array.isArray(value)) { + const encoded = []; + for (let index = 0;index < value.length; index += 1) { + if (!Object.hasOwn(value, index)) { + throw new OhValidationError("sparse-array", `${path}[${index}]`, "must not contain holes"); + } + encoded.push(encodeCanonical(value[index], `${path}[${index}]`, ancestors)); + } + const extraKeys = Reflect.ownKeys(value).filter((key) => key !== "length" && (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key) || Number(key) >= value.length)); + if (extraKeys.length > 0) { + throw new OhValidationError("non-json-property", path, "array has non-index properties"); + } + return `[${encoded.join(",")}]`; + } + if (!isPlainRecord(value)) { + throw new OhValidationError("non-plain-object", path, "must be a plain object"); + } + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== "string")) { + throw new OhValidationError("non-json-property", path, "object has a symbol property"); + } + const keys = ownKeys; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === undefined || !descriptor.enumerable || descriptor.get !== undefined || descriptor.set !== undefined) { + throw new OhValidationError("non-json-property", `${path}.${key}`, "must be an enumerable data property"); + } + } + keys.sort(); + const entries = keys.map((key) => { + assertUnicodeScalarString(key, `${path}.`); + return `${JSON.stringify(key)}:${encodeCanonical(value[key], `${path}.${key}`, ancestors)}`; + }); + return `{${entries.join(",")}}`; + } finally { + ancestors.delete(value); + } +} +function canonicalJson(value) { + return encodeCanonical(value, "$", new Set); +} +function parseCanonicalJson(text, maximumBytes = 16 * 1024 * 1024) { + if (utf8ByteLength(text) > maximumBytes) { + throw new OhValidationError("limit-exceeded", "$", "canonical JSON exceeds its byte limit"); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw new OhValidationError("invalid-json", "$", "is not valid JSON"); + } + if (canonicalJson(value) !== text) { + throw new OhValidationError("noncanonical-json", "$", "keys or values are not canonical"); + } + return value; +} +function utf8ByteLength(value) { + return Buffer.byteLength(value, "utf8"); +} +function sha256Hex(value) { + return createHash("sha256").update(value).digest("hex"); +} +function canonicalSha256(value) { + return sha256Hex(canonicalJson(value)); +} +function parseSha256Hex(value) { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : null; +} +function parseCanonicalInstantV1(value) { + if (typeof value !== "string" || !/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/u.test(value)) { + return null; + } + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value ? value : null; +} +function canonicalNow() { + return new Date().toISOString(); +} +function safeCode(value, maximumLength = 128) { + return typeof value === "string" && value.length <= maximumLength && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function orderedUnique(values, key) { + return values.every((value, index) => index === 0 || key(values[index - 1]) < key(value)); +} +function sortUnique(values, key) { + const sorted = [...values].sort((left, right) => { + const leftKey = key(left); + const rightKey = key(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; + }); + if (!orderedUnique(sorted, key)) { + throw new OhValidationError("duplicate", "$", "contains duplicate canonical values"); + } + return sorted; +} + +// src/graph.ts +var OH_GRAPH_FORMAT_VERSION_V1 = 1; +var OH_GRAPH_LIMITS_V1 = Object.freeze({ + changesPerOperation: 8192, + dependenciesPerRecord: 4096, + recordBytes: 1024 * 1024, + recordsPerSnapshot: 65536 +}); +var OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1 = [ + "activity", + "assertion", + "context", + "dependency-manifest", + "edition", + "entity", + "evidence", + "identity-operation", + "inquiry", + "inquiry-event", + "review-decision", + "rights-decision", + "schema", + "shape", + "statement", + "type-membership", + "view", + "vocabulary" +]; +function recordKey(value) { + return typeof value === "string" && value.length <= 512 && /^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$/u.test(value) ? value : null; +} +function createKnowledgeGraphRecordV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, ["dependencies", "key", "kind", "v", "value"]) || input.v !== 1 || !Array.isArray(input.dependencies)) + throw new TypeError("Invalid graph record input."); + const key = recordKey(input.key); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === input.kind); + if (key === null || kind === undefined || input.dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord) + throw new TypeError("Invalid graph record identity."); + const dependencies = input.dependencies.map(recordKey); + if (dependencies.some((dependency) => dependency === null) || !orderedUnique(dependencies, String) || dependencies.includes(key)) { + throw new TypeError("Graph dependencies must be ordered, unique, and non-reflexive."); + } + const valueJson = canonicalJson(input.value); + if (Buffer.byteLength(valueJson, "utf8") > OH_GRAPH_LIMITS_V1.recordBytes) { + throw new RangeError("Graph record value exceeds its canonical byte limit."); + } + const payload = { dependencies, key, kind, v: 1, value: input.value }; + return { ...payload, recordSha256: canonicalSha256(payload) }; +} +function parseKnowledgeGraphRecordV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "recordSha256")) + return null; + const recordSha256 = parseSha256Hex(value.recordSha256); + const { recordSha256: _digest, ...input } = value; + try { + const created = createKnowledgeGraphRecordV1(input); + return recordSha256 !== null && created.recordSha256 === recordSha256 ? { ...created, recordSha256 } : null; + } catch { + return null; + } +} +function knowledgeGraphRecordRefV1(record) { + return { + dependencies: record.dependencies, + key: record.key, + kind: record.kind, + sha256: record.recordSha256, + v: 1 + }; +} +function changeKey(change) { + return change.kind === "put" ? change.record.key : change.key; +} +function canonicalKnowledgeGraphChangesV1(changes) { + const normalized = []; + for (const change of changes) { + if (!isPlainRecord(change) || change.v !== 1) + throw new TypeError("Invalid graph change."); + if (change.kind === "put") { + const record = parseKnowledgeGraphRecordV1(change.record); + if (record === null) + throw new TypeError("Invalid graph record in change."); + normalized.push({ kind: "put", record, v: 1 }); + } else if (change.kind === "tombstone") { + const key = recordKey(change.key); + const priorSha256 = parseSha256Hex(change.priorSha256); + if (key === null || priorSha256 === null) + throw new TypeError("Invalid graph tombstone."); + normalized.push({ key, kind: "tombstone", priorSha256, v: 1 }); + } else + throw new TypeError("Unknown graph change kind."); + } + return sortUnique(normalized, changeKey); +} +function graphRevisionSha256V1(input) { + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + const operationId = safeCode(input.operationId); + const parentGraphRevisionSha256 = input.parentGraphRevisionSha256 === null ? null : parseSha256Hex(input.parentGraphRevisionSha256); + const recordsSha256 = parseSha256Hex(input.recordsSha256); + const revision = Number.isSafeInteger(input.revision) && input.revision > 0 ? input.revision : null; + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation || operationId === null || recordsSha256 === null || revision === null || input.parentGraphRevisionSha256 !== null && parentGraphRevisionSha256 === null || revision === 1 !== (parentGraphRevisionSha256 === null)) { + throw new TypeError("Invalid graph revision digest input."); + } + return canonicalSha256({ changes, operationId, parentGraphRevisionSha256, recordsSha256, revision, v: 1 }); +} + +// src/ontology.ts +var OH_ONTOLOGY_VERSION_V1 = "1.0.0"; +var OH_CONTRACT_ID_V1 = "oh.ontology.v1"; +var OH_KNOWLEDGE_LIMITS_V1 = Object.freeze({ + dimensions: 64, + listValues: 256, + qualifiers: 128, + statementBytes: 256 * 1024, + textBytes: 64 * 1024 +}); + +// src/schema.ts +var OH_SCHEMA_FORMAT_VERSION_V1 = 1; + +// src/contract.ts +var manifestPayload = Object.freeze({ + contractId: OH_CONTRACT_ID_V1, + graphFormatVersion: OH_GRAPH_FORMAT_VERSION_V1, + ontologyVersion: OH_ONTOLOGY_VERSION_V1, + recordKinds: OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + schemaFormatVersion: OH_SCHEMA_FORMAT_VERSION_V1, + v: 1 +}); +var OH_CONTRACT_MANIFEST_V1 = Object.freeze({ + ...manifestPayload, + contractSha256: canonicalSha256(manifestPayload) +}); +class OhRecordCodecRegistry { + #codecs = new Map; + #sealed = false; + register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); + if (this.#codecs.has(codec.kind)) + throw new TypeError(`A codec is already registered for ${codec.kind}.`); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); + return this; + } + parse(kind, value) { + const codec = this.#codecs.get(kind); + if (codec !== undefined) + return codec.parse(value); + try { + canonicalJson(value); + return value; + } catch { + return null; + } + } + has(kind) { + return this.#codecs.has(kind); + } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } +} +// src/operation.ts +var OH_OPERATION_MAX_BYTES_V1 = 64 * 1024 * 1024; +function parsePayload(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "changes", + "contractId", + "graphRevisionSha256", + "instant", + "operationId", + "parentOperationSha256", + "recordsSha256", + "sequence", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractId !== OH_CONTRACT_ID_V1 || !Array.isArray(value.changes)) + return null; + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const spaceId = safeCode(value.spaceId); + const graphRevisionSha256 = parseSha256Hex(value.graphRevisionSha256); + const parentOperationSha256 = value.parentOperationSha256 === null ? null : parseSha256Hex(value.parentOperationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const instant = parseCanonicalInstantV1(value.instant); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence > 0 ? value.sequence : null; + let changes; + try { + changes = canonicalKnowledgeGraphChangesV1(value.changes); + } catch { + return null; + } + if (changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) + return null; + return actorId !== null && operationId !== null && spaceId !== null && graphRevisionSha256 !== null && recordsSha256 !== null && instant !== null && sequence !== null && (value.parentOperationSha256 === null || parentOperationSha256 !== null) && sequence === 1 === (parentOperationSha256 === null) ? { + actorId, + changes, + contractId: OH_CONTRACT_ID_V1, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256, + recordsSha256, + sequence, + spaceId, + v: 1 + } : null; +} +function createOhOperationV1(input) { + const payload = parsePayload(input); + if (payload === null) + throw new TypeError("Invalid Oh operation payload."); + const operation = { ...payload, operationSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(operation), "utf8") > OH_OPERATION_MAX_BYTES_V1) { + throw new RangeError("Oh operation exceeds its canonical byte limit."); + } + return operation; +} +function parseOhOperationV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "operationSha256")) + return null; + const operationSha256 = parseSha256Hex(value.operationSha256); + const { operationSha256: _digest, ...input } = value; + const payload = parsePayload(input); + return operationSha256 !== null && payload !== null && Buffer.byteLength(canonicalJson({ ...payload, operationSha256 }), "utf8") <= OH_OPERATION_MAX_BYTES_V1 && canonicalSha256(payload) === operationSha256 ? { ...payload, operationSha256 } : null; +} + +// src/store.ts +class OhConflictError extends Error { + constructor(message) { + super(message); + this.name = "OhConflictError"; + } +} + +class OhIntegrityError extends Error { + constructor(message) { + super(message); + this.name = "OhIntegrityError"; + } +} + +class OhDependencyError extends Error { + constructor(message) { + super(message); + this.name = "OhDependencyError"; + } +} + +class OhProfileError extends Error { + constructor(message) { + super(message); + this.name = "OhProfileError"; + } +} +var OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1 +}); +var OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1 +}); +var OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8192, + roots: 1024 +}); + +class OhPurgedSpaceError extends Error { + receipt; + constructor(receipt) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} +var EMPTY_RECORDS_SHA256 = canonicalSha256([]); +function emptyOhHeadV1() { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1 + }; +} +function parseOhHeadV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "generation", + "graphRevisionSha256", + "operationSha256", + "recordsSha256", + "sequence", + "v" + ]) || value.v !== 1) + return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && value.generation >= 0 ? value.generation : null; + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return generation !== null && sequence !== null && generation === sequence && recordsSha256 !== null && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) && sequence === 0 === (graphRevisionSha256 === null) ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } : null; +} +function parseOhHeadRefV1(value) { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) + return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && value.sequence >= 0 ? value.sequence : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) && sequence === 0 === (operationSha256 === null) ? { operationSha256, sequence } : null; +} +function parseCapabilities(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "changesSince", + "dependencyClosureExport", + "exactSnapshots", + "operationReplication", + "semanticBundleCommit", + "v", + "wholeSpacePurge" + ]) || value.changesSince !== true || value.dependencyClosureExport !== true || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" || value.semanticBundleCommit !== true || value.v !== 1 || typeof value.wholeSpacePurge !== "boolean") + return null; + return { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: value.operationReplication, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: value.wholeSpacePurge + }; +} +function createOhStoreProfileV1(input) { + if (!isPlainRecord(input) || !hasExactKeys(input, [ + "applicationProfileSha256", + "capabilities", + "profileId", + "profileKind", + "v" + ]) || input.v !== 1) + throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null || input.applicationProfileSha256 !== null && applicationProfileSha256 === null || input.profileKind !== "canonical" && input.profileKind !== "working") { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { + applicationProfileSha256, + capabilities: Object.freeze(capabilities), + profileId, + profileKind: input.profileKind, + v: 1 + }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} +function parseOhStoreProfileV1(value) { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) + return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { + return null; + } +} +function createOhStoreBindingV1(input) { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, + realmId, + spaceId, + v: 1 + }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} +function parseOhStoreBindingV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "contractSha256", + "profile", + "realmId", + "spaceId", + "v" + ]) || value.v !== 1 || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) + return null; + const created = createOhStoreBindingV1({ + profile, + realmId: value.realmId, + spaceId: value.spaceId, + v: 1 + }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { + return null; + } +} +function sortedRecords(records) { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} +function verifyDependencies(records) { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) + throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} +function replayOhOperationsV1(spaceId, values, maximumRecords = OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map; + const operationIds = new Set; + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId || operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256 || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) + throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes: operation.changes, + operationId: operation.operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: operation.sequence + }); + if (recordsSha256 !== operation.recordsSha256 || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} +function transitionOhSnapshotV1(input) { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") + records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ + changes, + operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, + revision: head.sequence + 1 + }); + const operation = createOhOperationV1({ + actorId, + changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, + graphRevisionSha256, + instant, + operationId, + parentOperationSha256: head.operationSha256, + recordsSha256, + sequence: head.sequence + 1, + spaceId, + v: 1 + }); + const nextHead = { + generation: operation.sequence, + graphRevisionSha256, + operationSha256: operation.operationSha256, + recordsSha256, + sequence: operation.sequence, + v: 1 + }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} +function normalizeRoots(values) { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) + throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} +function closureRecords(available, roots, maximumRecords, maximumBytes = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024) { + const selected = new Map; + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop(); + if (selected.has(key)) + continue; + const record = available.get(key); + if (record === undefined) + throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) + throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} +function createOhDependencyClosureV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map; + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) + throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) + throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} +function parseOhDependencyClosureV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "binding", + "closureSha256", + "head", + "records", + "roots", + "v" + ]) || value.v !== 1 || !Array.isArray(value.records) || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) + return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) + return null; + const records = new Map; + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) + return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots); + if (canonicalJson(roots) !== canonicalJson(value.roots)) + return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) + return null; + const payload = { binding, head, records: exact, roots, v: 1 }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes ? Object.freeze(parsed) : null; + } catch { + return null; + } +} +function verifyOhDependencyClosureV1(value) { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} +function verifyOhDependencyClosureAgainstV1(value, expected) { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) + return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) + return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) + return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) + return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} +function createOhSpacePurgeReceiptV1(input) { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null || binding.profile.profileKind !== "working" || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { + bindingSha256: binding.bindingSha256, + priorHead, + purgedAt, + spaceId: binding.spaceId, + v: 1 + }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} +function parseOhSpacePurgeReceiptV1(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "bindingSha256", + "priorHead", + "purgedAt", + "receiptSha256", + "spaceId", + "v" + ]) || value.v !== 1) + return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null || receiptSha256 === null || spaceId === null) + return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 }; + return canonicalSha256(payload) === receiptSha256 ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +class OhSemanticBundleIngressV1 { + #codecs; + #store; + constructor(store, codecs) { + this.#store = store; + this.#codecs = codecs.seal(); + } + async commit(value) { + if (!isPlainRecord(value) || !hasExactKeys(value, [ + "actorId", + "expectedHead", + "instant", + "operationId", + "puts", + "tombstones", + "v" + ]) || value.v !== 1 || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) || value.puts.length + value.tombstones.length < 1 || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) || !hasExactKeys(expected, ["generation", "operationSha256"]) || !Number.isSafeInteger(expected.generation) || expected.generation < 0 || expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null || expected.generation === 0 !== (expected.operationSha256 === null) || value.instant !== null && instant === null) + throw new TypeError("Invalid semantic bundle identity."); + const changes = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) || item.v !== 1 || !Array.isArray(item.dependencies) || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind, item.value); + if (parsed === null) + throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ + dependencies: item.dependencies, + key: item.key, + kind: item.kind, + v: 1, + value: parsed + }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) + throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ + actorId, + changes: canonical, + expectedHead: { + generation: expected.generation, + operationSha256: expected.operationSha256 + }, + ...typeof instant === "string" ? { instant } : {}, + operationId + }); + } +} +export { + verifyOhDependencyClosureV1, + verifyOhDependencyClosureAgainstV1, + transitionOhSnapshotV1, + replayOhOperationsV1, + parseOhStoreProfileV1, + parseOhStoreBindingV1, + parseOhSpacePurgeReceiptV1, + parseOhHeadV1, + parseOhHeadRefV1, + parseOhDependencyClosureV1, + emptyOhHeadV1, + createOhStoreProfileV1, + createOhStoreBindingV1, + createOhSpacePurgeReceiptV1, + createOhDependencyClosureV1, + OhSemanticBundleIngressV1, + OhRecordCodecRegistry, + OhPurgedSpaceError, + OhProfileError, + OhIntegrityError, + OhDependencyError, + OhConflictError, + OH_WORKING_STORE_PROFILE_V1, + OH_DEPENDENCY_CLOSURE_LIMITS_V1, + OH_CANONICAL_STORE_PROFILE_V1 +}; diff --git a/dist/sync.js b/dist/sync.js index 5cf3394..a02e5fa 100644 --- a/dist/sync.js +++ b/dist/sync.js @@ -1024,10 +1024,13 @@ function parseOhContractManifestV1(value) { class OhRecordCodecRegistry { #codecs = new Map; + #sealed = false; register(codec) { + if (this.#sealed) + throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } parse(kind, value) { @@ -1044,6 +1047,27 @@ class OhRecordCodecRegistry { has(kind) { return this.#codecs.has(kind); } + parseRequired(kind, value) { + const codec = this.#codecs.get(kind); + if (codec === undefined) + return null; + try { + const parsed = codec.parse(value); + if (parsed === null) + return null; + canonicalJson(parsed); + return parsed; + } catch { + return null; + } + } + seal() { + this.#sealed = true; + return this; + } + get sealed() { + return this.#sealed; + } } // src/operation.ts diff --git a/package.json b/package.json index 503d90e..7fb0051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/oh", - "version": "0.1.1", + "version": "0.2.0", "description": "open-source tools for agentic research", "type": "module", "license": "MIT", @@ -23,7 +23,8 @@ "sideEffects": false, "packageManager": "bun@1.3.14", "engines": { - "bun": ">=1.3.14" + "bun": ">=1.3.14", + "node": ">=24" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -39,6 +40,14 @@ "types": "./dist/sdk.d.ts", "import": "./dist/sdk.js" }, + "./store": { + "types": "./dist/store.d.ts", + "import": "./dist/store.js" + }, + "./libsql": { + "types": "./dist/libsql.d.ts", + "import": "./dist/libsql.js" + }, "./sqlite": { "types": "./dist/sqlite/index.d.ts", "import": "./dist/sqlite/index.js" @@ -51,6 +60,18 @@ "types": "./dist/semantic.d.ts", "import": "./dist/semantic.js" }, + "./projection": { + "types": "./dist/projection-public.d.ts", + "import": "./dist/projection-public.js" + }, + "./experimental/projection-suss": { + "types": "./dist/projection-suss.d.ts", + "import": "./dist/projection-suss.js" + }, + "./experimental/memory": { + "types": "./dist/memory.d.ts", + "import": "./dist/memory.js" + }, "./package.json": "./package.json" }, "files": [ @@ -65,25 +86,34 @@ "access": "public" }, "scripts": { - "build": "bun run build:js && bun run build:types", + "build": "bun run build:js && bun run build:portable && bun run build:types", "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", + "build:portable": "bun build ./src/store.ts ./src/libsql.ts ./src/projection-public.ts ./src/projection-suss.ts ./src/memory.ts --outdir ./dist --target node --format esm --external @suss/datalog", "build:types": "tsc -p tsconfig.build.json", - "check": "bun run typecheck && bun run test && bun run build", + "check": "bun run typecheck && bun run test && bun run build && bun run test:types:node && bun run test:node && bun run test:node-projection", + "test:node": "node ./tests/node-portable.mjs", + "test:types:node": "tsc -p tsconfig.node-portable.json --noEmit", "test": "bun test ./src ./tests ./site/tests/source.test.ts", + "test:node-projection": "node --test ./scripts/projection-node.test.mjs", "typecheck": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { + "@suss/datalog": "0.20.0", "@types/bun": "1.3.14", "typescript": "5.9.3" }, "peerDependencies": { "@libsql/client": ">=0.17.4 <1", + "@suss/datalog": "0.20.0", "@tobilu/qmd": "2.5.3" }, "peerDependenciesMeta": { "@libsql/client": { "optional": true }, + "@suss/datalog": { + "optional": true + }, "@tobilu/qmd": { "optional": true } diff --git a/scripts/projection-node.test.mjs b/scripts/projection-node.test.mjs new file mode 100644 index 0000000..d691155 --- /dev/null +++ b/scripts/projection-node.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { test } from "node:test"; + +import * as projectionSurface from "../dist/projection-public.js"; + +import { + createOhProjectionDatasetV1, + createOhProjectionLiteralV1, + createOhProjectionQueryV1, + createOhProjectionRulePackV1, + createOhProjectionRuleV1, + createOhProjectionSnapshotV1, + evaluateOhProjectionV1, + ohProjectionVariableV1, + parseOhProjectionProofV1, + parseOhProjectionResultV1, +} from "../dist/projection-public.js"; + +const sha256 = (value) => createHash("sha256").update(value).digest("hex"); + +test("the projection subpath runs under Node without loading the SQLite runtime", async () => { + const snapshot = createOhProjectionSnapshotV1({ + head: { generation: 0, graphRevisionSha256: null, operationSha256: null, + recordsSha256: sha256("[]"), sequence: 0 }, + records: [], + spaceId: "node.serverless", + }); + const dataset = createOhProjectionDatasetV1({ extractorSha256: "a".repeat(64), + factPackId: "node.empty", factPackRevision: 1, facts: [], snapshot }); + const x = ohProjectionVariableV1("x"); + const base = createOhProjectionLiteralV1({ relation: "base", terms: [x] }); + const derived = createOhProjectionLiteralV1({ relation: "derived", terms: [x] }); + const rulePack = createOhProjectionRulePackV1({ rulePackId: "node.rules", rulePackRevision: 1, + rules: [createOhProjectionRuleV1({ body: [base], head: derived, ruleId: "derived.from-base" })] }); + const query = createOhProjectionQueryV1({ find: ["x"], queryId: "node.query", where: [derived] }); + const result = evaluateOhProjectionV1({ dataset, query, rulePack, snapshot }); + assert.deepEqual(result.rows, []); + assert.equal(result.authority, "derived"); + assert.deepEqual(parseOhProjectionResultV1(result), result); + assert.equal(parseOhProjectionProofV1({ unexpected: true }), null); + assert.equal(Object.hasOwn(projectionSurface, "evaluateOhProjectionWithMaterializerV1"), false); + + const sources = await Promise.all(["projection-public.js", "projection-suss.js"] + .map(async (path) => await readFile(new URL(`../dist/${path}`, import.meta.url), "utf8"))); + assert.equal(sources.some((source) => source.includes("bun:sqlite")), false); +}); diff --git a/site/app/spec/page.tsx b/site/app/spec/page.tsx index 2f73e6f..dd98a97 100644 --- a/site/app/spec/page.tsx +++ b/site/app/spec/page.tsx @@ -15,7 +15,7 @@ const specificationDescription = export const metadata: Metadata = { title: specificationTitle, description: - "The versioned contract for Oh records, graph revisions, local SQLite authority, synchronization, and semantic search.", + "The versioned contract for Oh records, graph revisions, SQLite and libSQL authority, projections, and composite memory.", alternates: { canonical: "/spec" }, openGraph: { title: specificationTitle, @@ -43,6 +43,8 @@ const sections = [ ["sqlite", "SQLite"], ["sync", "Sync"], ["semantic", "Semantic search"], + ["projection", "Derived projection"], + ["memory", "Composite memory"], ["versioning", "Versioning"], ] as const; @@ -97,7 +99,7 @@ export default function Specification() {
Ontology{contract.ontologyVersion}
Graph format{contract.graphFormatVersion}
Schema format{contract.schemaFormatVersion}
-
SQLite schema1
+
SQLite schema2
Sync protocoloh.sync.v1
HashSHA-256
@@ -181,8 +183,8 @@ export default function Specification() {

The transport port is HTTP-friendly and compatible with a libSQL/Turso implementation, while the local store remains the - source of truth. No cloud credential or endpoint appears in the - core package. + source of truth. A separate promise-based adapter can instead + use libSQL as the direct authority without importing Bun SQLite.

@@ -206,8 +208,45 @@ export default function Specification() { -
+
07
+
+

Derived projection

+

+ Typed positive rules derive recursive views from one exact + graph head and content-addressed fact pack. Rule, query, input, + and result identities use canonical bytes, while explicit work + limits bound tuples, rounds, joins, and proof trees. +

+

+ Projection rows always remain derived cache output. They do not + become assertions, review decisions, or operation history until + an application submits new records through its authority path. +

+
+
+ +
+
08
+
+

Composite agent memory

+

+ One experimental facade composes a separately governed working + authority with one exact canonical head. Host-purposed named + programs see lane-tagged facts, visible conflicts, exact + physical authority and extractor digests, and bounded proofs + without receiving store locators or canonical mutation handles. +

+

+ Working nominations are verified dependency-closure proposals. + Review and durable adoption remain destination-owned operations; + a derived result never promotes itself. +

+
+
+ +
+
09

Versioning and evolution

diff --git a/site/package.json b/site/package.json index 6b0dd49..da6e344 100644 --- a/site/package.json +++ b/site/package.json @@ -1,6 +1,6 @@ { "name": "oh-site", - "version": "0.1.1", + "version": "0.2.0", "private": true, "packageManager": "bun@1.3.14", "engines": { diff --git a/site/public/spec/README.md b/site/public/spec/README.md index 0957d95..33c56aa 100644 --- a/site/public/spec/README.md +++ b/site/public/spec/README.md @@ -2,8 +2,8 @@ This directory is the versioned public contract for Oh. It defines the canonical bytes, ontology identities, graph envelopes, schema revisions, -SQLite state, operation sync, and local embedding profile that independent -implementations need to interoperate. +SQLite and direct libSQL authority, operation sync, store profiles, and local +embedding profile that independent implementations need to interoperate. [`manifest.json`](manifest.json) is the discovery document. V1 is current and binds these versions: @@ -14,9 +14,11 @@ binds these versions: | Contract ID | `oh.ontology.v1` | | Graph format | `1` | | Schema format | `1` | -| SQLite schema | `1` | +| SQLite schema | `2` | | Sync protocol | `oh.sync.v1` | | Embedding profile | `1` | +| Projection semantics | `oh.projection.positive-datalog.v1` | +| Composite memory | `experimental v1` | ## V1 documents @@ -25,8 +27,11 @@ binds these versions: - [Schema evolution](v1/schema-evolution.md) - [Graph and operations](v1/graph.md) - [SQLite storage](v1/storage.md) +- [Store ports, profiles, and direct libSQL authority](v1/store.md) - [Sync protocol](v1/sync.md) - [Local embedding profile](v1/embedding.md) +- [Derived projections](v1/projection.md) +- [Experimental composite agent memory](v1/memory.md) - [Compatibility and migration](v1/migration.md) Machine-readable V1 artifacts: @@ -39,6 +44,10 @@ Machine-readable V1 artifacts: - [`schema-revision.schema.json`](v1/schema-revision.schema.json) - [`operation.schema.json`](v1/operation.schema.json) - [`sync-bundle.schema.json`](v1/sync-bundle.schema.json) +- [`projection-rule-pack.schema.json`](v1/projection-rule-pack.schema.json) +- [`projection-query.schema.json`](v1/projection-query.schema.json) +- [`projection-identity.schema.json`](v1/projection-identity.schema.json) +- [`projection-result.schema.json`](v1/projection-result.schema.json) ## Conformance diff --git a/site/public/spec/manifest.json b/site/public/spec/manifest.json index f470d9d..6c28d11 100644 --- a/site/public/spec/manifest.json +++ b/site/public/spec/manifest.json @@ -11,13 +11,27 @@ "contractSha256": "e53ae573c2af417082be9f554d0f6f3e317f054daf745181f462608e3f622594", "embeddingProfile": "./v1/embedding-profile.json", "id": "v1", + "memory": { + "specification": "./v1/memory.md" + }, "ontology": "./v1/ontology.json", + "projection": { + "identitySchema": "./v1/projection-identity.schema.json", + "querySchema": "./v1/projection-query.schema.json", + "resultSchema": "./v1/projection-result.schema.json", + "rulePackSchema": "./v1/projection-rule-pack.schema.json", + "specification": "./v1/projection.md" + }, "schemas": [ "./v1/contract.schema.json", "./v1/record.schema.json", "./v1/schema-revision.schema.json", "./v1/operation.schema.json", - "./v1/sync-bundle.schema.json" + "./v1/sync-bundle.schema.json", + "./v1/projection-rule-pack.schema.json", + "./v1/projection-query.schema.json", + "./v1/projection-identity.schema.json", + "./v1/projection-result.schema.json" ], "specification": "./v1/ontology.md", "status": "current", diff --git a/site/public/spec/v1/memory.md b/site/public/spec/v1/memory.md new file mode 100644 index 0000000..49b6199 --- /dev/null +++ b/site/public/spec/v1/memory.md @@ -0,0 +1,114 @@ +# Experimental composite agent memory + +`@hraness/oh/experimental/memory` is the first consumer-facing composition of +the stable store and projection contracts. It is experimental API, not a new +ontology or a third storage authority. + +## One kernel, two authorities + +A host MUST bind two distinct physical authorities before it creates the +facade: + +- a working-profile store, writable only through codec-enforced semantic + bundles; and +- a canonical-profile store pinned at one exact host-selected head and never + writable through the returned agent object. + +The host supplies opaque authority IDs plus the exact expected binding digests. +The factory rejects equal authority IDs, a binding mismatch, or the wrong +profile. Authority IDs identify custody in results without revealing a database +path, URL, credential, or purge handle. An Oh space, a semantic context, a +runtime tenant/session, and a physical authority remain different boundaries. +The host also binds the working actor, every named program's purpose, and every +named nomination route. None of those authority-bearing labels comes from +agent input. + +The returned object exposes only `remember`, `query`, `explain`, and +`nominate`. It has no generic commit, store selection, path, sync, rule +registration, canonical write, or purge operation. + +`remember` accepts only an expected working head, semantic puts and tombstones, +and an idempotency request ID. The facade supplies its host-bound actor, uses +its non-regressing host clock, derives the operation ID, and returns an +immutable, locator-free receipt with the working authority ID, binding digest, +resulting complete head, operation digest, actor, and actual instant. It never +returns raw operation changes or accepts a caller-asserted actor or timestamp. + +## Composite projection + +Every query reads the current working head and the factory-pinned canonical +head. It builds a disposable composite dataset with lane-tagged +`memory.record` and `memory.dependency` facts. Equal logical keys with different +record digests produce an explicit `memory.conflict` fact and result entry; +working data never shadows canonical data by recency. Equal digests produce +`memory.agreement`. + +Trusted host code may register digest-identified domain fact extractors. Each +extractor declares a disjoint set of relations it owns. The facade supplies a +deeply immutable record and its exact physical source, bounds invocation and +output counts, and forbids custom ownership of reserved `memory.*` or `oh.*` +relations. Every fact proof identifies the built-in fact pack or exact domain +extractor ID and digest that emitted it. Trusted host code also registers a +bounded set of parsed rule-pack/query pairs and their fixed purposes under +names. Agent input selects a name; it cannot submit a purpose, rule, query AST, +or inert validity label. + +The composite identity binds: + +- both opaque authority IDs, binding digests, complete heads, projection + snapshots, and lane dataset digests; +- the composite fact dataset; +- the named program, exact rule pack and query; +- evaluation and engine identity inherited from the projection result; +- the host-bound program purpose and fixed visible-conflict policy. + +Changing any of these values produces a different memory digest and a full +rebuild. Results and proofs remain `derived`. A rule cannot upgrade the +authority of its premises. Each public row is labeled from its visible physical +premise lanes; a truncated or missing witness is `unknown` rather than +silently canonical. Returned result, row, value, proof, source, and receipt +graphs are detached and deeply immutable, so a caller cannot mutate bytes after +their digest or explanation capability is issued. + +## Explanations and nominations + +Query returns an opaque, random, short-lived explanation capability bound to +that exact deterministic result. Its expiry uses a non-regressing monotonic +clock; the wall-clock instant is display metadata. A capability may explain +multiple rows until expiry or eviction. Count, per-entry bytes, and aggregate +retained evidence are bounded. `explain` also requires the exact result digest +and row index. It maps every projection fact witness back to an authority ID, +binding digest, complete pinned head, lane, original record key and digest. A +wrong, expired, evicted, or result-mismatched capability fails closed. + +`nominate` selects one host-registered route by opaque name, then exports and +re-verifies an exact dependency closure and exact requested root set from the +current working head. Its +output is only a content-addressed `prepared` proposal for that route's fixed +destination purpose. It does not sync the working operation chain, mutate the +canonical store, import a derived tuple, grant rights, record a review, or turn +a proposed assertion into reviewed knowledge. Destination-owned application +code must perform those steps under its own policy and compare-and-swap head. + +## Lifecycle and custody boundary + +Oh deliberately does not choose a tenant, session, retention deadline, +physical database, credential, scheduler, or backup policy. The application +host owns those lifecycle controls and retains the separate working store host +object. Purge removes a working authority through that host-only +capability; tombstoning is not erasure because operation history retains prior +bytes. Database credentials or same-UID filesystem access remain outside this +API boundary. + +The facade requests at most 8,192 records per lane, rejects a lane snapshot +over 32 MiB, rejects a remember request over 8 MiB, bounds extractor +invocations and emitted facts, limits the public result to 32 MiB, and retains +at most 64 MiB of explanation evidence. A trusted store still constructs the +snapshot before returning it, and a trusted synchronous fact extractor can +consume time or temporary memory before it returns. Provider response limits, +host storage quotas, callback review, isolation, deadlines, and cancellation +remain application responsibilities. + +Suss is an optional differential evaluator behind the separate projection +compatibility subpath. The memory facade uses the package-owned bounded +reference evaluator. Cozo is neither evaluated nor loaded. diff --git a/site/public/spec/v1/migration.md b/site/public/spec/v1/migration.md index 1e7d701..05a0ea4 100644 --- a/site/public/spec/v1/migration.md +++ b/site/public/spec/v1/migration.md @@ -77,3 +77,16 @@ rollback depend on reversing a content or identifier rewrite. SQLite schema migrations are separate from space transfer. The store records each applied migration name and SQL digest in `oh_migrations` and refuses to open when an applied version has different migration bytes. + +SQLite schema version 2 appends `0002_store_realms` without changing the +released `0001_oh_core` SQL. Existing spaces remain unbound after upgrade. A +host may bind one through the promise-based store authority; once persisted, +the exact realm and profile bytes cannot be replaced. A purged working space +cannot be used as a migration source or recreated under the same identifier. + +The direct libSQL authority has its own `oh_authority_` schema digest. It emits +the same V1 record and operation bytes, but it is not a destination for the +offline CLI import procedure above. Applications moving authority between +adapters MUST prove an exact complete operation chain and matching head through +a separately reviewed migration workflow. A dependency-closure capsule is a +selective content export for adoption, not proof of full authority migration. diff --git a/site/public/spec/v1/projection-identity.schema.json b/site/public/spec/v1/projection-identity.schema.json new file mode 100644 index 0000000..485e724 --- /dev/null +++ b/site/public/spec/v1/projection-identity.schema.json @@ -0,0 +1,58 @@ +{ + "$defs": { + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + } + }, + "$id": "https://oh.computer/spec/v1/projection-identity.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance requires the exact public contract digest and recomputes projectionSha256 over every identity field other than projectionSha256 itself.", + "additionalProperties": false, + "properties": { + "contractSha256": { + "const": "e53ae573c2af417082be9f554d0f6f3e317f054daf745181f462608e3f622594" + }, + "datasetSha256": { + "$ref": "#/$defs/sha256" + }, + "engineSha256": { + "$ref": "#/$defs/sha256" + }, + "evaluationSha256": { + "$ref": "#/$defs/sha256" + }, + "projectionSha256": { + "$ref": "#/$defs/sha256" + }, + "querySha256": { + "$ref": "#/$defs/sha256" + }, + "rulePackSha256": { + "$ref": "#/$defs/sha256" + }, + "semantics": { + "const": "oh.projection.positive-datalog.v1" + }, + "snapshotSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "contractSha256", + "datasetSha256", + "engineSha256", + "evaluationSha256", + "projectionSha256", + "querySha256", + "rulePackSha256", + "semantics", + "snapshotSha256", + "v" + ], + "title": "Oh projection identity V1", + "type": "object" +} diff --git a/site/public/spec/v1/projection-query.schema.json b/site/public/spec/v1/projection-query.schema.json new file mode 100644 index 0000000..d9db1e5 --- /dev/null +++ b/site/public/spec/v1/projection-query.schema.json @@ -0,0 +1,60 @@ +{ + "$defs": { + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + } + }, + "$id": "https://oh.computer/spec/v1/projection-query.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally requires unique projected variables, binds every projected variable in the body, enforces the 256-variable aggregate ceiling, and recomputes querySha256.", + "additionalProperties": false, + "properties": { + "find": { + "items": { + "$ref": "#/$defs/safeCode" + }, + "maxItems": 32, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "limit": { + "maximum": 65536, + "minimum": 1, + "type": "integer" + }, + "queryId": { + "$ref": "#/$defs/safeCode" + }, + "querySha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + }, + "where": { + "items": { + "$ref": "./projection-rule-pack.schema.json#/$defs/literal" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "find", + "limit", + "queryId", + "querySha256", + "where", + "v" + ], + "title": "Oh projection query V1", + "type": "object" +} diff --git a/site/public/spec/v1/projection-result.schema.json b/site/public/spec/v1/projection-result.schema.json new file mode 100644 index 0000000..9c0cf93 --- /dev/null +++ b/site/public/spec/v1/projection-result.schema.json @@ -0,0 +1,452 @@ +{ + "$defs": { + "atom": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "number" + }, + { + "maxLength": 16382, + "type": "string" + } + ] + }, + "derivedProof": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "premisesTruncated": { + "const": false + } + }, + "required": [ + "premisesTruncated" + ] + }, + "then": { + "properties": { + "premises": { + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "premisesTruncated": { + "const": true + } + }, + "required": [ + "premisesTruncated" + ] + }, + "then": { + "properties": { + "premises": { + "maxItems": 63 + } + } + } + } + ], + "properties": { + "kind": { + "const": "derived" + }, + "premises": { + "items": { + "$ref": "#/$defs/proof" + }, + "maxItems": 64, + "type": "array" + }, + "premisesTruncated": { + "type": "boolean" + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "ruleId": { + "$ref": "#/$defs/safeCode" + }, + "ruleSha256": { + "$ref": "#/$defs/sha256" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "premises", + "premisesTruncated", + "relation", + "ruleId", + "ruleSha256", + "tuple", + "v" + ], + "type": "object" + }, + "evaluation": { + "additionalProperties": false, + "properties": { + "maximumDerivedTuples": { + "maximum": 262144, + "minimum": 1, + "type": "integer" + }, + "maximumProofDepth": { + "maximum": 128, + "minimum": 1, + "type": "integer" + }, + "maximumProofNodes": { + "maximum": 4096, + "minimum": 1, + "type": "integer" + }, + "maximumResultBytes": { + "maximum": 16777216, + "minimum": 65536, + "type": "integer" + }, + "maximumRounds": { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + "maximumTotalProofNodes": { + "maximum": 65536, + "minimum": 1, + "type": "integer" + }, + "maximumWorkUnits": { + "maximum": 16777216, + "minimum": 1, + "type": "integer" + }, + "v": { + "const": 1 + } + }, + "required": [ + "maximumDerivedTuples", + "maximumProofDepth", + "maximumProofNodes", + "maximumResultBytes", + "maximumRounds", + "maximumTotalProofNodes", + "maximumWorkUnits", + "v" + ], + "type": "object" + }, + "engineCode": { + "maxLength": 256, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "factProof": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "fact" + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "sources": { + "items": { + "$ref": "#/$defs/source" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "relation", + "sources", + "tuple", + "v" + ], + "type": "object" + }, + "proof": { + "oneOf": [ + { + "$ref": "#/$defs/factProof" + }, + { + "$ref": "#/$defs/derivedProof" + }, + { + "$ref": "#/$defs/truncatedProof" + } + ] + }, + "row": { + "additionalProperties": false, + "properties": { + "proofs": { + "items": { + "$ref": "#/$defs/proof" + }, + "maxItems": 64, + "type": "array" + }, + "proofsTruncated": { + "type": "boolean" + }, + "supportCount": { + "maximum": 262144, + "minimum": 1, + "type": "integer" + }, + "values": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "proofs", + "proofsTruncated", + "supportCount", + "values", + "v" + ], + "type": "object" + }, + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + "source": { + "additionalProperties": false, + "properties": { + "key": { + "maxLength": 512, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "recordSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "key", + "recordSha256", + "v" + ], + "type": "object" + }, + "stats": { + "additionalProperties": false, + "properties": { + "baseFacts": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "derivedFacts": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "proofNodes": { + "maximum": 65536, + "minimum": 0, + "type": "integer" + }, + "proofsTruncated": { + "type": "boolean" + }, + "queryMatches": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "relations": { + "maximum": 4096, + "minimum": 0, + "type": "integer" + }, + "rounds": { + "maximum": 1024, + "minimum": 0, + "type": "integer" + }, + "truncated": { + "type": "boolean" + }, + "truncationReasons": { + "items": { + "enum": [ + "query-limit", + "result-bytes" + ] + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "v": { + "const": 1 + }, + "workUnits": { + "maximum": 16777216, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "baseFacts", + "derivedFacts", + "proofNodes", + "proofsTruncated", + "queryMatches", + "relations", + "rounds", + "truncated", + "truncationReasons", + "v", + "workUnits" + ], + "type": "object" + }, + "truncatedProof": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "truncated" + }, + "reason": { + "enum": [ + "cycle", + "depth", + "nodes" + ] + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "reason", + "relation", + "tuple", + "v" + ], + "type": "object" + }, + "tuple": { + "items": { + "$ref": "#/$defs/atom" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + } + }, + "$id": "https://oh.computer/spec/v1/projection-result.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally checks canonical row and source order, atom byte limits, declared aggregate limits, proof depth and node totals, truncation consistency, engineSha256, evaluationSha256, projectionSha256, and resultSha256.", + "additionalProperties": false, + "properties": { + "authority": { + "const": "derived" + }, + "cache": { + "additionalProperties": false, + "properties": { + "strategy": { + "const": "full-rebuild" + }, + "v": { + "const": 1 + } + }, + "required": [ + "strategy", + "v" + ], + "type": "object" + }, + "engine": { + "$ref": "#/$defs/engineCode" + }, + "evaluation": { + "$ref": "#/$defs/evaluation" + }, + "identity": { + "$ref": "./projection-identity.schema.json" + }, + "resultSha256": { + "$ref": "#/$defs/sha256" + }, + "rows": { + "items": { + "$ref": "#/$defs/row" + }, + "maxItems": 65536, + "type": "array" + }, + "stats": { + "$ref": "#/$defs/stats" + }, + "v": { + "const": 1 + } + }, + "required": [ + "authority", + "cache", + "engine", + "evaluation", + "identity", + "resultSha256", + "rows", + "stats", + "v" + ], + "title": "Oh projection result V1", + "type": "object" +} diff --git a/site/public/spec/v1/projection-rule-pack.schema.json b/site/public/spec/v1/projection-rule-pack.schema.json new file mode 100644 index 0000000..4864012 --- /dev/null +++ b/site/public/spec/v1/projection-rule-pack.schema.json @@ -0,0 +1,182 @@ +{ + "$defs": { + "atom": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "number" + }, + { + "maxLength": 16382, + "type": "string" + } + ] + }, + "constantTerm": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "constant" + }, + "v": { + "const": 1 + }, + "value": { + "$ref": "#/$defs/atom" + } + }, + "required": [ + "kind", + "v", + "value" + ], + "type": "object" + }, + "literal": { + "additionalProperties": false, + "properties": { + "relation": { + "$ref": "#/$defs/safeCode" + }, + "terms": { + "items": { + "$ref": "#/$defs/term" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + }, + "v": { + "const": 1 + } + }, + "required": [ + "relation", + "terms", + "v" + ], + "type": "object" + }, + "rule": { + "additionalProperties": false, + "properties": { + "body": { + "items": { + "$ref": "#/$defs/literal" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + }, + "head": { + "$ref": "#/$defs/literal" + }, + "ruleId": { + "$ref": "#/$defs/safeCode" + }, + "ruleSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "body", + "head", + "ruleId", + "ruleSha256", + "v" + ], + "type": "object" + }, + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + "term": { + "oneOf": [ + { + "$ref": "#/$defs/constantTerm" + }, + { + "$ref": "#/$defs/variableTerm" + } + ] + }, + "variableTerm": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "variable" + }, + "name": { + "$ref": "#/$defs/safeCode" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "name", + "v" + ], + "type": "object" + } + }, + "$id": "https://oh.computer/spec/v1/projection-rule-pack.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally measures each atom's exact 16 KiB canonical-JSON byte ceiling, requires safe head variables, canonical rule ordering, unique rule IDs, and recomputed rule and pack digests.", + "additionalProperties": false, + "properties": { + "rulePackId": { + "$ref": "#/$defs/safeCode" + }, + "rulePackRevision": { + "minimum": 1, + "type": "integer" + }, + "rulePackSha256": { + "$ref": "#/$defs/sha256" + }, + "rules": { + "items": { + "$ref": "#/$defs/rule" + }, + "maxItems": 1024, + "minItems": 1, + "type": "array" + }, + "rulesSha256": { + "$ref": "#/$defs/sha256" + }, + "semantics": { + "const": "oh.projection.positive-datalog.v1" + }, + "v": { + "const": 1 + } + }, + "required": [ + "rulePackId", + "rulePackRevision", + "rulePackSha256", + "rules", + "rulesSha256", + "semantics", + "v" + ], + "title": "Oh projection rule pack V1", + "type": "object" +} diff --git a/site/public/spec/v1/projection.md b/site/public/spec/v1/projection.md new file mode 100644 index 0000000..fe32ab7 --- /dev/null +++ b/site/public/spec/v1/projection.md @@ -0,0 +1,165 @@ +# Derived projections + +Oh projections are reproducible, non-authoritative views over one exact graph +snapshot. The runtime exposes the same typed rule, query, identity, and result +contract through `@hraness/oh/projection` without changing the V1 graph record, +operation, canonical JSON, or SQLite formats. + +## Authority boundary + +A projection result has `authority: "derived"`. Its tuples and proof trees are +cache output. They MUST NOT be interpreted as graph assertions, review +decisions, accepted knowledge, or operation history. An application that wants +to retain a conclusion MUST create and review new graph records through its +ordinary authority path. + +The projection module imports no SQLite runtime. A caller supplies an exact +snapshot assembled from the authority it selected. This keeps the same contract +usable in a Node 24 serverless process, a local agent, or an application-owned +remote-store adapter. + +## Exact input identity + +`createOhProjectionSnapshotV1` accepts a space ID, current head, and complete +record snapshot. It: + +1. parses every record under the unchanged V1 graph contract; +2. sorts record references by logical key; +3. recomputes `recordsSha256` and requires it to equal the head; +4. checks dependency closure; and +5. hashes the contract digest, space, head, and record references into + `snapshotSha256`. + +A fact is one relation tuple plus one or more exact source record keys and +digests. A dataset rejects a source that is not current at its snapshot. +Duplicate relation tuples are coalesced and retain the union of their source +records. The dataset identity binds: + +- the snapshot digest; +- a fact-pack ID and revision; +- the SHA-256 digest of the application-owned extractor implementation; and +- the canonical ordered fact set. + +Oh supplies structural `oh.record(key, kind, digest)` and +`oh.dependency(key, dependency)` facts. Domain packs may emit richer relations, +but their extractor digest and every source record remain explicit. The +package-owned structural extractor profile is published as +`OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1`. + +## Positive rule semantics + +A term is a variable or a JSON primitive constant. A literal names one relation +and an ordered term list. A rule has one head and a nonempty conjunction of +positive body literals. Every head variable MUST occur in its body. Relation +arity MUST be consistent across base facts, rule heads, rule bodies, and the +query. + +The V1 projection evaluator implements finite, positive Datalog with set +semantics and synchronous naïve fixpoint rounds. It supports recursive rules. +It does not support negation, aggregation, arithmetic, function symbols, or +callbacks inside rules. Unknown objects are parsed with exact keys, so an +unsupported operator fails rather than being ignored. + +Rule packs are sorted by rule ID and content-addressed. A query declares an +ordered `find` variable list, a nonempty positive body, and an output limit. +Query results use set semantics and sort tuples by canonical JSON. Declaration, +fact, and insertion order do not affect rule-pack identity or output bytes. +The projection identity also binds the selected engine and resolved evaluation +limits, so results created with different engines, proof budgets, or work +budgets cannot share a cache identity. + +## Evaluation limits + +The implementation checks hard ceilings before or during work: + +| Item | Maximum | +| --- | ---: | +| Tuple arity | 32 | +| Canonical bytes per atom | 16 KiB | +| Base facts | 262,144 | +| Derived tuples | 262,144 | +| Rules | 1,024 | +| Body literals per rule | 64 | +| Evaluation rounds | 1,024 | +| Join matches per rule or query body | 262,144 | +| Tuple-unification work units per evaluation | 16,777,216 | +| Returned query rows | 65,536 | +| Proof depth | 128 | +| Proof nodes per row | 4,096 | +| Proof nodes across returned rows | 65,536 | +| Canonical result bytes | 16 MiB | + +A caller may request smaller derived-tuple, round, proof-depth, proof-node, and +global tuple-unification work bounds. The global work counter spans every rule, +round, and the final query, including unsuccessful candidate matches. Exceeding +a work bound fails closed. Result construction additionally stops before the +aggregate proof-node or canonical-byte ceilings. A query's declared output +limit or the result-byte ceiling returns a canonical prefix, sets +`stats.truncated: true`, and lists `query-limit` or `result-bytes` in +`stats.truncationReasons`. + +## Proofs + +Each returned row carries one proof for each literal in one canonical supporting +query-body match. `supportCount` reports how many complete matches produced the +same projected value tuple; V1 deliberately does not serialize every alternate +witness. A fact leaf names its relation, tuple, and exact source record +references. A derived node names the rule ID and digest and recursively contains +its premises. Depth and cycle guards emit an explicit `truncated` node. If a +node or byte budget ends between sibling premises, the enclosing derived node +sets `premisesTruncated: true`; if it ends between query-body proofs, the row +sets `proofsTruncated: true`. +`stats.proofsTruncated` reports either form across all returned rows. A proof +establishes how the bounded evaluator derived a tuple from the supplied bytes; +it does not establish that a proposition is true. + +## Safe cached ingress + +Projection declarations and cache output are untrusted exchange data. The +`parseOhProjectionRulePackV1`, `parseOhProjectionQueryV1`, and +`parseOhProjectionIdentityV1` parsers reject unknown keys and invalid digest +preimages. `parseOhProjectionProofV1` additionally applies the public proof +depth, node, tuple, source, atom-byte, and aggregate-byte ceilings before +returning a proof tree. + +`parseOhProjectionResultV1` is the cache-ingress boundary. It verifies the +result digest; canonical row and source order; `supportCount`; proof-node, +work-unit, relation, match, round, and byte totals; every proof and result +truncation marker; and all declared evaluation ceilings. It also recomputes the +engine and evaluation digests and requires them to match the projection +identity. A valid SHA-256 string by itself is not enough to make an envelope +acceptable. Cache readers SHOULD pass the projection digest they requested as +the parser's second argument; an internally consistent envelope does not prove +that a cache returned the requested identity. + +The discovery manifest publishes machine-readable schemas for rule packs, +queries, identities, and result envelopes. JSON Schema describes the exchange +shape and static maxima. The runtime parsers remain normative for canonical +ordering, digest preimages, aggregate budgets, and cross-field consistency that +the schemas cannot express. + +## Cache invalidation + +`projectionSha256` binds the current contract, snapshot, dataset, rule pack, +query, engine, resolved evaluation limits, and positive-Datalog semantics. A cached +result is reusable only when that digest is unchanged. Any snapshot, dataset, +rule-pack, query, engine, or evaluation-limit change has `kind: "full-rebuild"` and +lists the changed identities. V1 does not claim incremental deletion or +cross-snapshot maintenance. + +## Optional Suss equivalence lane + +`@hraness/oh/experimental/projection-suss` supports exactly +`@suss/datalog@0.20.0` as an optional peer. It encodes every JSON primitive atom +into canonical JSON text, evaluates the positive rules with Suss, and compares +every complete relation to the Oh reference semantics. It returns only after +exact set agreement. + +Suss's public evaluator does not expose an execution-budget hook. Before calling +it, the adapter first runs the bounded reference evaluator, then computes a +conservative finite-domain upper bound on new tuples in rule-head relations and +refuses a program it cannot prove will remain under the requested derived-tuple +ceiling. It compares Suss's complete result to the reference materialization and +uses the reference witnesses for canonical proof construction. This lane +evaluates compatibility, not performance. Refusal does not disable the built-in +evaluator. diff --git a/site/public/spec/v1/storage.md b/site/public/spec/v1/storage.md index 085d4c4..8cc36e5 100644 --- a/site/public/spec/v1/storage.md +++ b/site/public/spec/v1/storage.md @@ -1,6 +1,6 @@ # SQLite storage V1 -SQLite schema version `1` is the local authority for an Oh space. The default +SQLite schema version `2` is the local authority for an Oh space. The default CLI database is `.oh/oh.sqlite`; callers may select another path or use an in-memory database. @@ -34,10 +34,37 @@ rolls back the transaction and preserves the original error. | `oh_sync_state` | Last settled state for a named remote. | | `oh_search_documents` | Derived keyword text bound to a record digest. | | `oh_search_fts` | Derived FTS5 index. | +| `oh_space_bindings` | Host-selected realm, lifecycle profile, capabilities, and application-profile digest for a supported store port. | +| `oh_space_purges` | Minimal receipt that permanently reserves the identifier of a purged working space. | -The first migration is named `0001_oh_core`. An implementation MUST store and -check the exact SHA-256 digest of applied migration SQL. It MUST refuse to run -when the same migration version or name has different bytes. +The first migration is named `0001_oh_core`; its released bytes remain +unchanged. Schema version 2 adds `0002_store_realms`. An implementation MUST +store and check the exact SHA-256 digest of applied migration SQL. It MUST +refuse to run when the same migration version or name has different bytes. + +## Realm profiles and purge + +A promise-based store port MAY bind one space to one host-selected realm and +profile. The binding includes the exact Oh contract digest, an optional +application-profile digest, and declared capabilities. A supported runtime +MUST reject a later attempt to open the same space under different binding +bytes. A working profile disables operation replication and enables only +host-controlled whole-space purge. A canonical profile cannot be purged by +that API. + +Purge deletes the space head, complete operation history, current records, +dependency and operation materializations, sync state, and derived keyword +rows in one immediate transaction. It leaves only a content-free receipt with +the prior head, binding digest, purge instant, and receipt digest. The purged +space identifier cannot be reopened in the same database. A host that deletes +an entire database file MUST retain any required deletion evidence in its own +control plane. + +Realm and profile binding is additive store control metadata. V1 operation +digest preimages do not contain the binding. It therefore protects supported +opens and operations but is not a portable cryptographic claim about a V1 +history. Such a claim requires a new wire contract rather than a change to V1 +operation bytes. ## Authority and derivation diff --git a/site/public/spec/v1/store.md b/site/public/spec/v1/store.md new file mode 100644 index 0000000..85aa95e --- /dev/null +++ b/site/public/spec/v1/store.md @@ -0,0 +1,117 @@ +# Store ports, profiles, and direct libSQL authority + +The graph and operation contract remains V1. This document defines additive +host APIs that preserve those bytes across local SQLite and direct libSQL +authorities. + +## Promise-based store port + +`@hraness/oh/store` has no dependency on `bun:sqlite`. Its methods return +promises and expose: + +- the current exact head; +- a current or historical snapshot at an exact sequence and operation digest; +- a bounded contiguous change page through a pinned head; +- compare-and-swap commit; +- dependency-closure export; +- replay and materialization verification; and +- close. + +A historical read MUST fail if its sequence is absent or identifies a +different operation digest. A change page MUST name its source cursor, pinned +through-head, returned cursor, and whether more operations remain. + +## Semantic bundle ingress + +Model-facing code SHOULD use `OhSemanticBundleIngressV1` instead of generic +record puts. The ingress seals its codec registry, requires a registered codec +for every put kind, parses all values, creates canonical record envelopes, and +submits all puts and tombstones as one compare-and-swap operation. Missing +codecs, invalid values, duplicate keys, stale heads, and incomplete dependency +closures fail before the authority head moves. + +## Profiles and host control + +A binding combines one exact contract, application profile, lifecycle profile, +realm, and space. The built-in canonical profile permits operation replication +and forbids whole-space purge. The built-in working profile forbids operation +replication and permits purge. + +Creating an authority returns separate `store` and `host` objects. The ordinary +store never has a purge method. Trusted control-plane code retains the host +object and does not pass it to an agent tool or model. This is an API and +custody boundary, not protection from code that already holds raw database +credentials or direct filesystem access. + +## Dependency-closure capsules + +A closure export pins the source binding and exact head, sorts unique roots, +and includes exactly the records reachable through declared dependencies. The +capsule digest binds all of those fields. Verification rejects a missing +dependency, an extra unrelated record, a changed record digest, reordered +roots or records, and a false capsule digest. V1 bounds a capsule to 1,024 +roots, 8,192 records, and 67,108,864 canonical UTF-8 bytes. + +A closure is content evidence for a later reviewed adoption. It does not copy +source authority, review state, credentials, or operation history into a +destination. + +## Direct libSQL authority + +`@hraness/oh/libsql` accepts the `execute` and transactional `batch` shape of +`@libsql/client`. It is Node 24 and serverless compatible and does not import +`bun:sqlite`. Unlike the V1 sync transport, it treats libSQL as the current +record and operation authority. + +`bootstrapOhLibSqlAuthorityV1` is the only API that creates schema objects. +Run it in a deployment or migration step with a short-lived schema credential. +`createOhLibSqlStoreAuthorityV1` is the runtime open: it only verifies the +installed schema and contract before reading or creating a bound data space, +so a runtime token does not need schema-change permission. + +Runtime open verifies the exact installed table, index, and trigger set, not +only a schema marker. Every operation, binding, and purge receipt read parses +its canonical JSON and cross-checks each duplicated SQL column. Current reads +also prove contiguous operation coverage through the exact terminal head, +record provenance puts, record digests, and dependency materialization. + +Its private implementation tables use the `oh_authority_` prefix: + +| Table | Role | +| --- | --- | +| `oh_authority_schemas` | Exact adapter schema name and digest. | +| `oh_authority_contracts` | Exact Oh contract manifest. | +| `oh_authority_spaces` | Current compare-and-swap heads. | +| `oh_authority_operations` | Canonical append-only operations. | +| `oh_authority_operation_records` | Ordered changes per operation. | +| `oh_authority_records` | Current record materialization. | +| `oh_authority_dependencies` | Current dependency edges. | +| `oh_authority_bindings` | Realm and profile control metadata. | +| `oh_authority_purges` | Minimal whole-space purge receipts. | +| `oh_authority_commit_guards` | Empty constraint table used to abort a stale transactional batch. | + +A normal remote commit takes three atomic provider round trips: an idempotency, +head, and purge preflight; one exact current-materialization read; and one +guarded write batch. The final write guard aborts the complete transaction when +compare-and-swap did not settle at the declared operation. Its write-batch +readback must reproduce both the canonical operation and persisted head before +the adapter returns success. + +Provider responses are bounded as part of the API. V1 accepts at most 64 +changes, 512 dependencies, and 512 KiB of canonical operation JSON per commit. +A feed returns at most seven operations plus one checked sentinel and refuses a +page whose conservative transport estimate exceeds 9,000,000 bytes. Historical +replay is limited to 16,384 operations, 4 MiB of canonical operation JSON, and +the same response estimate. Current snapshot and full-verification result sets +are independently transport-estimated and SQL-gated before rows are returned. +Sizing, rows, and the pinned head are read in the same transaction, so a +concurrent append cannot grow an unchecked response between preflight and read. + +Remote purge similarly inserts a receipt only for the expected working head, +deletes every payload and materialization row under that receipt in the same +write batch, and aborts if either the receipt or deletion is incomplete. A +later open returns the stored purge receipt instead of recreating the space. +Operation-record deletion resolves ownership through the canonical operation; +the purge postcondition also rejects any global orphan or cross-space owner +mismatch. Purge receipts are immutable and intentionally retain only binding, +prior-head, and purge-event evidence. diff --git a/skills/oh/SKILL.md b/skills/oh/SKILL.md index a26f67c..7dd821f 100644 --- a/skills/oh/SKILL.md +++ b/skills/oh/SKILL.md @@ -30,7 +30,7 @@ oh --help oh version ``` -The supported CLI is `@hraness/oh@0.1.1` from the immutable `v0.1.1` GitHub +The supported CLI is `@hraness/oh@0.2.0` from the immutable `v0.2.0` GitHub tag. It requires Bun 1.3.14 or newer. The versioned contract is published at . @@ -167,6 +167,24 @@ and credential source. Never print credentials or embed them in records. Oh settles fast-forward histories only; preserve both logs when it reports a divergence. +## Use composite memory only through host bindings + +`@hraness/oh/experimental/memory` is an SDK-only surface. Do not let a model +construct its options. Trusted application code must bind two distinct +authority handles, exact binding digests, a pinned canonical head, working +codecs, a working actor, domain extractor relation ownership and digests, +host-purposed named rule/query programs, and named nomination routes before +giving the returned object to an agent. + +The agent-facing object may call only `remember`, `query`, `explain`, and +`nominate`. Never add a tool parameter for a database path or URL, authority, +realm, space, store profile, rule pack, raw query, sync destination, canonical +write, caller-asserted actor/time, or purge operation. Preserve lane, conflict, +fact-policy, and premise-authority labels in query output. Treat every result +as derived. A nomination may select only a host-registered route and is a +prepared dependency-closure candidate for destination-owned review, not +permission to write durable knowledge or import the working operation chain. + ## Finish with evidence Report the exact database and space, reads or mutations performed, final head diff --git a/spec/README.md b/spec/README.md index 0957d95..33c56aa 100644 --- a/spec/README.md +++ b/spec/README.md @@ -2,8 +2,8 @@ This directory is the versioned public contract for Oh. It defines the canonical bytes, ontology identities, graph envelopes, schema revisions, -SQLite state, operation sync, and local embedding profile that independent -implementations need to interoperate. +SQLite and direct libSQL authority, operation sync, store profiles, and local +embedding profile that independent implementations need to interoperate. [`manifest.json`](manifest.json) is the discovery document. V1 is current and binds these versions: @@ -14,9 +14,11 @@ binds these versions: | Contract ID | `oh.ontology.v1` | | Graph format | `1` | | Schema format | `1` | -| SQLite schema | `1` | +| SQLite schema | `2` | | Sync protocol | `oh.sync.v1` | | Embedding profile | `1` | +| Projection semantics | `oh.projection.positive-datalog.v1` | +| Composite memory | `experimental v1` | ## V1 documents @@ -25,8 +27,11 @@ binds these versions: - [Schema evolution](v1/schema-evolution.md) - [Graph and operations](v1/graph.md) - [SQLite storage](v1/storage.md) +- [Store ports, profiles, and direct libSQL authority](v1/store.md) - [Sync protocol](v1/sync.md) - [Local embedding profile](v1/embedding.md) +- [Derived projections](v1/projection.md) +- [Experimental composite agent memory](v1/memory.md) - [Compatibility and migration](v1/migration.md) Machine-readable V1 artifacts: @@ -39,6 +44,10 @@ Machine-readable V1 artifacts: - [`schema-revision.schema.json`](v1/schema-revision.schema.json) - [`operation.schema.json`](v1/operation.schema.json) - [`sync-bundle.schema.json`](v1/sync-bundle.schema.json) +- [`projection-rule-pack.schema.json`](v1/projection-rule-pack.schema.json) +- [`projection-query.schema.json`](v1/projection-query.schema.json) +- [`projection-identity.schema.json`](v1/projection-identity.schema.json) +- [`projection-result.schema.json`](v1/projection-result.schema.json) ## Conformance diff --git a/spec/manifest.json b/spec/manifest.json index f470d9d..6c28d11 100644 --- a/spec/manifest.json +++ b/spec/manifest.json @@ -11,13 +11,27 @@ "contractSha256": "e53ae573c2af417082be9f554d0f6f3e317f054daf745181f462608e3f622594", "embeddingProfile": "./v1/embedding-profile.json", "id": "v1", + "memory": { + "specification": "./v1/memory.md" + }, "ontology": "./v1/ontology.json", + "projection": { + "identitySchema": "./v1/projection-identity.schema.json", + "querySchema": "./v1/projection-query.schema.json", + "resultSchema": "./v1/projection-result.schema.json", + "rulePackSchema": "./v1/projection-rule-pack.schema.json", + "specification": "./v1/projection.md" + }, "schemas": [ "./v1/contract.schema.json", "./v1/record.schema.json", "./v1/schema-revision.schema.json", "./v1/operation.schema.json", - "./v1/sync-bundle.schema.json" + "./v1/sync-bundle.schema.json", + "./v1/projection-rule-pack.schema.json", + "./v1/projection-query.schema.json", + "./v1/projection-identity.schema.json", + "./v1/projection-result.schema.json" ], "specification": "./v1/ontology.md", "status": "current", diff --git a/spec/v1/memory.md b/spec/v1/memory.md new file mode 100644 index 0000000..49b6199 --- /dev/null +++ b/spec/v1/memory.md @@ -0,0 +1,114 @@ +# Experimental composite agent memory + +`@hraness/oh/experimental/memory` is the first consumer-facing composition of +the stable store and projection contracts. It is experimental API, not a new +ontology or a third storage authority. + +## One kernel, two authorities + +A host MUST bind two distinct physical authorities before it creates the +facade: + +- a working-profile store, writable only through codec-enforced semantic + bundles; and +- a canonical-profile store pinned at one exact host-selected head and never + writable through the returned agent object. + +The host supplies opaque authority IDs plus the exact expected binding digests. +The factory rejects equal authority IDs, a binding mismatch, or the wrong +profile. Authority IDs identify custody in results without revealing a database +path, URL, credential, or purge handle. An Oh space, a semantic context, a +runtime tenant/session, and a physical authority remain different boundaries. +The host also binds the working actor, every named program's purpose, and every +named nomination route. None of those authority-bearing labels comes from +agent input. + +The returned object exposes only `remember`, `query`, `explain`, and +`nominate`. It has no generic commit, store selection, path, sync, rule +registration, canonical write, or purge operation. + +`remember` accepts only an expected working head, semantic puts and tombstones, +and an idempotency request ID. The facade supplies its host-bound actor, uses +its non-regressing host clock, derives the operation ID, and returns an +immutable, locator-free receipt with the working authority ID, binding digest, +resulting complete head, operation digest, actor, and actual instant. It never +returns raw operation changes or accepts a caller-asserted actor or timestamp. + +## Composite projection + +Every query reads the current working head and the factory-pinned canonical +head. It builds a disposable composite dataset with lane-tagged +`memory.record` and `memory.dependency` facts. Equal logical keys with different +record digests produce an explicit `memory.conflict` fact and result entry; +working data never shadows canonical data by recency. Equal digests produce +`memory.agreement`. + +Trusted host code may register digest-identified domain fact extractors. Each +extractor declares a disjoint set of relations it owns. The facade supplies a +deeply immutable record and its exact physical source, bounds invocation and +output counts, and forbids custom ownership of reserved `memory.*` or `oh.*` +relations. Every fact proof identifies the built-in fact pack or exact domain +extractor ID and digest that emitted it. Trusted host code also registers a +bounded set of parsed rule-pack/query pairs and their fixed purposes under +names. Agent input selects a name; it cannot submit a purpose, rule, query AST, +or inert validity label. + +The composite identity binds: + +- both opaque authority IDs, binding digests, complete heads, projection + snapshots, and lane dataset digests; +- the composite fact dataset; +- the named program, exact rule pack and query; +- evaluation and engine identity inherited from the projection result; +- the host-bound program purpose and fixed visible-conflict policy. + +Changing any of these values produces a different memory digest and a full +rebuild. Results and proofs remain `derived`. A rule cannot upgrade the +authority of its premises. Each public row is labeled from its visible physical +premise lanes; a truncated or missing witness is `unknown` rather than +silently canonical. Returned result, row, value, proof, source, and receipt +graphs are detached and deeply immutable, so a caller cannot mutate bytes after +their digest or explanation capability is issued. + +## Explanations and nominations + +Query returns an opaque, random, short-lived explanation capability bound to +that exact deterministic result. Its expiry uses a non-regressing monotonic +clock; the wall-clock instant is display metadata. A capability may explain +multiple rows until expiry or eviction. Count, per-entry bytes, and aggregate +retained evidence are bounded. `explain` also requires the exact result digest +and row index. It maps every projection fact witness back to an authority ID, +binding digest, complete pinned head, lane, original record key and digest. A +wrong, expired, evicted, or result-mismatched capability fails closed. + +`nominate` selects one host-registered route by opaque name, then exports and +re-verifies an exact dependency closure and exact requested root set from the +current working head. Its +output is only a content-addressed `prepared` proposal for that route's fixed +destination purpose. It does not sync the working operation chain, mutate the +canonical store, import a derived tuple, grant rights, record a review, or turn +a proposed assertion into reviewed knowledge. Destination-owned application +code must perform those steps under its own policy and compare-and-swap head. + +## Lifecycle and custody boundary + +Oh deliberately does not choose a tenant, session, retention deadline, +physical database, credential, scheduler, or backup policy. The application +host owns those lifecycle controls and retains the separate working store host +object. Purge removes a working authority through that host-only +capability; tombstoning is not erasure because operation history retains prior +bytes. Database credentials or same-UID filesystem access remain outside this +API boundary. + +The facade requests at most 8,192 records per lane, rejects a lane snapshot +over 32 MiB, rejects a remember request over 8 MiB, bounds extractor +invocations and emitted facts, limits the public result to 32 MiB, and retains +at most 64 MiB of explanation evidence. A trusted store still constructs the +snapshot before returning it, and a trusted synchronous fact extractor can +consume time or temporary memory before it returns. Provider response limits, +host storage quotas, callback review, isolation, deadlines, and cancellation +remain application responsibilities. + +Suss is an optional differential evaluator behind the separate projection +compatibility subpath. The memory facade uses the package-owned bounded +reference evaluator. Cozo is neither evaluated nor loaded. diff --git a/spec/v1/migration.md b/spec/v1/migration.md index 1e7d701..05a0ea4 100644 --- a/spec/v1/migration.md +++ b/spec/v1/migration.md @@ -77,3 +77,16 @@ rollback depend on reversing a content or identifier rewrite. SQLite schema migrations are separate from space transfer. The store records each applied migration name and SQL digest in `oh_migrations` and refuses to open when an applied version has different migration bytes. + +SQLite schema version 2 appends `0002_store_realms` without changing the +released `0001_oh_core` SQL. Existing spaces remain unbound after upgrade. A +host may bind one through the promise-based store authority; once persisted, +the exact realm and profile bytes cannot be replaced. A purged working space +cannot be used as a migration source or recreated under the same identifier. + +The direct libSQL authority has its own `oh_authority_` schema digest. It emits +the same V1 record and operation bytes, but it is not a destination for the +offline CLI import procedure above. Applications moving authority between +adapters MUST prove an exact complete operation chain and matching head through +a separately reviewed migration workflow. A dependency-closure capsule is a +selective content export for adoption, not proof of full authority migration. diff --git a/spec/v1/projection-identity.schema.json b/spec/v1/projection-identity.schema.json new file mode 100644 index 0000000..485e724 --- /dev/null +++ b/spec/v1/projection-identity.schema.json @@ -0,0 +1,58 @@ +{ + "$defs": { + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + } + }, + "$id": "https://oh.computer/spec/v1/projection-identity.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance requires the exact public contract digest and recomputes projectionSha256 over every identity field other than projectionSha256 itself.", + "additionalProperties": false, + "properties": { + "contractSha256": { + "const": "e53ae573c2af417082be9f554d0f6f3e317f054daf745181f462608e3f622594" + }, + "datasetSha256": { + "$ref": "#/$defs/sha256" + }, + "engineSha256": { + "$ref": "#/$defs/sha256" + }, + "evaluationSha256": { + "$ref": "#/$defs/sha256" + }, + "projectionSha256": { + "$ref": "#/$defs/sha256" + }, + "querySha256": { + "$ref": "#/$defs/sha256" + }, + "rulePackSha256": { + "$ref": "#/$defs/sha256" + }, + "semantics": { + "const": "oh.projection.positive-datalog.v1" + }, + "snapshotSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "contractSha256", + "datasetSha256", + "engineSha256", + "evaluationSha256", + "projectionSha256", + "querySha256", + "rulePackSha256", + "semantics", + "snapshotSha256", + "v" + ], + "title": "Oh projection identity V1", + "type": "object" +} diff --git a/spec/v1/projection-query.schema.json b/spec/v1/projection-query.schema.json new file mode 100644 index 0000000..d9db1e5 --- /dev/null +++ b/spec/v1/projection-query.schema.json @@ -0,0 +1,60 @@ +{ + "$defs": { + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + } + }, + "$id": "https://oh.computer/spec/v1/projection-query.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally requires unique projected variables, binds every projected variable in the body, enforces the 256-variable aggregate ceiling, and recomputes querySha256.", + "additionalProperties": false, + "properties": { + "find": { + "items": { + "$ref": "#/$defs/safeCode" + }, + "maxItems": 32, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "limit": { + "maximum": 65536, + "minimum": 1, + "type": "integer" + }, + "queryId": { + "$ref": "#/$defs/safeCode" + }, + "querySha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + }, + "where": { + "items": { + "$ref": "./projection-rule-pack.schema.json#/$defs/literal" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "find", + "limit", + "queryId", + "querySha256", + "where", + "v" + ], + "title": "Oh projection query V1", + "type": "object" +} diff --git a/spec/v1/projection-result.schema.json b/spec/v1/projection-result.schema.json new file mode 100644 index 0000000..9c0cf93 --- /dev/null +++ b/spec/v1/projection-result.schema.json @@ -0,0 +1,452 @@ +{ + "$defs": { + "atom": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "number" + }, + { + "maxLength": 16382, + "type": "string" + } + ] + }, + "derivedProof": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "premisesTruncated": { + "const": false + } + }, + "required": [ + "premisesTruncated" + ] + }, + "then": { + "properties": { + "premises": { + "minItems": 1 + } + } + } + }, + { + "if": { + "properties": { + "premisesTruncated": { + "const": true + } + }, + "required": [ + "premisesTruncated" + ] + }, + "then": { + "properties": { + "premises": { + "maxItems": 63 + } + } + } + } + ], + "properties": { + "kind": { + "const": "derived" + }, + "premises": { + "items": { + "$ref": "#/$defs/proof" + }, + "maxItems": 64, + "type": "array" + }, + "premisesTruncated": { + "type": "boolean" + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "ruleId": { + "$ref": "#/$defs/safeCode" + }, + "ruleSha256": { + "$ref": "#/$defs/sha256" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "premises", + "premisesTruncated", + "relation", + "ruleId", + "ruleSha256", + "tuple", + "v" + ], + "type": "object" + }, + "evaluation": { + "additionalProperties": false, + "properties": { + "maximumDerivedTuples": { + "maximum": 262144, + "minimum": 1, + "type": "integer" + }, + "maximumProofDepth": { + "maximum": 128, + "minimum": 1, + "type": "integer" + }, + "maximumProofNodes": { + "maximum": 4096, + "minimum": 1, + "type": "integer" + }, + "maximumResultBytes": { + "maximum": 16777216, + "minimum": 65536, + "type": "integer" + }, + "maximumRounds": { + "maximum": 1024, + "minimum": 1, + "type": "integer" + }, + "maximumTotalProofNodes": { + "maximum": 65536, + "minimum": 1, + "type": "integer" + }, + "maximumWorkUnits": { + "maximum": 16777216, + "minimum": 1, + "type": "integer" + }, + "v": { + "const": 1 + } + }, + "required": [ + "maximumDerivedTuples", + "maximumProofDepth", + "maximumProofNodes", + "maximumResultBytes", + "maximumRounds", + "maximumTotalProofNodes", + "maximumWorkUnits", + "v" + ], + "type": "object" + }, + "engineCode": { + "maxLength": 256, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "factProof": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "fact" + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "sources": { + "items": { + "$ref": "#/$defs/source" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "relation", + "sources", + "tuple", + "v" + ], + "type": "object" + }, + "proof": { + "oneOf": [ + { + "$ref": "#/$defs/factProof" + }, + { + "$ref": "#/$defs/derivedProof" + }, + { + "$ref": "#/$defs/truncatedProof" + } + ] + }, + "row": { + "additionalProperties": false, + "properties": { + "proofs": { + "items": { + "$ref": "#/$defs/proof" + }, + "maxItems": 64, + "type": "array" + }, + "proofsTruncated": { + "type": "boolean" + }, + "supportCount": { + "maximum": 262144, + "minimum": 1, + "type": "integer" + }, + "values": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "proofs", + "proofsTruncated", + "supportCount", + "values", + "v" + ], + "type": "object" + }, + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + "source": { + "additionalProperties": false, + "properties": { + "key": { + "maxLength": 512, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "recordSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "key", + "recordSha256", + "v" + ], + "type": "object" + }, + "stats": { + "additionalProperties": false, + "properties": { + "baseFacts": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "derivedFacts": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "proofNodes": { + "maximum": 65536, + "minimum": 0, + "type": "integer" + }, + "proofsTruncated": { + "type": "boolean" + }, + "queryMatches": { + "maximum": 262144, + "minimum": 0, + "type": "integer" + }, + "relations": { + "maximum": 4096, + "minimum": 0, + "type": "integer" + }, + "rounds": { + "maximum": 1024, + "minimum": 0, + "type": "integer" + }, + "truncated": { + "type": "boolean" + }, + "truncationReasons": { + "items": { + "enum": [ + "query-limit", + "result-bytes" + ] + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "v": { + "const": 1 + }, + "workUnits": { + "maximum": 16777216, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "baseFacts", + "derivedFacts", + "proofNodes", + "proofsTruncated", + "queryMatches", + "relations", + "rounds", + "truncated", + "truncationReasons", + "v", + "workUnits" + ], + "type": "object" + }, + "truncatedProof": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "truncated" + }, + "reason": { + "enum": [ + "cycle", + "depth", + "nodes" + ] + }, + "relation": { + "$ref": "#/$defs/safeCode" + }, + "tuple": { + "$ref": "#/$defs/tuple" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "reason", + "relation", + "tuple", + "v" + ], + "type": "object" + }, + "tuple": { + "items": { + "$ref": "#/$defs/atom" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + } + }, + "$id": "https://oh.computer/spec/v1/projection-result.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally checks canonical row and source order, atom byte limits, declared aggregate limits, proof depth and node totals, truncation consistency, engineSha256, evaluationSha256, projectionSha256, and resultSha256.", + "additionalProperties": false, + "properties": { + "authority": { + "const": "derived" + }, + "cache": { + "additionalProperties": false, + "properties": { + "strategy": { + "const": "full-rebuild" + }, + "v": { + "const": 1 + } + }, + "required": [ + "strategy", + "v" + ], + "type": "object" + }, + "engine": { + "$ref": "#/$defs/engineCode" + }, + "evaluation": { + "$ref": "#/$defs/evaluation" + }, + "identity": { + "$ref": "./projection-identity.schema.json" + }, + "resultSha256": { + "$ref": "#/$defs/sha256" + }, + "rows": { + "items": { + "$ref": "#/$defs/row" + }, + "maxItems": 65536, + "type": "array" + }, + "stats": { + "$ref": "#/$defs/stats" + }, + "v": { + "const": 1 + } + }, + "required": [ + "authority", + "cache", + "engine", + "evaluation", + "identity", + "resultSha256", + "rows", + "stats", + "v" + ], + "title": "Oh projection result V1", + "type": "object" +} diff --git a/spec/v1/projection-rule-pack.schema.json b/spec/v1/projection-rule-pack.schema.json new file mode 100644 index 0000000..4864012 --- /dev/null +++ b/spec/v1/projection-rule-pack.schema.json @@ -0,0 +1,182 @@ +{ + "$defs": { + "atom": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "number" + }, + { + "maxLength": 16382, + "type": "string" + } + ] + }, + "constantTerm": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "constant" + }, + "v": { + "const": 1 + }, + "value": { + "$ref": "#/$defs/atom" + } + }, + "required": [ + "kind", + "v", + "value" + ], + "type": "object" + }, + "literal": { + "additionalProperties": false, + "properties": { + "relation": { + "$ref": "#/$defs/safeCode" + }, + "terms": { + "items": { + "$ref": "#/$defs/term" + }, + "maxItems": 32, + "minItems": 1, + "type": "array" + }, + "v": { + "const": 1 + } + }, + "required": [ + "relation", + "terms", + "v" + ], + "type": "object" + }, + "rule": { + "additionalProperties": false, + "properties": { + "body": { + "items": { + "$ref": "#/$defs/literal" + }, + "maxItems": 64, + "minItems": 1, + "type": "array" + }, + "head": { + "$ref": "#/$defs/literal" + }, + "ruleId": { + "$ref": "#/$defs/safeCode" + }, + "ruleSha256": { + "$ref": "#/$defs/sha256" + }, + "v": { + "const": 1 + } + }, + "required": [ + "body", + "head", + "ruleId", + "ruleSha256", + "v" + ], + "type": "object" + }, + "safeCode": { + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*(?:[._:/-][a-z0-9]+)*$", + "type": "string" + }, + "sha256": { + "pattern": "^[a-f0-9]{64}$", + "type": "string" + }, + "term": { + "oneOf": [ + { + "$ref": "#/$defs/constantTerm" + }, + { + "$ref": "#/$defs/variableTerm" + } + ] + }, + "variableTerm": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "variable" + }, + "name": { + "$ref": "#/$defs/safeCode" + }, + "v": { + "const": 1 + } + }, + "required": [ + "kind", + "name", + "v" + ], + "type": "object" + } + }, + "$id": "https://oh.computer/spec/v1/projection-rule-pack.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Runtime conformance additionally measures each atom's exact 16 KiB canonical-JSON byte ceiling, requires safe head variables, canonical rule ordering, unique rule IDs, and recomputed rule and pack digests.", + "additionalProperties": false, + "properties": { + "rulePackId": { + "$ref": "#/$defs/safeCode" + }, + "rulePackRevision": { + "minimum": 1, + "type": "integer" + }, + "rulePackSha256": { + "$ref": "#/$defs/sha256" + }, + "rules": { + "items": { + "$ref": "#/$defs/rule" + }, + "maxItems": 1024, + "minItems": 1, + "type": "array" + }, + "rulesSha256": { + "$ref": "#/$defs/sha256" + }, + "semantics": { + "const": "oh.projection.positive-datalog.v1" + }, + "v": { + "const": 1 + } + }, + "required": [ + "rulePackId", + "rulePackRevision", + "rulePackSha256", + "rules", + "rulesSha256", + "semantics", + "v" + ], + "title": "Oh projection rule pack V1", + "type": "object" +} diff --git a/spec/v1/projection.md b/spec/v1/projection.md new file mode 100644 index 0000000..fe32ab7 --- /dev/null +++ b/spec/v1/projection.md @@ -0,0 +1,165 @@ +# Derived projections + +Oh projections are reproducible, non-authoritative views over one exact graph +snapshot. The runtime exposes the same typed rule, query, identity, and result +contract through `@hraness/oh/projection` without changing the V1 graph record, +operation, canonical JSON, or SQLite formats. + +## Authority boundary + +A projection result has `authority: "derived"`. Its tuples and proof trees are +cache output. They MUST NOT be interpreted as graph assertions, review +decisions, accepted knowledge, or operation history. An application that wants +to retain a conclusion MUST create and review new graph records through its +ordinary authority path. + +The projection module imports no SQLite runtime. A caller supplies an exact +snapshot assembled from the authority it selected. This keeps the same contract +usable in a Node 24 serverless process, a local agent, or an application-owned +remote-store adapter. + +## Exact input identity + +`createOhProjectionSnapshotV1` accepts a space ID, current head, and complete +record snapshot. It: + +1. parses every record under the unchanged V1 graph contract; +2. sorts record references by logical key; +3. recomputes `recordsSha256` and requires it to equal the head; +4. checks dependency closure; and +5. hashes the contract digest, space, head, and record references into + `snapshotSha256`. + +A fact is one relation tuple plus one or more exact source record keys and +digests. A dataset rejects a source that is not current at its snapshot. +Duplicate relation tuples are coalesced and retain the union of their source +records. The dataset identity binds: + +- the snapshot digest; +- a fact-pack ID and revision; +- the SHA-256 digest of the application-owned extractor implementation; and +- the canonical ordered fact set. + +Oh supplies structural `oh.record(key, kind, digest)` and +`oh.dependency(key, dependency)` facts. Domain packs may emit richer relations, +but their extractor digest and every source record remain explicit. The +package-owned structural extractor profile is published as +`OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1`. + +## Positive rule semantics + +A term is a variable or a JSON primitive constant. A literal names one relation +and an ordered term list. A rule has one head and a nonempty conjunction of +positive body literals. Every head variable MUST occur in its body. Relation +arity MUST be consistent across base facts, rule heads, rule bodies, and the +query. + +The V1 projection evaluator implements finite, positive Datalog with set +semantics and synchronous naïve fixpoint rounds. It supports recursive rules. +It does not support negation, aggregation, arithmetic, function symbols, or +callbacks inside rules. Unknown objects are parsed with exact keys, so an +unsupported operator fails rather than being ignored. + +Rule packs are sorted by rule ID and content-addressed. A query declares an +ordered `find` variable list, a nonempty positive body, and an output limit. +Query results use set semantics and sort tuples by canonical JSON. Declaration, +fact, and insertion order do not affect rule-pack identity or output bytes. +The projection identity also binds the selected engine and resolved evaluation +limits, so results created with different engines, proof budgets, or work +budgets cannot share a cache identity. + +## Evaluation limits + +The implementation checks hard ceilings before or during work: + +| Item | Maximum | +| --- | ---: | +| Tuple arity | 32 | +| Canonical bytes per atom | 16 KiB | +| Base facts | 262,144 | +| Derived tuples | 262,144 | +| Rules | 1,024 | +| Body literals per rule | 64 | +| Evaluation rounds | 1,024 | +| Join matches per rule or query body | 262,144 | +| Tuple-unification work units per evaluation | 16,777,216 | +| Returned query rows | 65,536 | +| Proof depth | 128 | +| Proof nodes per row | 4,096 | +| Proof nodes across returned rows | 65,536 | +| Canonical result bytes | 16 MiB | + +A caller may request smaller derived-tuple, round, proof-depth, proof-node, and +global tuple-unification work bounds. The global work counter spans every rule, +round, and the final query, including unsuccessful candidate matches. Exceeding +a work bound fails closed. Result construction additionally stops before the +aggregate proof-node or canonical-byte ceilings. A query's declared output +limit or the result-byte ceiling returns a canonical prefix, sets +`stats.truncated: true`, and lists `query-limit` or `result-bytes` in +`stats.truncationReasons`. + +## Proofs + +Each returned row carries one proof for each literal in one canonical supporting +query-body match. `supportCount` reports how many complete matches produced the +same projected value tuple; V1 deliberately does not serialize every alternate +witness. A fact leaf names its relation, tuple, and exact source record +references. A derived node names the rule ID and digest and recursively contains +its premises. Depth and cycle guards emit an explicit `truncated` node. If a +node or byte budget ends between sibling premises, the enclosing derived node +sets `premisesTruncated: true`; if it ends between query-body proofs, the row +sets `proofsTruncated: true`. +`stats.proofsTruncated` reports either form across all returned rows. A proof +establishes how the bounded evaluator derived a tuple from the supplied bytes; +it does not establish that a proposition is true. + +## Safe cached ingress + +Projection declarations and cache output are untrusted exchange data. The +`parseOhProjectionRulePackV1`, `parseOhProjectionQueryV1`, and +`parseOhProjectionIdentityV1` parsers reject unknown keys and invalid digest +preimages. `parseOhProjectionProofV1` additionally applies the public proof +depth, node, tuple, source, atom-byte, and aggregate-byte ceilings before +returning a proof tree. + +`parseOhProjectionResultV1` is the cache-ingress boundary. It verifies the +result digest; canonical row and source order; `supportCount`; proof-node, +work-unit, relation, match, round, and byte totals; every proof and result +truncation marker; and all declared evaluation ceilings. It also recomputes the +engine and evaluation digests and requires them to match the projection +identity. A valid SHA-256 string by itself is not enough to make an envelope +acceptable. Cache readers SHOULD pass the projection digest they requested as +the parser's second argument; an internally consistent envelope does not prove +that a cache returned the requested identity. + +The discovery manifest publishes machine-readable schemas for rule packs, +queries, identities, and result envelopes. JSON Schema describes the exchange +shape and static maxima. The runtime parsers remain normative for canonical +ordering, digest preimages, aggregate budgets, and cross-field consistency that +the schemas cannot express. + +## Cache invalidation + +`projectionSha256` binds the current contract, snapshot, dataset, rule pack, +query, engine, resolved evaluation limits, and positive-Datalog semantics. A cached +result is reusable only when that digest is unchanged. Any snapshot, dataset, +rule-pack, query, engine, or evaluation-limit change has `kind: "full-rebuild"` and +lists the changed identities. V1 does not claim incremental deletion or +cross-snapshot maintenance. + +## Optional Suss equivalence lane + +`@hraness/oh/experimental/projection-suss` supports exactly +`@suss/datalog@0.20.0` as an optional peer. It encodes every JSON primitive atom +into canonical JSON text, evaluates the positive rules with Suss, and compares +every complete relation to the Oh reference semantics. It returns only after +exact set agreement. + +Suss's public evaluator does not expose an execution-budget hook. Before calling +it, the adapter first runs the bounded reference evaluator, then computes a +conservative finite-domain upper bound on new tuples in rule-head relations and +refuses a program it cannot prove will remain under the requested derived-tuple +ceiling. It compares Suss's complete result to the reference materialization and +uses the reference witnesses for canonical proof construction. This lane +evaluates compatibility, not performance. Refusal does not disable the built-in +evaluator. diff --git a/spec/v1/storage.md b/spec/v1/storage.md index 085d4c4..8cc36e5 100644 --- a/spec/v1/storage.md +++ b/spec/v1/storage.md @@ -1,6 +1,6 @@ # SQLite storage V1 -SQLite schema version `1` is the local authority for an Oh space. The default +SQLite schema version `2` is the local authority for an Oh space. The default CLI database is `.oh/oh.sqlite`; callers may select another path or use an in-memory database. @@ -34,10 +34,37 @@ rolls back the transaction and preserves the original error. | `oh_sync_state` | Last settled state for a named remote. | | `oh_search_documents` | Derived keyword text bound to a record digest. | | `oh_search_fts` | Derived FTS5 index. | +| `oh_space_bindings` | Host-selected realm, lifecycle profile, capabilities, and application-profile digest for a supported store port. | +| `oh_space_purges` | Minimal receipt that permanently reserves the identifier of a purged working space. | -The first migration is named `0001_oh_core`. An implementation MUST store and -check the exact SHA-256 digest of applied migration SQL. It MUST refuse to run -when the same migration version or name has different bytes. +The first migration is named `0001_oh_core`; its released bytes remain +unchanged. Schema version 2 adds `0002_store_realms`. An implementation MUST +store and check the exact SHA-256 digest of applied migration SQL. It MUST +refuse to run when the same migration version or name has different bytes. + +## Realm profiles and purge + +A promise-based store port MAY bind one space to one host-selected realm and +profile. The binding includes the exact Oh contract digest, an optional +application-profile digest, and declared capabilities. A supported runtime +MUST reject a later attempt to open the same space under different binding +bytes. A working profile disables operation replication and enables only +host-controlled whole-space purge. A canonical profile cannot be purged by +that API. + +Purge deletes the space head, complete operation history, current records, +dependency and operation materializations, sync state, and derived keyword +rows in one immediate transaction. It leaves only a content-free receipt with +the prior head, binding digest, purge instant, and receipt digest. The purged +space identifier cannot be reopened in the same database. A host that deletes +an entire database file MUST retain any required deletion evidence in its own +control plane. + +Realm and profile binding is additive store control metadata. V1 operation +digest preimages do not contain the binding. It therefore protects supported +opens and operations but is not a portable cryptographic claim about a V1 +history. Such a claim requires a new wire contract rather than a change to V1 +operation bytes. ## Authority and derivation diff --git a/spec/v1/store.md b/spec/v1/store.md new file mode 100644 index 0000000..85aa95e --- /dev/null +++ b/spec/v1/store.md @@ -0,0 +1,117 @@ +# Store ports, profiles, and direct libSQL authority + +The graph and operation contract remains V1. This document defines additive +host APIs that preserve those bytes across local SQLite and direct libSQL +authorities. + +## Promise-based store port + +`@hraness/oh/store` has no dependency on `bun:sqlite`. Its methods return +promises and expose: + +- the current exact head; +- a current or historical snapshot at an exact sequence and operation digest; +- a bounded contiguous change page through a pinned head; +- compare-and-swap commit; +- dependency-closure export; +- replay and materialization verification; and +- close. + +A historical read MUST fail if its sequence is absent or identifies a +different operation digest. A change page MUST name its source cursor, pinned +through-head, returned cursor, and whether more operations remain. + +## Semantic bundle ingress + +Model-facing code SHOULD use `OhSemanticBundleIngressV1` instead of generic +record puts. The ingress seals its codec registry, requires a registered codec +for every put kind, parses all values, creates canonical record envelopes, and +submits all puts and tombstones as one compare-and-swap operation. Missing +codecs, invalid values, duplicate keys, stale heads, and incomplete dependency +closures fail before the authority head moves. + +## Profiles and host control + +A binding combines one exact contract, application profile, lifecycle profile, +realm, and space. The built-in canonical profile permits operation replication +and forbids whole-space purge. The built-in working profile forbids operation +replication and permits purge. + +Creating an authority returns separate `store` and `host` objects. The ordinary +store never has a purge method. Trusted control-plane code retains the host +object and does not pass it to an agent tool or model. This is an API and +custody boundary, not protection from code that already holds raw database +credentials or direct filesystem access. + +## Dependency-closure capsules + +A closure export pins the source binding and exact head, sorts unique roots, +and includes exactly the records reachable through declared dependencies. The +capsule digest binds all of those fields. Verification rejects a missing +dependency, an extra unrelated record, a changed record digest, reordered +roots or records, and a false capsule digest. V1 bounds a capsule to 1,024 +roots, 8,192 records, and 67,108,864 canonical UTF-8 bytes. + +A closure is content evidence for a later reviewed adoption. It does not copy +source authority, review state, credentials, or operation history into a +destination. + +## Direct libSQL authority + +`@hraness/oh/libsql` accepts the `execute` and transactional `batch` shape of +`@libsql/client`. It is Node 24 and serverless compatible and does not import +`bun:sqlite`. Unlike the V1 sync transport, it treats libSQL as the current +record and operation authority. + +`bootstrapOhLibSqlAuthorityV1` is the only API that creates schema objects. +Run it in a deployment or migration step with a short-lived schema credential. +`createOhLibSqlStoreAuthorityV1` is the runtime open: it only verifies the +installed schema and contract before reading or creating a bound data space, +so a runtime token does not need schema-change permission. + +Runtime open verifies the exact installed table, index, and trigger set, not +only a schema marker. Every operation, binding, and purge receipt read parses +its canonical JSON and cross-checks each duplicated SQL column. Current reads +also prove contiguous operation coverage through the exact terminal head, +record provenance puts, record digests, and dependency materialization. + +Its private implementation tables use the `oh_authority_` prefix: + +| Table | Role | +| --- | --- | +| `oh_authority_schemas` | Exact adapter schema name and digest. | +| `oh_authority_contracts` | Exact Oh contract manifest. | +| `oh_authority_spaces` | Current compare-and-swap heads. | +| `oh_authority_operations` | Canonical append-only operations. | +| `oh_authority_operation_records` | Ordered changes per operation. | +| `oh_authority_records` | Current record materialization. | +| `oh_authority_dependencies` | Current dependency edges. | +| `oh_authority_bindings` | Realm and profile control metadata. | +| `oh_authority_purges` | Minimal whole-space purge receipts. | +| `oh_authority_commit_guards` | Empty constraint table used to abort a stale transactional batch. | + +A normal remote commit takes three atomic provider round trips: an idempotency, +head, and purge preflight; one exact current-materialization read; and one +guarded write batch. The final write guard aborts the complete transaction when +compare-and-swap did not settle at the declared operation. Its write-batch +readback must reproduce both the canonical operation and persisted head before +the adapter returns success. + +Provider responses are bounded as part of the API. V1 accepts at most 64 +changes, 512 dependencies, and 512 KiB of canonical operation JSON per commit. +A feed returns at most seven operations plus one checked sentinel and refuses a +page whose conservative transport estimate exceeds 9,000,000 bytes. Historical +replay is limited to 16,384 operations, 4 MiB of canonical operation JSON, and +the same response estimate. Current snapshot and full-verification result sets +are independently transport-estimated and SQL-gated before rows are returned. +Sizing, rows, and the pinned head are read in the same transaction, so a +concurrent append cannot grow an unchecked response between preflight and read. + +Remote purge similarly inserts a receipt only for the expected working head, +deletes every payload and materialization row under that receipt in the same +write batch, and aborts if either the receipt or deletion is incomplete. A +later open returns the stored purge receipt instead of recreating the space. +Operation-record deletion resolves ownership through the canonical operation; +the purge postcondition also rejects any global orphan or cross-space owner +mismatch. Purge receipts are immutable and intentionally retain only binding, +prior-head, and purge-event evidence. diff --git a/src/cli.test.ts b/src/cli.test.ts index bb86020..e3e80c9 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -93,7 +93,7 @@ describe("oh CLI", () => { expect(result.code).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ manifest: { contractId: "oh.ontology.v1" }, - sqliteSchemaVersion: 1, + sqliteSchemaVersion: 2, v: 1, }); expect(existsSync(join(root, ".oh"))).toBe(false); diff --git a/src/cli.ts b/src/cli.ts index a5f5a72..280f2d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ import { Oh } from "./sdk"; import { OH_SQLITE_SCHEMA_VERSION } from "./sqlite/migrations"; import { createOhSyncBundleV1, parseOhSyncBundleV1 } from "./sync"; -export const OH_PACKAGE_VERSION = "0.1.1" as const; +export const OH_PACKAGE_VERSION = "0.2.0" as const; type ParsedArguments = { options: Map; positionals: string[] }; type ValidatedInvocation = Readonly<{ diff --git a/src/contract.ts b/src/contract.ts index 9ea18f7..e626dd5 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -44,10 +44,12 @@ export type OhRecordCodec = Readonly<{ /** Optional semantic validation layered over the immutable generic record envelope. */ export class OhRecordCodecRegistry { readonly #codecs = new Map(); + #sealed = false; register(codec: OhRecordCodec): this { + if (this.#sealed) throw new TypeError("The codec registry is sealed."); if (this.#codecs.has(codec.kind)) throw new TypeError(`A codec is already registered for ${codec.kind}.`); - this.#codecs.set(codec.kind, codec); + this.#codecs.set(codec.kind, Object.freeze({ kind: codec.kind, parse: codec.parse })); return this; } @@ -60,4 +62,26 @@ export class OhRecordCodecRegistry { has(kind: KnowledgeGraphRecordKindV1): boolean { return this.#codecs.has(kind); } + + /** Parses only through an explicitly registered codec. */ + parseRequired(kind: KnowledgeGraphRecordKindV1, value: unknown): JsonValue | null { + const codec = this.#codecs.get(kind); + if (codec === undefined) return null; + try { + const parsed = codec.parse(value); + if (parsed === null) return null; + canonicalJson(parsed); + return parsed; + } catch { return null; } + } + + /** Prevents the validation policy from changing after an ingress is created. */ + seal(): this { + this.#sealed = true; + return this; + } + + get sealed(): boolean { + return this.#sealed; + } } diff --git a/src/index.ts b/src/index.ts index 2d2ab6a..8424d86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,4 +4,5 @@ export * from "./graph"; export * from "./ontology"; export * from "./operation"; export * from "./schema"; +export * from "./store"; export * from "./sync"; diff --git a/src/libsql.test.ts b/src/libsql.test.ts new file mode 100644 index 0000000..512f4cf --- /dev/null +++ b/src/libsql.test.ts @@ -0,0 +1,463 @@ +import { describe, expect, test } from "bun:test"; +import { Database, type SQLQueryBindings } from "bun:sqlite"; + +import { canonicalJson } from "./canonical"; +import { createKnowledgeGraphRecordV1 } from "./graph"; +import { + bootstrapOhLibSqlAuthorityV1, + createOhLibSqlStoreAuthorityV1, + OH_LIBSQL_STORE_LIMITS_V1, + type OhLibSqlClientV1, + type OhLibSqlResultV1, + type OhLibSqlStatementV1, +} from "./libsql"; +import { + OH_CANONICAL_STORE_PROFILE_V1, + OH_WORKING_STORE_PROFILE_V1, + OhConflictError, + OhIntegrityError, + OhProfileError, + OhPurgedSpaceError, +} from "./store"; + +class SqliteCompatibleLibSqlClient implements OhLibSqlClientV1 { + readonly database = new Database(":memory:", { strict: true }); + lastBatchResponseBytes = 0; + + #execute(statement: OhLibSqlStatementV1 | string): OhLibSqlResultV1 { + const sql = typeof statement === "string" ? statement : statement.sql; + const args = typeof statement === "string" ? [] : statement.args ?? []; + const bindings: SQLQueryBindings[] = args.map((value) => value instanceof Date + ? value.toISOString() : value instanceof ArrayBuffer ? new Uint8Array(value) : value); + if (/^\s*(?:SELECT|PRAGMA)\b/iu.test(sql)) { + return { rows: this.database.query, SQLQueryBindings[]>(sql).all(...bindings) }; + } + const result = this.database.query(sql).run(...bindings); + return { rows: [], rowsAffected: result.changes }; + } + + async execute(statement: OhLibSqlStatementV1 | string): Promise { + return this.#execute(statement); + } + + async batch( + statements: readonly OhLibSqlStatementV1[], + _mode?: "deferred" | "read" | "write", + ): Promise { + const results = this.database.transaction((items: readonly OhLibSqlStatementV1[]) => + items.map((statement) => this.#execute(statement)))(statements); + this.lastBatchResponseBytes = Buffer.byteLength(JSON.stringify(results), "utf8"); + return results; + } + + close(): void { this.database.close(); } +} + +const entity = (key: string, name: string, dependencies: readonly string[] = []) => + createKnowledgeGraphRecordV1({ dependencies, key, kind: "entity", v: 1, value: { name } }); + +async function bootstrappedClient(): Promise { + const client = new SqliteCompatibleLibSqlClient(); + expect(await bootstrapOhLibSqlAuthorityV1(client)).toMatchObject({ schemaVersion: 1, v: 1 }); + return client; +} + +describe("direct libSQL Oh authority", () => { + test("refuses to bless preexisting or drifted authority schema objects", async () => { + const malformed = new SqliteCompatibleLibSqlClient(); + malformed.database.exec("CREATE TABLE oh_authority_commit_guards(value TEXT)"); + await expect(bootstrapOhLibSqlAuthorityV1(malformed)).rejects.toThrow(OhIntegrityError); + malformed.close(); + + const drifted = await bootstrappedClient(); + drifted.database.exec(`CREATE TRIGGER unexpected_authority_trigger + BEFORE INSERT ON oh_authority_records BEGIN SELECT 1; END`); + await expect(createOhLibSqlStoreAuthorityV1(drifted, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:drift", spaceId: "drift", + })).rejects.toThrow(OhIntegrityError); + drifted.close(); + }); + + test("does not bootstrap schema as a side effect of the runtime open", async () => { + const client = new SqliteCompatibleLibSqlClient(); + await expect(createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:missing", spaceId: "missing", + })).rejects.toThrow(); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count FROM sqlite_schema + WHERE name LIKE 'oh_authority_%'`).get()?.count).toBe(0); + client.close(); + }); + + test("opens with a data-only runtime client after one explicit schema bootstrap", async () => { + const schemaClient = await bootstrappedClient(); + const rejectDdl = (statement: OhLibSqlStatementV1 | string): void => { + const sql = typeof statement === "string" ? statement : statement.sql; + if (/^\s*(?:ALTER|CREATE|DROP|REINDEX|VACUUM)\b/iu.test(sql)) { + throw new Error("runtime credential cannot execute schema DDL"); + } + }; + const runtimeClient: OhLibSqlClientV1 = { + execute: async (statement) => { + rejectDdl(statement); + return await schemaClient.execute(statement); + }, + batch: async (statements, mode) => { + statements.forEach(rejectDdl); + return await schemaClient.batch(statements, mode); + }, + }; + const authority = await createOhLibSqlStoreAuthorityV1(runtimeClient, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:data-only", spaceId: "data-only", + }); + expect((await authority.store.head()).sequence).toBe(0); + await authority.store.close(); + schemaClient.close(); + }); + + test("is an async authoritative store rather than an operation-sync cache", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:remote", spaceId: "remote", + }); + const parent = entity("entity:parent", "Parent"); + const child = entity("entity:child", "Child", [parent.key]); + const first = await authority.store.commit({ actorId: "agent.remote", changes: [ + { kind: "put", record: parent, v: 1 }, { kind: "put", record: child, v: 1 }, + ], expectedHead: await authority.store.head(), instant: "2026-08-29T12:00:00.000Z", + operationId: "op_remote_one" }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:later", "Later"), v: 1 }], expectedHead: await authority.store.head(), + instant: "2026-08-29T12:01:00.000Z", operationId: "op_remote_two" }); + + expect((await authority.store.snapshot({ head: { + operationSha256: first.operationSha256, sequence: first.sequence } })).records) + .toEqual([child, parent].sort((left, right) => left.key.localeCompare(right.key))); + expect(await authority.store.snapshot({ head: { operationSha256: null, sequence: 0 } })) + .toMatchObject({ head: { operationSha256: null, sequence: 0 }, records: [] }); + expect((await authority.store.exportDependencyClosure({ roots: [child.key] })).records) + .toEqual([child, parent].sort((left, right) => left.key.localeCompare(right.key))); + expect(await authority.store.verify()).toMatchObject({ integrity: "verified", operations: 2, records: 3 }); + + const reopened = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:remote", spaceId: "remote", + }); + expect((await reopened.store.head()).operationSha256).toBe((await authority.store.head()).operationSha256); + await reopened.store.close(); + await authority.store.close(); + client.close(); + }); + + test("uses compare-and-swap guards to leave no partial remote mutation", async () => { + const client = await bootstrappedClient(); + const first = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:cas", spaceId: "cas", + }); + const second = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:cas", spaceId: "cas", + }); + const stale = await second.store.head(); + await first.store.commit({ actorId: "agent.one", changes: [{ kind: "put", + record: entity("entity:first", "First"), v: 1 }], expectedHead: stale, + operationId: "op_first" }); + await expect(second.store.commit({ actorId: "agent.two", changes: [{ kind: "put", + record: entity("entity:stale", "Stale"), v: 1 }], expectedHead: stale, + operationId: "op_stale" })).rejects.toThrow(OhConflictError); + expect((await first.store.snapshot()).records.map(({ key }) => key)).toEqual(["entity:first"]); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_operations WHERE operation_id = 'op_stale'`).get()?.count).toBe(0); + await first.store.close(); await second.store.close(); client.close(); + }); + + test("rejects orphaned idempotency rows and duplicated binding-column drift", async () => { + const sourceClient = await bootstrappedClient(); + const source = await createOhLibSqlStoreAuthorityV1(sourceClient, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:remote-orphan", spaceId: "remote-orphan", + }); + const changes = [{ kind: "put" as const, record: entity("entity:orphan", "Orphan"), v: 1 as const }]; + const operation = await source.store.commit({ actorId: "agent.remote", changes, + expectedHead: await source.store.head(), operationId: "op_remote_orphan" }); + + const targetClient = await bootstrappedClient(); + const target = await createOhLibSqlStoreAuthorityV1(targetClient, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:remote-orphan", spaceId: "remote-orphan", + }); + targetClient.database.query(`INSERT INTO oh_authority_operations(operation_sha256, space_id, + sequence, operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, + operation_json, instant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + operation.operationSha256, operation.spaceId, operation.sequence, operation.operationId, + operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256, + canonicalJson(operation), operation.instant, + ); + await expect(target.store.commit({ actorId: operation.actorId, changes, + expectedHead: await target.store.head(), operationId: operation.operationId })) + .rejects.toThrow(OhIntegrityError); + expect((await target.store.head()).sequence).toBe(0); + targetClient.database.query("UPDATE oh_authority_bindings SET realm_id = ? WHERE space_id = ?") + .run("realm:alien", "remote-orphan"); + await expect(createOhLibSqlStoreAuthorityV1(targetClient, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:remote-orphan", spaceId: "remote-orphan", + })).rejects.toThrow(OhIntegrityError); + await target.store.close(); await source.store.close(); targetClient.close(); sourceClient.close(); + }); + + test("refuses snapshots and commits when the current head drifts from its operation", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:head-drift", spaceId: "head-drift", + }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:one", "One"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_head_one" }); + client.database.query("UPDATE oh_authority_spaces SET graph_revision_sha256 = ? WHERE space_id = ?") + .run("f".repeat(64), "head-drift"); + const drifted = await authority.store.head(); + await expect(authority.store.snapshot()).rejects.toThrow(OhIntegrityError); + await expect(authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:two", "Two"), v: 1 }], expectedHead: drifted, + operationId: "op_head_two" })).rejects.toThrow(OhIntegrityError); + expect((await authority.store.head()).sequence).toBe(1); + await authority.store.close(); client.close(); + }); + + test("detects record-column and operation-record tampering before advancing authority", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:tamper", spaceId: "tamper", + }); + const record = entity("entity:tamper", "Tamper"); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", record, v: 1 }], + expectedHead: await authority.store.head(), operationId: "op_tamper" }); + const head = await authority.store.head(); + client.database.query(`UPDATE oh_authority_records SET record_sha256 = ? + WHERE space_id = ? AND record_key = ?`).run("f".repeat(64), "tamper", record.key); + await expect(authority.store.verify()).rejects.toThrow(OhIntegrityError); + await expect(authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "tombstone", + key: record.key, priorSha256: record.recordSha256, v: 1 }], expectedHead: head, + operationId: "op_after_tamper" })).rejects.toThrow(OhIntegrityError); + expect((await authority.store.head()).sequence).toBe(1); + client.database.query(`UPDATE oh_authority_records SET record_sha256 = ? + WHERE space_id = ? AND record_key = ?`).run(record.recordSha256, "tamper", record.key); + expect(() => client.database.query("DELETE FROM oh_authority_operation_records WHERE space_id = ?") + .run("tamper")).toThrow("require a purge receipt"); + client.database.exec("DROP TRIGGER oh_authority_operation_records_guard_delete"); + client.database.query("DELETE FROM oh_authority_operation_records WHERE space_id = ?").run("tamper"); + await expect(authority.store.verify()).rejects.toThrow(OhIntegrityError); + await authority.store.close(); client.close(); + }); + + test("refuses to extend noncanonical current-record provenance", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:provenance", spaceId: "provenance", + }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:first", "First"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_provenance_one" }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:second", "Second"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_provenance_two" }); + const row = client.database.query<{ operation_json: string }, []>(`SELECT operation_json + FROM oh_authority_operations WHERE operation_id = 'op_provenance_one'`).get(); + expect(row).not.toBeNull(); + const noncanonical = JSON.stringify(JSON.parse(row?.operation_json ?? "null"), null, 2); + expect(() => client.database.query(`UPDATE oh_authority_operations SET operation_json = ? + WHERE operation_id = 'op_provenance_one'`).run(noncanonical)).toThrow("immutable"); + client.database.exec("DROP TRIGGER oh_authority_operations_no_update"); + client.database.query(`UPDATE oh_authority_operations SET operation_json = ? + WHERE operation_id = 'op_provenance_one'`).run(noncanonical); + const head = await authority.store.head(); + await expect(authority.store.snapshot()).rejects.toThrow(OhIntegrityError); + await expect(authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:third", "Third"), v: 1 }], expectedHead: head, + operationId: "op_provenance_three" })).rejects.toThrow(OhIntegrityError); + expect((await authority.store.head()).sequence).toBe(2); + await authority.store.close(); client.close(); + }); + + test("never reports a remote feed that omits its tail or hides a bad sentinel", async () => { + const client = await bootstrappedClient(); + const populate = async (spaceId: string) => { + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: `realm:${spaceId}`, spaceId, + }); + for (let index = 1; index <= 3; index += 1) { + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity(`entity:${spaceId}-${index}`, `${spaceId} ${index}`), v: 1 }], + expectedHead: await authority.store.head(), operationId: `op_${spaceId}_${index}` }); + } + return authority; + }; + const tail = await populate("feed-tail"); + const sentinel = await populate("feed-sentinel"); + client.database.exec("DROP TRIGGER oh_authority_operations_guard_delete"); + client.database.query("DELETE FROM oh_authority_operations WHERE space_id = ? AND sequence = 3") + .run("feed-tail"); + await expect(tail.store.changesSince({ operationSha256: null, sequence: 0 }, { limit: 3 })) + .rejects.toThrow(OhIntegrityError); + client.database.query("DELETE FROM oh_authority_operations WHERE space_id = ? AND sequence = 2") + .run("feed-sentinel"); + await expect(sentinel.store.changesSince({ operationSha256: null, sequence: 0 }, { limit: 1 })) + .rejects.toThrow(OhIntegrityError); + await tail.store.close(); await sentinel.store.close(); client.close(); + }); + + test("enforces provider-safe operation, feed, and snapshot response bounds", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:provider-bounds", spaceId: "provider-bounds", + }); + await expect(authority.store.changesSince({ operationSha256: null, sequence: 0 }, { limit: 16 })) + .rejects.toThrow(RangeError); + const oversized = entity("entity:oversized", "x".repeat(600_000)); + await expect(authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: oversized, v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_oversized" })).rejects.toThrow("canonical byte bound"); + expect((await authority.store.head()).sequence).toBe(0); + + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:bounded", "Bounded"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_bounded" }); + client.database.query(`UPDATE oh_authority_records + SET record_json = json_object('blob', hex(zeroblob(2200000))) WHERE space_id = ?`) + .run("provider-bounds"); + await expect(authority.store.snapshot()).rejects.toThrow("provider-safe response bounds"); + await authority.store.close(); client.close(); + }); + + test("keeps escape-heavy feeds and accepted history below the libSQL response ceiling", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:response-ceiling", + spaceId: "response-ceiling", + }); + const escaped = "\\\"\n".repeat(50_000); + const heads = []; + for (let index = 1; index <= 14; index += 1) { + const operation = await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:response-ceiling", `${String(index).padStart(2, "0")}:${escaped}`), v: 1 }], + expectedHead: await authority.store.head(), operationId: `op_response_ceiling_${index}` }); + heads.push({ operationSha256: operation.operationSha256, sequence: operation.sequence }); + } + expect(OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit).toBe(7); + const page = await authority.store.changesSince({ operationSha256: null, sequence: 0 }); + expect(page.operations).toHaveLength(7); + expect(page.hasMore).toBe(true); + expect(client.lastBatchResponseBytes).toBeLessThan(10_000_000); + + const historical = await authority.store.snapshot({ head: heads[12]! }); + expect(historical.head.sequence).toBe(13); + expect(client.lastBatchResponseBytes).toBeLessThan(10_000_000); + await authority.store.close(); client.close(); + }); + + test("rejects profile drift and permanently purges a working realm with a receipt", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:purge", spaceId: "purge", + }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:private", "Private"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_private" }); + await expect(createOhLibSqlStoreAuthorityV1(client, { + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:other", spaceId: "purge", + })).rejects.toThrow(OhProfileError); + + const receipt = await authority.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" }); + expect(receipt.priorHead.sequence).toBe(1); + expect("purgeWorkingSpace" in authority.store).toBe(false); + for (const table of ["oh_authority_spaces", "oh_authority_bindings", "oh_authority_operations", + "oh_authority_operation_records", "oh_authority_records", "oh_authority_dependencies"]) { + const count = client.database.query<{ count: number }, []>(`SELECT count(*) AS count FROM ${table}`).get()?.count; + expect(count, table).toBe(0); + } + expect(client.database.query<{ count: number }, []>( + "SELECT count(*) AS count FROM oh_authority_purges WHERE space_id = 'purge'", + ).get()?.count).toBe(1); + await expect(createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:purge", spaceId: "purge", + })).rejects.toThrow(OhPurgedSpaceError); + client.close(); + }); + + test("rejects new cross-space operation records and cleans legacy mismatches by owner", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:owner-purge", spaceId: "owner-purge", + }); + const operation = await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:private", "Private"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_owner_private" }); + const insertMismatch = () => client.database.query(`INSERT INTO oh_authority_operation_records( + space_id, operation_sha256, ordinal, record_key, change_kind, record_sha256) + VALUES (?, ?, 1, ?, 'put', ?)`).run("alien-space", operation.operationSha256, + "entity:alien", "f".repeat(64)); + expect(insertMismatch).toThrow("no owning operation"); + client.database.exec("DROP TRIGGER oh_authority_operation_records_guard_insert"); + insertMismatch(); + await authority.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" }); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_operation_records`).get()?.count).toBe(0); + await authority.store.close(); client.close(); + }); + + test("cannot resurrect a space purged between open preflight and creation batch", async () => { + const client = await bootstrappedClient(); + const original = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:open-purge-race", spaceId: "open-purge-race", + }); + let interleaved = false; + const racingClient: OhLibSqlClientV1 = { + execute: async (statement) => { + const result = await client.execute(statement); + const sql = typeof statement === "string" ? statement : statement.sql; + const firstArgument = typeof statement === "string" ? undefined : statement.args?.[0]; + if (!interleaved && firstArgument === "open-purge-race" + && sql.includes("FROM oh_authority_purges WHERE space_id = ?")) { + interleaved = true; + await original.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" }); + } + return result; + }, + batch: async (statements, mode) => await client.batch(statements, mode), + }; + await expect(createOhLibSqlStoreAuthorityV1(racingClient, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:open-purge-race", spaceId: "open-purge-race", + })).rejects.toThrow(OhPurgedSpaceError); + expect(interleaved).toBe(true); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_spaces WHERE space_id = 'open-purge-race'`).get()?.count).toBe(0); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_purges WHERE space_id = 'open-purge-race'`).get()?.count).toBe(1); + client.close(); + }); + + test("rolls purge back when any private payload row resists deletion", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:blocked-purge", spaceId: "blocked-purge", + }); + await authority.store.commit({ actorId: "agent.remote", changes: [{ kind: "put", + record: entity("entity:private", "Private"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_blocked_private" }); + client.database.exec(`CREATE TRIGGER block_record_delete BEFORE DELETE ON oh_authority_records + BEGIN SELECT RAISE(IGNORE); END`); + await expect(authority.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" })) + .rejects.toThrow(); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_purges WHERE space_id = 'blocked-purge'`).get()?.count).toBe(0); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_records WHERE space_id = 'blocked-purge'`).get()?.count).toBe(1); + expect(client.database.query<{ count: number }, []>(`SELECT count(*) AS count + FROM oh_authority_spaces WHERE space_id = 'blocked-purge'`).get()?.count).toBe(1); + await authority.store.close(); client.close(); + }); + + test("does not grant canonical stores a purge path", async () => { + const client = await bootstrappedClient(); + const authority = await createOhLibSqlStoreAuthorityV1(client, { + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:canonical", spaceId: "canonical", + }); + await expect(authority.host.purgeWorkingSpace({})).rejects.toThrow(OhProfileError); + await authority.store.close(); client.close(); + }); +}); diff --git a/src/libsql.ts b/src/libsql.ts new file mode 100644 index 0000000..32e9b31 --- /dev/null +++ b/src/libsql.ts @@ -0,0 +1,1488 @@ +import { + canonicalJson, + canonicalNow, + canonicalSha256, + parseSha256Hex, + safeCode, + utf8ByteLength, + type Sha256Hex, +} from "./canonical"; +import { OH_CONTRACT_MANIFEST_V1 } from "./contract"; +import { canonicalKnowledgeGraphChangesV1, knowledgeGraphRecordRefV1, OH_GRAPH_LIMITS_V1, + parseKnowledgeGraphRecordV1, + type KnowledgeGraphRecordV1 } from "./graph"; +import { parseOhOperationV1, type OhOperationV1 } from "./operation"; +import { + createOhDependencyClosureV1, + createOhSpacePurgeReceiptV1, + createOhStoreBindingV1, + emptyOhHeadV1, + OH_CANONICAL_STORE_PROFILE_V1, + OhConflictError, + OhIntegrityError, + OhProfileError, + OhPurgedSpaceError, + parseOhHeadRefV1, + parseOhSpacePurgeReceiptV1, + parseOhStoreBindingV1, + parseOhStoreProfileV1, + replayOhOperationsV1, + transitionOhSnapshotV1, + type OhChangesPageV1, + type OhCommitInputV1, + type OhDependencyClosureV1, + type OhHeadRefV1, + type OhHeadV1, + type OhSnapshotV1, + type OhSpacePurgeReceiptV1, + type OhStoreAuthorityV1, + type OhStoreBindingV1, + type OhStoreHostControlV1, + type OhStoreProfileV1, + type OhStoreV1, + type OhStoreVerificationV1, +} from "./store"; + +export type OhLibSqlValueV1 = ArrayBuffer | Date | Uint8Array | bigint | boolean | null | number | string; +export type OhLibSqlStatementV1 = Readonly<{ args?: readonly OhLibSqlValueV1[]; sql: string }>; +export type OhLibSqlResultV1 = Readonly<{ + rows: readonly (Readonly> | readonly unknown[])[]; + rowsAffected?: number; +}>; + +/** Structural subset implemented by `@libsql/client` clients. */ +export interface OhLibSqlClientV1 { + batch( + statements: readonly OhLibSqlStatementV1[], + mode?: "deferred" | "read" | "write", + ): Promise; + close?(): void; + execute(statement: OhLibSqlStatementV1 | string): Promise; +} + +export type OhLibSqlStoreAuthorityOptionsV1 = Readonly<{ + closeClient?: boolean; + profile?: OhStoreProfileV1; + realmId?: string; + spaceId?: string; +}>; + +export const OH_LIBSQL_STORE_LIMITS_V1 = Object.freeze({ + changesPerCommit: 64, + changeFeedLimit: 7, + dependenciesPerCommit: 512, + historyBytes: 4 * 1024 * 1024, + historyOperations: 16_384, + operationBytes: 512 * 1024, + providerResponseBytes: 9_000_000, + snapshotComponentBytes: 6 * 1024 * 1024, +}); + +const AUTHORITY_SCHEMA_NAME = "oh.libsql-authority.v1"; +const AUTHORITY_SCHEMA_VERSION = 1; +const EMPTY_RECORDS_SHA256 = canonicalSha256([]); +const PURGE_ROW_SELECT = `SELECT space_id, binding_sha256, prior_operation_sha256, + prior_sequence, purged_at, receipt_sha256, receipt_json + FROM oh_authority_purges WHERE space_id = ?`; +const BINDING_ROW_SELECT = `SELECT space_id, realm_id, profile_id, profile_kind, + profile_sha256, binding_sha256, binding_json FROM oh_authority_bindings WHERE space_id = ?`; +const OPERATION_ROW_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +// libSQL serializes text again in its JSON response. Twice the UTF-8 text plus all +// duplicated columns and a fixed row reserve is a conservative upper bound for +// canonical JSON, whose own escapes can only be escaped once more by transport. +const OPERATION_RESPONSE_BYTES = `2 * length(CAST(operation.operation_json AS BLOB)) + + 2 * (length(operation.operation_sha256) + length(operation.space_id) + + length(operation.operation_id) + coalesce(length(operation.parent_operation_sha256), 0) + + length(operation.graph_revision_sha256) + length(operation.records_sha256) + + length(operation.instant)) + 512`; +const RECORD_RESPONSE_BYTES = `2 * length(CAST(record.record_json AS BLOB)) + + 2 * (length(record.record_key) + length(record.kind) + length(record.record_sha256) + + length(record.operation_sha256)) + 384`; +const DEPENDENCY_RESPONSE_BYTES = `2 * (length(dependency.record_key) + + length(dependency.dependency_key)) + 192`; +const OPERATION_RECORD_RESPONSE_BYTES = `2 * (length(materialized.space_id) + + length(materialized.operation_sha256) + length(materialized.record_key) + + length(materialized.change_kind) + length(materialized.record_sha256)) + 320`; + +const AUTHORITY_SCHEMA_TABLE_STATEMENT = `CREATE TABLE IF NOT EXISTS oh_authority_schemas ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + schema_sha256 TEXT NOT NULL, + applied_at TEXT NOT NULL +) STRICT`; + +const AUTHORITY_SCHEMA_STATEMENTS = Object.freeze([ + `CREATE TABLE IF NOT EXISTS oh_authority_contracts ( + contract_id TEXT PRIMARY KEY, + contract_sha256 TEXT NOT NULL, + manifest_json TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_spaces ( + space_id TEXT PRIMARY KEY, + contract_id TEXT NOT NULL, + generation INTEGER NOT NULL CHECK(generation >= 0), + head_operation_sha256 TEXT, + graph_revision_sha256 TEXT, + records_sha256 TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence >= 0), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + CHECK(generation = sequence) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_operations ( + operation_sha256 TEXT PRIMARY KEY, + space_id TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence > 0), + operation_id TEXT NOT NULL, + parent_operation_sha256 TEXT, + graph_revision_sha256 TEXT NOT NULL, + records_sha256 TEXT NOT NULL, + operation_json TEXT NOT NULL, + instant TEXT NOT NULL, + UNIQUE(space_id, sequence), + UNIQUE(space_id, operation_id) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_operation_records ( + space_id TEXT NOT NULL, + operation_sha256 TEXT NOT NULL, + ordinal INTEGER NOT NULL CHECK(ordinal >= 0), + record_key TEXT NOT NULL, + change_kind TEXT NOT NULL CHECK(change_kind IN ('put', 'tombstone')), + record_sha256 TEXT NOT NULL, + PRIMARY KEY(operation_sha256, ordinal), + UNIQUE(operation_sha256, record_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_records ( + space_id TEXT NOT NULL, + record_key TEXT NOT NULL, + kind TEXT NOT NULL, + record_sha256 TEXT NOT NULL, + record_json TEXT NOT NULL, + operation_sha256 TEXT NOT NULL, + sequence INTEGER NOT NULL CHECK(sequence > 0), + PRIMARY KEY(space_id, record_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_dependencies ( + space_id TEXT NOT NULL, + record_key TEXT NOT NULL, + dependency_key TEXT NOT NULL, + PRIMARY KEY(space_id, record_key, dependency_key) + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_bindings ( + space_id TEXT PRIMARY KEY, + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL, + binding_sha256 TEXT NOT NULL UNIQUE, + binding_json TEXT NOT NULL, + created_at TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL, + prior_operation_sha256 TEXT, + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE, + receipt_json TEXT NOT NULL + ) STRICT`, + `CREATE TABLE IF NOT EXISTS oh_authority_commit_guards ( + value TEXT NOT NULL CHECK(value = 'ok') + ) STRICT`, + "CREATE INDEX IF NOT EXISTS oh_authority_operations_space_sequence ON oh_authority_operations(space_id, sequence)", + "CREATE INDEX IF NOT EXISTS oh_authority_records_space_kind ON oh_authority_records(space_id, kind, record_key)", + "CREATE INDEX IF NOT EXISTS oh_authority_dependencies_dependency ON oh_authority_dependencies(space_id, dependency_key)", + `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_no_update + BEFORE UPDATE ON oh_authority_operations + BEGIN SELECT RAISE(ABORT, 'Oh authority operations are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operations_guard_delete + BEFORE DELETE ON oh_authority_operations + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = OLD.space_id) + BEGIN SELECT RAISE(ABORT, 'Oh authority operations require a purge receipt'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_no_update + BEFORE UPDATE ON oh_authority_operation_records + BEGIN SELECT RAISE(ABORT, 'Oh authority operation records are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_insert + BEFORE INSERT ON oh_authority_operation_records + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations + WHERE operation_sha256 = NEW.operation_sha256 AND space_id = NEW.space_id) + BEGIN SELECT RAISE(ABORT, 'Oh authority operation record has no owning operation'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_operation_records_guard_delete + BEFORE DELETE ON oh_authority_operation_records + WHEN NOT EXISTS (SELECT 1 FROM oh_authority_operations AS operation + JOIN oh_authority_purges AS purge ON purge.space_id = operation.space_id + WHERE operation.operation_sha256 = OLD.operation_sha256) + BEGIN SELECT RAISE(ABORT, 'Oh authority operation records require a purge receipt'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_update + BEFORE UPDATE ON oh_authority_purges + BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`, + `CREATE TRIGGER IF NOT EXISTS oh_authority_purges_immutable_delete + BEFORE DELETE ON oh_authority_purges + BEGIN SELECT RAISE(ABORT, 'Oh authority purge receipts are immutable'); END`, +]); + +function normalizedSchemaSql(sql: string): string { + return sql.replace(/\bIF\s+NOT\s+EXISTS\b/giu, "").replace(/\s+/gu, " ").trim(); +} + +function expectedSchemaObject(statement: string): Readonly<{ + name: string; + sql: string; + tableName: string; + type: "index" | "table" | "trigger"; +}> { + const match = /^CREATE\s+(TABLE|INDEX|TRIGGER)(?:\s+IF\s+NOT\s+EXISTS)?\s+([a-z0-9_]+)/iu.exec(statement.trim()); + if (match === null) throw new Error("Invalid compiled authority schema statement."); + const declaredType = match[1]?.toLowerCase(); + const type = declaredType === "index" ? "index" as const + : declaredType === "trigger" ? "trigger" as const : "table" as const; + const name = match[2] as string; + const tableMatch = type === "index" || type === "trigger" ? /\bON\s+([a-z0-9_]+)/iu.exec(statement) : null; + const tableName = type === "table" ? name : tableMatch?.[1]; + if (tableName === undefined) throw new Error("Invalid compiled authority index statement."); + return { name, sql: normalizedSchemaSql(statement), tableName, type }; +} + +const AUTHORITY_SCHEMA_OBJECTS = Object.freeze( + [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS] + .map(expectedSchemaObject) + .sort((left, right) => canonicalJson([left.type, left.name]) + .localeCompare(canonicalJson([right.type, right.name]))), +); +const AUTHORITY_SCHEMA_SHA256 = canonicalSha256(AUTHORITY_SCHEMA_OBJECTS); + +function rowValue( + row: Readonly> | readonly unknown[], + key: string, + index: number, +): unknown { + return Array.isArray(row) ? row[index] : (row as Readonly>)[key]; +} + +function integer(value: unknown): number | null { + if (typeof value === "number") return Number.isSafeInteger(value) ? value : null; + if (typeof value === "bigint") { + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; + } + return null; +} + +function normalizeLimit(value: number | undefined, fallback = 100, maximum = 1000): number { + const limit = value ?? fallback; + if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) { + throw new RangeError(`limit must be an integer from 1 through ${maximum}.`); + } + return limit; +} + +function parseOperationJson(value: unknown): OhOperationV1 { + if (typeof value !== "string") throw new OhIntegrityError("A stored operation is not JSON text."); + let parsedValue: unknown; + try { parsedValue = JSON.parse(value); } catch { throw new OhIntegrityError("A stored operation is not JSON."); } + const operation = parseOhOperationV1(parsedValue); + if (operation === null || canonicalJson(operation) !== value) { + throw new OhIntegrityError("A stored operation is invalid."); + } + return operation; +} + +function parseOperationRow( + row: Readonly> | readonly unknown[], + expected: Readonly<{ operationId?: string; operationSha256?: string; spaceId?: string }> = {}, +): OhOperationV1 { + const operation = parseOperationJson(rowValue(row, "operation_json", 7)); + if (rowValue(row, "operation_sha256", 0) !== operation.operationSha256 + || rowValue(row, "space_id", 1) !== operation.spaceId + || integer(rowValue(row, "sequence", 2)) !== operation.sequence + || rowValue(row, "operation_id", 3) !== operation.operationId + || rowValue(row, "parent_operation_sha256", 4) !== operation.parentOperationSha256 + || rowValue(row, "graph_revision_sha256", 5) !== operation.graphRevisionSha256 + || rowValue(row, "records_sha256", 6) !== operation.recordsSha256 + || rowValue(row, "instant", 8) !== operation.instant + || (expected.spaceId !== undefined && operation.spaceId !== expected.spaceId) + || (expected.operationId !== undefined && operation.operationId !== expected.operationId) + || (expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256)) { + throw new OhIntegrityError("Remote operation columns do not match their canonical envelope."); + } + return operation; +} + +function parseBindingRow( + row: Readonly> | readonly unknown[], + expectedSpaceId: string, +): OhStoreBindingV1 { + const json = rowValue(row, "binding_json", 6); + if (typeof json !== "string") throw new OhIntegrityError("A remote store binding is not JSON text."); + let value: unknown; + try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote store binding is not JSON."); } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== json || binding.spaceId !== expectedSpaceId + || rowValue(row, "space_id", 0) !== binding.spaceId + || rowValue(row, "realm_id", 1) !== binding.realmId + || rowValue(row, "profile_id", 2) !== binding.profile.profileId + || rowValue(row, "profile_kind", 3) !== binding.profile.profileKind + || rowValue(row, "profile_sha256", 4) !== binding.profile.profileSha256 + || rowValue(row, "binding_sha256", 5) !== binding.bindingSha256) { + throw new OhIntegrityError("Remote binding columns do not match their canonical envelope."); + } + return binding; +} + +function parsePurgeReceiptRow( + row: Readonly> | readonly unknown[], + expectedSpaceId: string, + expectedBindingSha256?: Sha256Hex, +): OhSpacePurgeReceiptV1 { + const json = rowValue(row, "receipt_json", 6); + if (typeof json !== "string") throw new OhIntegrityError("A remote purge receipt is invalid."); + let value: unknown; + try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A remote purge receipt is invalid."); } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== json) { + throw new OhIntegrityError("A remote purge receipt is invalid."); + } + if (receipt.spaceId !== expectedSpaceId + || (expectedBindingSha256 !== undefined && receipt.bindingSha256 !== expectedBindingSha256) + || rowValue(row, "space_id", 0) !== receipt.spaceId + || rowValue(row, "binding_sha256", 1) !== receipt.bindingSha256 + || rowValue(row, "prior_operation_sha256", 2) !== receipt.priorHead.operationSha256 + || integer(rowValue(row, "prior_sequence", 3)) !== receipt.priorHead.sequence + || rowValue(row, "purged_at", 4) !== receipt.purgedAt + || rowValue(row, "receipt_sha256", 5) !== receipt.receiptSha256) { + throw new OhIntegrityError("Remote purge columns do not match their canonical receipt."); + } + return receipt; +} + +function parseHeadRow(row: Readonly> | readonly unknown[]): OhHeadV1 { + const generation = integer(rowValue(row, "generation", 0)); + const graphValue = rowValue(row, "graph_revision_sha256", 1); + const operationValue = rowValue(row, "head_operation_sha256", 2); + const recordsSha256 = parseSha256Hex(rowValue(row, "records_sha256", 3)); + const sequence = integer(rowValue(row, "sequence", 4)); + const graphRevisionSha256 = graphValue === null ? null : parseSha256Hex(graphValue); + const operationSha256 = operationValue === null ? null : parseSha256Hex(operationValue); + if (generation === null || sequence === null || generation !== sequence || recordsSha256 === null + || (graphValue !== null && graphRevisionSha256 === null) + || (operationValue !== null && operationSha256 === null) + || ((sequence === 0) !== (operationSha256 === null)) + || ((sequence === 0) !== (graphRevisionSha256 === null))) { + throw new OhIntegrityError("The remote authority contains an invalid head."); + } + return { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 }; +} + +async function queryOne( + client: OhLibSqlClientV1, + statement: OhLibSqlStatementV1, +): Promise> | readonly unknown[] | null> { + return (await client.execute(statement)).rows[0] ?? null; +} + +async function verifyAuthoritySchemaObjects(client: OhLibSqlClientV1): Promise { + const rows = (await client.execute({ sql: `SELECT type, name, tbl_name, sql FROM sqlite_schema + WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*' + OR tbl_name GLOB 'oh_authority_*') + ORDER BY type, name` })).rows; + const actual = rows.map((row) => { + const type = rowValue(row, "type", 0); + const name = rowValue(row, "name", 1); + const tableName = rowValue(row, "tbl_name", 2); + const sql = rowValue(row, "sql", 3); + if ((type !== "table" && type !== "index" && type !== "trigger") || typeof name !== "string" + || typeof tableName !== "string" || typeof sql !== "string") { + throw new OhIntegrityError("The installed libSQL authority has an invalid schema object."); + } + return { name, sql: normalizedSchemaSql(sql), tableName, type }; + }); + if (canonicalJson(actual) !== canonicalJson(AUTHORITY_SCHEMA_OBJECTS)) { + throw new OhIntegrityError("The installed libSQL authority objects differ from this runtime."); + } +} + +async function verifyAuthoritySchema(client: OhLibSqlClientV1): Promise { + const installed = await queryOne(client, { sql: `SELECT name, schema_sha256 + FROM oh_authority_schemas WHERE version = ?`, args: [AUTHORITY_SCHEMA_VERSION] }); + if (installed === null || rowValue(installed, "name", 0) !== AUTHORITY_SCHEMA_NAME + || rowValue(installed, "schema_sha256", 1) !== AUTHORITY_SCHEMA_SHA256) { + throw new OhIntegrityError("The installed libSQL authority schema differs from this runtime."); + } + const contract = await queryOne(client, { sql: `SELECT contract_sha256, manifest_json + FROM oh_authority_contracts WHERE contract_id = ?`, args: [OH_CONTRACT_MANIFEST_V1.contractId] }); + if (contract === null || rowValue(contract, "contract_sha256", 0) !== OH_CONTRACT_MANIFEST_V1.contractSha256 + || rowValue(contract, "manifest_json", 1) !== canonicalJson(OH_CONTRACT_MANIFEST_V1)) { + throw new OhIntegrityError("The remote authority contract differs from this runtime."); + } + await verifyAuthoritySchemaObjects(client); +} + +/** One-time schema operation for a client authorized to create authority tables. */ +export async function bootstrapOhLibSqlAuthorityV1( + client: OhLibSqlClientV1, +): Promise> { + const existingObjects = (await client.execute({ sql: `SELECT name FROM sqlite_schema + WHERE sql IS NOT NULL AND (name = 'oh_authority_schemas' OR name GLOB 'oh_authority_*' + OR tbl_name GLOB 'oh_authority_*')` })).rows; + if (existingObjects.length > 0) { + if (!existingObjects.some((row) => rowValue(row, "name", 0) === "oh_authority_schemas")) { + throw new OhIntegrityError("Refusing to bootstrap over preexisting Oh authority objects."); + } + await verifyAuthoritySchema(client); + return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 }; + } + const setup: OhLibSqlStatementV1[] = [AUTHORITY_SCHEMA_TABLE_STATEMENT, ...AUTHORITY_SCHEMA_STATEMENTS] + .map((sql) => ({ sql })); + setup.push({ sql: `INSERT INTO oh_authority_schemas(version, name, schema_sha256, applied_at) + VALUES (?, ?, ?, ?) ON CONFLICT(version) DO NOTHING`, + args: [AUTHORITY_SCHEMA_VERSION, AUTHORITY_SCHEMA_NAME, AUTHORITY_SCHEMA_SHA256, canonicalNow()] }); + setup.push({ sql: `INSERT INTO oh_authority_contracts(contract_id, contract_sha256, manifest_json) + VALUES (?, ?, ?) ON CONFLICT(contract_id) DO NOTHING`, args: [OH_CONTRACT_MANIFEST_V1.contractId, + OH_CONTRACT_MANIFEST_V1.contractSha256, canonicalJson(OH_CONTRACT_MANIFEST_V1)] }); + await client.batch(setup, "write"); + await verifyAuthoritySchema(client); + return { schemaSha256: AUTHORITY_SCHEMA_SHA256, schemaVersion: 1, v: 1 }; +} + +async function initializeSpace( + client: OhLibSqlClientV1, + binding: OhStoreBindingV1, +): Promise { + const purged = await queryOne(client, { sql: PURGE_ROW_SELECT, + args: [binding.spaceId] }); + if (purged !== null) throw new OhPurgedSpaceError(parsePurgeReceiptRow( + purged, binding.spaceId, binding.bindingSha256)); + const now = canonicalNow(); + try { await client.batch([ + { sql: `INSERT INTO oh_authority_spaces(space_id, contract_id, generation, + head_operation_sha256, graph_revision_sha256, records_sha256, sequence, created_at, updated_at) + SELECT ?, ?, 0, NULL, NULL, ?, 0, ?, ? + WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + ON CONFLICT(space_id) DO NOTHING`, + args: [binding.spaceId, OH_CONTRACT_MANIFEST_V1.contractId, EMPTY_RECORDS_SHA256, now, now, + binding.spaceId] }, + { sql: `INSERT INTO oh_authority_bindings(space_id, realm_id, profile_id, profile_kind, + profile_sha256, binding_sha256, binding_json, created_at) + SELECT ?, ?, ?, ?, ?, ?, ?, ? + WHERE NOT EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + AND EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + ON CONFLICT(space_id) DO NOTHING`, + args: [binding.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, + binding.profile.profileSha256, binding.bindingSha256, canonicalJson(binding), now, + binding.spaceId, binding.spaceId] }, + { sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ?) + OR NOT EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + OR NOT EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`, + args: [binding.spaceId, binding.spaceId, binding.spaceId, binding.bindingSha256] }, + ], "write"); } catch (error) { + const raced = await queryOne(client, { sql: PURGE_ROW_SELECT, + args: [binding.spaceId] }); + if (raced !== null) throw new OhPurgedSpaceError(parsePurgeReceiptRow( + raced, binding.spaceId, binding.bindingSha256)); + const persisted = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }); + if (persisted !== null + && canonicalJson(parseBindingRow(persisted, binding.spaceId)) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is already bound to a different realm or profile."); + } + throw error; + } + const persisted = await queryOne(client, { sql: BINDING_ROW_SELECT, args: [binding.spaceId] }); + if (persisted === null) { + const raced = await queryOne(client, { sql: PURGE_ROW_SELECT, + args: [binding.spaceId] }); + if (raced !== null) throw new OhPurgedSpaceError(parsePurgeReceiptRow( + raced, binding.spaceId, binding.bindingSha256)); + throw new OhIntegrityError("The remote space has no persisted binding after initialization."); + } + if (canonicalJson(parseBindingRow(persisted, binding.spaceId)) !== canonicalJson(binding)) { + throw new OhProfileError("The remote space is already bound to a different realm or profile."); + } +} + +class OhLibSqlStoreV1 implements OhStoreV1 { + readonly binding: OhStoreBindingV1; + readonly #client: OhLibSqlClientV1; + readonly #closeClient: boolean; + #closed = false; + #purged: OhSpacePurgeReceiptV1 | null = null; + + constructor(client: OhLibSqlClientV1, binding: OhStoreBindingV1, closeClient: boolean) { + this.#client = client; + this.binding = binding; + this.#closeClient = closeClient; + } + + #assertOpen(): void { + if (this.#purged !== null) throw new OhPurgedSpaceError(this.#purged); + if (this.#closed) throw new Error("The Oh libSQL store is closed."); + } + + async head(): Promise { + this.#assertOpen(); + const row = await queryOne(this.#client, { sql: `SELECT generation, graph_revision_sha256, + head_operation_sha256, records_sha256, sequence FROM oh_authority_spaces WHERE space_id = ?`, + args: [this.binding.spaceId] }); + if (row === null) { + const purged = await this.#readPurge(); + if (purged !== null) { this.#purged = purged; throw new OhPurgedSpaceError(purged); } + throw new OhIntegrityError("The remote Oh space does not exist."); + } + return parseHeadRow(row); + } + + async #readPurge(): Promise { + const row = await queryOne(this.#client, { sql: PURGE_ROW_SELECT, + args: [this.binding.spaceId] }); + if (row === null) return null; + return parsePurgeReceiptRow(row, this.binding.spaceId, this.binding.bindingSha256); + } + + async #headAt(reference: OhHeadRefV1): Promise { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) return emptyOhHeadV1(); + const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, parsed.sequence] }); + if (row === null) throw new OhConflictError("The requested head is not present in this space."); + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (operation.spaceId !== this.binding.spaceId || operation.sequence !== parsed.sequence + || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { generation: operation.sequence, graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256, + sequence: operation.sequence, v: 1 }; + } + + async #currentMaterializedSnapshot(expectedHead: OhHeadV1, maximumRecords: number): Promise { + const provenancePredicate = `operation.space_id = ? AND (operation.operation_sha256 IS ? + OR operation.operation_sha256 IN (SELECT record.operation_sha256 + FROM oh_authority_records AS record WHERE record.space_id = ?))`; + const results = await this.#client.batch([ + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: `SELECT + (SELECT count(*) FROM (SELECT DISTINCT operation.operation_sha256 + FROM oh_authority_operations AS operation WHERE ${provenancePredicate})) AS provenance_count, + (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT operation.operation_sha256, + ${OPERATION_RESPONSE_BYTES} AS bytes FROM oh_authority_operations AS operation + WHERE ${provenancePredicate})) AS provenance_bytes, + (SELECT count(*) FROM oh_authority_records WHERE space_id = ?) AS record_count, + (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) FROM oh_authority_records AS record + WHERE record.space_id = ?) AS record_bytes, + (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ?) AS dependency_bytes`, + args: [this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId, + this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId, + this.binding.spaceId, this.binding.spaceId, this.binding.spaceId] }, + { sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence + FROM oh_authority_records AS record WHERE record.space_id = ? + AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? ORDER BY record_key`, args: [this.binding.spaceId, + this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies + AS dependency WHERE dependency.space_id = ? + AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ? + ORDER BY record_key, dependency_key`, + args: [this.binding.spaceId, this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} + FROM oh_authority_operations AS operation + WHERE ${provenancePredicate} + AND (SELECT coalesce(sum(bytes), 0) FROM (SELECT DISTINCT candidate.operation_sha256, + ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND (candidate.operation_sha256 IS ? + OR candidate.operation_sha256 IN (SELECT record.operation_sha256 + FROM oh_authority_records AS record WHERE record.space_id = ?)))) <= ? + ORDER BY operation.sequence`, + args: [this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId, + this.binding.spaceId, expectedHead.operationSha256, this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_authority_operations WHERE space_id = ?`, args: [this.binding.spaceId] }, + ], "read"); + if (results.length !== 6) throw new OhIntegrityError("The remote authority returned an incomplete snapshot batch."); + const [headResult, sizeResult, recordResult, dependencyResult, provenanceResult, historyResult] = results as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, + OhLibSqlResultV1, OhLibSqlResultV1]; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purge = await this.#readPurge(); + if (purge !== null) { this.#purged = purge; throw new OhPurgedSpaceError(purge); } + throw new OhIntegrityError("The remote Oh space disappeared while reading its snapshot."); + } + const head = parseHeadRow(headRow); + if (canonicalJson(head) !== canonicalJson(expectedHead)) { + throw new OhConflictError("The remote space head changed while reading its current snapshot."); + } + const size = sizeResult.rows[0]; + const provenanceOperations = size === undefined ? null : integer(rowValue(size, "provenance_count", 0)); + const provenanceBytes = size === undefined ? null : integer(rowValue(size, "provenance_bytes", 1)); + const recordCount = size === undefined ? null : integer(rowValue(size, "record_count", 2)); + const recordBytes = size === undefined ? null : integer(rowValue(size, "record_bytes", 3)); + const dependencyBytes = size === undefined ? null : integer(rowValue(size, "dependency_bytes", 4)); + if (provenanceOperations === null || provenanceBytes === null || recordCount === null + || recordBytes === null || dependencyBytes === null + || provenanceOperations > OH_LIBSQL_STORE_LIMITS_V1.historyOperations + || provenanceBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + || recordBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + || dependencyBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) { + throw new RangeError("The current libSQL materialization exceeds its provider-safe response bounds."); + } + if (recordResult.rows.length !== recordCount || provenanceResult.rows.length !== provenanceOperations) { + throw new OhIntegrityError("The provider-safe snapshot queries omitted bounded authority rows."); + } + const history = historyResult.rows[0]; + const operationCount = history === undefined ? null : integer(rowValue(history, "count", 0)); + const minimumValue = history === undefined ? undefined : rowValue(history, "minimum", 1); + const maximumValue = history === undefined ? undefined : rowValue(history, "maximum", 2); + const minimumSequence = history === undefined ? null : integer(rowValue(history, "minimum", 1)); + const maximumSequence = history === undefined ? null : integer(rowValue(history, "maximum", 2)); + if (operationCount !== head.sequence + || (head.sequence === 0 && (minimumValue !== null || maximumValue !== null)) + || (head.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== head.sequence))) { + throw new OhIntegrityError("The remote operation history does not exactly cover its current head."); + } + if (recordResult.rows.length > maximumRecords) { + throw new RangeError("The remote graph exceeds the requested record snapshot bound."); + } + const provenanceBySha256 = new Map(); + for (const row of provenanceResult.rows) { + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (provenanceBySha256.has(operation.operationSha256)) { + throw new OhIntegrityError("A current materialization provenance operation is invalid."); + } + provenanceBySha256.set(operation.operationSha256, operation); + } + if (provenanceBySha256.size !== provenanceOperations + || (head.operationSha256 !== null && !provenanceBySha256.has(head.operationSha256))) { + throw new OhIntegrityError("The current materialization omitted required provenance operations."); + } + if (head.sequence > 0) { + const terminal = head.operationSha256 === null ? undefined : provenanceBySha256.get(head.operationSha256); + if (terminal === undefined || terminal.sequence !== head.sequence + || terminal.graphRevisionSha256 !== head.graphRevisionSha256 + || terminal.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The remote space head differs from its terminal canonical operation."); + } + } + const materialized = recordResult.rows.map((row) => { + const json = rowValue(row, "record_json", 3); + if (typeof json !== "string") throw new OhIntegrityError("A materialized remote record is not JSON text."); + let value: unknown; + try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A materialized remote record is invalid."); } + const record = parseKnowledgeGraphRecordV1(value); + const operationSha256 = parseSha256Hex(rowValue(row, "operation_sha256", 4)); + const sequence = integer(rowValue(row, "sequence", 5)); + if (record === null || canonicalJson(record) !== json || operationSha256 === null + || sequence === null || sequence < 1 || sequence > head.sequence + || rowValue(row, "record_key", 0) !== record.key + || rowValue(row, "kind", 1) !== record.kind + || rowValue(row, "record_sha256", 2) !== record.recordSha256) { + throw new OhIntegrityError("A materialized remote record is invalid."); + } + const provenance = provenanceBySha256.get(operationSha256); + if (provenance === undefined || provenance.sequence !== sequence + || !provenance.changes.some((change) => change.kind === "put" + && canonicalJson(change.record) === json)) { + throw new OhIntegrityError("A materialized remote record has no exact canonical provenance put."); + } + return { record, sequence }; + }); + const records = materialized.map(({ record }) => record); + if (canonicalSha256(records.map(knowledgeGraphRecordRefV1)) !== head.recordsSha256) { + throw new OhIntegrityError("Materialized remote records do not reproduce the current head."); + } + const dependencyRows = dependencyResult.rows.map((row) => ({ + dependency_key: rowValue(row, "dependency_key", 1), + record_key: rowValue(row, "record_key", 0), + })); + const expectedDependencies = records.flatMap((record) => + record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key }))); + if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) { + throw new OhIntegrityError("Materialized remote dependencies do not match their record envelopes."); + } + return { head, records, v: 1 }; + } + + async snapshot(options: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }> = {}): Promise { + this.#assertOpen(); + const current = await this.head(); + const target = options.head === undefined ? current : await this.#headAt(options.head); + if (target.sequence > current.sequence) throw new OhConflictError("The requested head is ahead of this space."); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 + || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + if (target.operationSha256 === current.operationSha256) { + return await this.#currentMaterializedSnapshot(current, maximumRecords); + } + if (target.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) { + throw new RangeError("The requested libSQL history exceeds its operation replay bound."); + } + const historyResults = await this.#client.batch([ + { sql: `SELECT count(*) AS count, min(operation.sequence) AS minimum, + max(operation.sequence) AS maximum, + coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes, + coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS response_bytes + FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ?`, + args: [this.binding.spaceId, target.sequence] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + ORDER BY operation.sequence`, args: [this.binding.spaceId, target.sequence, + this.binding.spaceId, target.sequence, OH_LIBSQL_STORE_LIMITS_V1.historyBytes, + this.binding.spaceId, target.sequence, OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }, + ], "read"); + if (historyResults.length !== 3) { + throw new OhIntegrityError("The remote authority returned an incomplete history batch."); + } + const [historySizeResult, historyRowResult, purgeResult] = historyResults as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1]; + const historySizeRow = historySizeResult.rows[0]; + const historyBytes = historySizeRow === undefined ? null + : integer(rowValue(historySizeRow, "canonical_bytes", 3)); + const responseBytes = historySizeRow === undefined ? null + : integer(rowValue(historySizeRow, "response_bytes", 4)); + if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes + || responseBytes === null || responseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) { + throw new RangeError("The requested libSQL history exceeds its provider-safe replay bounds."); + } + const historyCount = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "count", 0)); + const minimumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "minimum", 1)); + const maximumSequence = historySizeRow === undefined ? null : integer(rowValue(historySizeRow, "maximum", 2)); + if (historyCount !== target.sequence + || (target.sequence === 0 && (rowValue(historySizeRow!, "minimum", 1) !== null + || rowValue(historySizeRow!, "maximum", 2) !== null)) + || (target.sequence > 0 && (minimumSequence !== 1 || maximumSequence !== target.sequence)) + || historyRowResult.rows.length !== historyCount) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote operation history does not exactly cover the requested head."); + } + const operations = historyRowResult.rows.map((row) => parseOperationRow(row, + { spaceId: this.binding.spaceId })); + const snapshot = replayOhOperationsV1(this.binding.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 + || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Remote operation replay does not reproduce the requested head."); + } + return snapshot; + } + + async changesSince( + fromValue: OhHeadRefV1, + options: Readonly<{ limit?: number; through?: OhHeadRefV1 }> = {}, + ): Promise { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) throw new TypeError("Invalid change-feed cursor."); + const requestedThrough = options.through === undefined ? undefined : parseOhHeadRefV1(options.through); + if (requestedThrough === null) throw new TypeError("Invalid change-feed through head."); + const limit = normalizeLimit(options.limit, OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit, + OH_LIBSQL_STORE_LIMITS_V1.changeFeedLimit); + const throughSequence = requestedThrough?.sequence ?? null; + const results = await this.#client.batch([ + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, from.sequence] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, throughSequence] }, + { sql: `SELECT count(*) AS count, coalesce(sum(response_bytes), 0) AS response_bytes FROM ( + SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence > ? + AND candidate.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + ORDER BY candidate.sequence LIMIT ? + )`, args: [this.binding.spaceId, from.sequence, throughSequence, + this.binding.spaceId, limit + 1] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + AS operation WHERE operation.space_id = ? AND operation.sequence > ? + AND operation.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + AND (SELECT coalesce(sum(response_bytes), 0) FROM ( + SELECT ${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")} AS response_bytes + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence > ? + AND candidate.sequence <= coalesce(?, + (SELECT sequence FROM oh_authority_spaces WHERE space_id = ?)) + ORDER BY candidate.sequence LIMIT ? + )) <= ? + ORDER BY operation.sequence LIMIT ?`, + args: [this.binding.spaceId, from.sequence, throughSequence, this.binding.spaceId, + this.binding.spaceId, from.sequence, throughSequence, this.binding.spaceId, limit + 1, + OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes, limit + 1] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }, + ], "read"); + if (results.length !== 6) throw new OhIntegrityError("The remote authority returned an incomplete change-feed batch."); + const [currentResult, fromResult, throughResult, pageSizeResult, pageResult, purgeResult] = results as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, + OhLibSqlResultV1, OhLibSqlResultV1]; + const currentRow = currentResult.rows[0]; + if (currentRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote Oh space disappeared while reading its change feed."); + } + const current = parseHeadRow(currentRow); + const resolveHead = (reference: OhHeadRefV1, result: OhLibSqlResultV1): OhHeadV1 => { + if (reference.sequence === 0) return emptyOhHeadV1(); + const row = result.rows[0]; + if (row === undefined) throw new OhConflictError("A requested change-feed head is not present in this space."); + const operation = parseOperationRow(row, { spaceId: this.binding.spaceId }); + if (operation.spaceId !== this.binding.spaceId || operation.sequence !== reference.sequence + || operation.operationSha256 !== reference.operationSha256) { + throw new OhConflictError("A requested change-feed sequence identifies a different operation head."); + } + return { generation: operation.sequence, graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256, + sequence: operation.sequence, v: 1 }; + }; + const fromHead = resolveHead(from, fromResult); + const through = requestedThrough === undefined ? current : resolveHead(requestedThrough, throughResult); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one remote history prefix."); + } + const pageSizeRow = pageSizeResult.rows[0]; + const pageCount = pageSizeRow === undefined ? null : integer(rowValue(pageSizeRow, "count", 0)); + const pageResponseBytes = pageSizeRow === undefined ? null + : integer(rowValue(pageSizeRow, "response_bytes", 1)); + if (pageCount === null || pageCount > limit + 1 || pageResponseBytes === null) { + throw new OhIntegrityError("The remote change feed returned invalid response bounds."); + } + if (pageResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes) { + throw new RangeError("The requested change-feed page exceeds its provider response bound."); + } + const parsed = pageResult.rows.map((row) => parseOperationRow(row, { spaceId: this.binding.spaceId })); + if (parsed.length !== pageCount) { + throw new OhIntegrityError("The remote change feed omitted provider-bounded rows."); + } + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior: OhHeadRefV1 = fromHead; + for (const operation of parsed) { + if (operation.spaceId !== this.binding.spaceId + || operation.sequence !== prior.sequence + 1 + || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The remote change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence + || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The remote change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined + ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } + : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, operations, through, to, v: 1 }; + } + + async #assertMaterializedSnapshot(snapshot: OhSnapshotV1): Promise { + if (snapshot.head.sequence > OH_LIBSQL_STORE_LIMITS_V1.historyOperations) { + throw new RangeError("The libSQL authority exceeds its explicit verification operation bound."); + } + const verificationResults = await this.#client.batch([ + { sql: `SELECT * FROM (WITH bounded_operation AS ( + SELECT * FROM oh_authority_operations + WHERE space_id = ? AND sequence <= ? + ) SELECT count(*) AS operation_count, min(operation.sequence) AS minimum, + max(operation.sequence) AS maximum, + coalesce(sum(length(CAST(operation.operation_json AS BLOB))), 0) AS canonical_bytes, + coalesce(sum(${OPERATION_RESPONSE_BYTES}), 0) AS operation_response_bytes, + (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) AS record_response_bytes, + (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ?) AS dependency_response_bytes, + (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_operation_records AS materialized + JOIN bounded_operation AS owner + ON owner.operation_sha256 = materialized.operation_sha256) AS operation_record_response_bytes + FROM bounded_operation AS operation)`, args: [this.binding.spaceId, snapshot.head.sequence, + this.binding.spaceId, this.binding.spaceId] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} + FROM oh_authority_operations AS operation + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(length(CAST(candidate.operation_json AS BLOB))), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + AND (SELECT coalesce(sum(${OPERATION_RESPONSE_BYTES.replaceAll("operation.", "candidate.")}), 0) + FROM oh_authority_operations AS candidate + WHERE candidate.space_id = ? AND candidate.sequence <= ?) <= ? + ORDER BY operation.sequence`, args: [this.binding.spaceId, snapshot.head.sequence, + this.binding.spaceId, snapshot.head.sequence, OH_LIBSQL_STORE_LIMITS_V1.historyBytes, + this.binding.spaceId, snapshot.head.sequence, OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes] }, + { sql: `SELECT record_key, kind, record_sha256, record_json, operation_sha256, sequence + FROM oh_authority_records AS record WHERE record.space_id = ? + AND (SELECT coalesce(sum(${RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_records AS record WHERE record.space_id = ?) <= ? + ORDER BY record.record_key`, args: [this.binding.spaceId, this.binding.spaceId, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT record_key, dependency_key FROM oh_authority_dependencies AS dependency + WHERE dependency.space_id = ? + AND (SELECT coalesce(sum(${DEPENDENCY_RESPONSE_BYTES}), 0) + FROM oh_authority_dependencies AS dependency WHERE dependency.space_id = ?) <= ? + ORDER BY dependency.record_key, dependency.dependency_key`, args: [this.binding.spaceId, + this.binding.spaceId, OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT materialized.space_id, materialized.operation_sha256, materialized.ordinal, + materialized.record_key, materialized.change_kind, materialized.record_sha256 + FROM oh_authority_operation_records AS materialized + JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ? AND operation.sequence <= ? + AND (SELECT coalesce(sum(${OPERATION_RECORD_RESPONSE_BYTES}), 0) + FROM oh_authority_operation_records AS materialized + JOIN oh_authority_operations AS owner + ON owner.operation_sha256 = materialized.operation_sha256 + WHERE owner.space_id = ? AND owner.sequence <= ?) <= ? + ORDER BY operation.sequence, materialized.ordinal`, args: [this.binding.spaceId, + snapshot.head.sequence, this.binding.spaceId, snapshot.head.sequence, + OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + ], "read"); + if (verificationResults.length !== 6) { + throw new OhIntegrityError("The remote authority returned an incomplete verification batch."); + } + const [sizeResult, operationResult, recordResult, dependencyResult, operationRecordResult, headResult] = + verificationResults as [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, + OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1]; + const sizeRow = sizeResult.rows[0]; + const operationCount = sizeRow === undefined ? null : integer(rowValue(sizeRow, "operation_count", 0)); + const minimumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "minimum", 1); + const maximumValue = sizeRow === undefined ? undefined : rowValue(sizeRow, "maximum", 2); + const minimumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "minimum", 1)); + const maximumSequence = sizeRow === undefined ? null : integer(rowValue(sizeRow, "maximum", 2)); + const historyBytes = sizeRow === undefined ? null : integer(rowValue(sizeRow, "canonical_bytes", 3)); + const operationResponseBytes = sizeRow === undefined ? null + : integer(rowValue(sizeRow, "operation_response_bytes", 4)); + const recordResponseBytes = sizeRow === undefined ? null + : integer(rowValue(sizeRow, "record_response_bytes", 5)); + const dependencyResponseBytes = sizeRow === undefined ? null + : integer(rowValue(sizeRow, "dependency_response_bytes", 6)); + const operationRecordResponseBytes = sizeRow === undefined ? null + : integer(rowValue(sizeRow, "operation_record_response_bytes", 7)); + if (historyBytes === null || historyBytes > OH_LIBSQL_STORE_LIMITS_V1.historyBytes + || operationResponseBytes === null + || operationResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.providerResponseBytes + || recordResponseBytes === null + || recordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + || dependencyResponseBytes === null + || dependencyResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes + || operationRecordResponseBytes === null + || operationRecordResponseBytes > OH_LIBSQL_STORE_LIMITS_V1.snapshotComponentBytes) { + throw new RangeError("The libSQL authority exceeds its provider-safe verification bounds."); + } + if (operationCount !== snapshot.head.sequence + || (snapshot.head.sequence === 0 && (minimumValue !== null || maximumValue !== null)) + || (snapshot.head.sequence > 0 + && (minimumSequence !== 1 || maximumSequence !== snapshot.head.sequence)) + || operationResult.rows.length !== operationCount) { + throw new OhIntegrityError("The remote operation history does not exactly cover its verified head."); + } + const headRow = headResult.rows[0]; + if (headRow === undefined) throw new OhIntegrityError("The remote authority lost its head during verification."); + if (canonicalJson(parseHeadRow(headRow)) !== canonicalJson(snapshot.head)) { + throw new OhConflictError("The remote authority head changed during verification."); + } + const operations = operationResult.rows.map((row) => { + return parseOperationRow(row, { spaceId: this.binding.spaceId }); + }); + const replayed = replayOhOperationsV1(this.binding.spaceId, operations); + if (canonicalJson(replayed) !== canonicalJson(snapshot)) { + throw new OhIntegrityError("Remote operation replay changed during materialization verification."); + } + const materializedBy = new Map>(); + for (const operation of operations) { + for (const change of operation.changes) { + const key = change.kind === "put" ? change.record.key : change.key; + if (change.kind === "put") materializedBy.set(key, + { operationSha256: operation.operationSha256, sequence: operation.sequence }); + else materializedBy.delete(key); + } + } + const records: KnowledgeGraphRecordV1[] = recordResult.rows.map((row) => { + const json = rowValue(row, "record_json", 3); + if (typeof json !== "string") throw new OhIntegrityError("A materialized remote record is not JSON text."); + let value: unknown; + try { value = JSON.parse(json); } catch { throw new OhIntegrityError("A materialized remote record is invalid."); } + const record = parseKnowledgeGraphRecordV1(value); + const provenance = record === null ? undefined : materializedBy.get(record.key); + if (record === null || canonicalJson(record) !== json + || rowValue(row, "record_key", 0) !== record.key + || rowValue(row, "kind", 1) !== record.kind + || rowValue(row, "record_sha256", 2) !== record.recordSha256 + || provenance === undefined + || rowValue(row, "operation_sha256", 4) !== provenance.operationSha256 + || integer(rowValue(row, "sequence", 5)) !== provenance.sequence) { + throw new OhIntegrityError("A materialized remote record differs from operation replay."); + } + return record; + }); + if (canonicalJson(records) !== canonicalJson(snapshot.records)) { + throw new OhIntegrityError("Remote materialized records do not match operation replay."); + } + const dependencyRows = dependencyResult.rows.map((row) => ({ + dependency_key: rowValue(row, "dependency_key", 1), + record_key: rowValue(row, "record_key", 0), + })); + const expectedDependencies = snapshot.records.flatMap((record) => + record.dependencies.map((dependency) => ({ dependency_key: dependency, record_key: record.key }))); + if (canonicalJson(dependencyRows) !== canonicalJson(expectedDependencies)) { + throw new OhIntegrityError("Remote materialized dependencies do not match operation replay."); + } + const operationRecordRows = operationRecordResult.rows.map((row) => ({ + change_kind: rowValue(row, "change_kind", 4), + operation_sha256: rowValue(row, "operation_sha256", 1), + ordinal: integer(rowValue(row, "ordinal", 2)), + record_key: rowValue(row, "record_key", 3), + record_sha256: rowValue(row, "record_sha256", 5), + space_id: rowValue(row, "space_id", 0), + })); + const expectedOperationRecords = operations.flatMap((operation) => + operation.changes.map((change, ordinal) => ({ change_kind: change.kind, + operation_sha256: operation.operationSha256, ordinal, + record_key: change.kind === "put" ? change.record.key : change.key, + record_sha256: change.kind === "put" ? change.record.recordSha256 : change.priorSha256, + space_id: this.binding.spaceId }))); + if (canonicalJson(operationRecordRows) !== canonicalJson(expectedOperationRecords)) { + throw new OhIntegrityError("Remote operation-record rows do not match operation replay."); + } + } + + async #operationById(operationId: string): Promise { + const row = await queryOne(this.#client, { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }); + if (row === null) return null; + return parseOperationRow(row, { operationId, spaceId: this.binding.spaceId }); + } + + async #commitPreflight(operationId: string): Promise> { + const results = await this.#client.batch([ + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }, + ], "read"); + if (results.length !== 3) throw new OhIntegrityError("The remote authority returned an incomplete commit preflight."); + const [duplicateResult, headResult, purgeResult] = results as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1]; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote space disappeared during commit preflight."); + } + const duplicateRow = duplicateResult.rows[0]; + return { current: parseHeadRow(headRow), duplicate: duplicateRow === undefined ? null + : parseOperationRow(duplicateRow, { operationId, spaceId: this.binding.spaceId }) }; + } + + async #assertOperationReachable(operation: OhOperationV1, expectedHead: OhHeadV1): Promise { + if (operation.sequence < 1 || operation.sequence > expectedHead.sequence) { + throw new OhIntegrityError("A remote idempotent operation is not reachable from the current head."); + } + const results = await this.#client.batch([ + { sql: `SELECT * FROM (WITH RECURSIVE authority_chain(sequence, operation_sha256) AS ( + SELECT sequence, operation_sha256 FROM oh_authority_operations + WHERE space_id = ? AND sequence = ? AND operation_sha256 = ? + UNION ALL + SELECT candidate.sequence, candidate.operation_sha256 + FROM oh_authority_operations AS candidate + JOIN authority_chain AS prior + ON candidate.space_id = ? AND candidate.sequence = prior.sequence + 1 + AND candidate.parent_operation_sha256 = prior.operation_sha256 + WHERE candidate.sequence <= ? + ) SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum, + (SELECT operation_sha256 FROM authority_chain ORDER BY sequence DESC LIMIT 1) AS terminal_sha256 + FROM authority_chain)`, args: [this.binding.spaceId, operation.sequence, + operation.operationSha256, this.binding.spaceId, expectedHead.sequence] }, + { sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND sequence = ?`, args: [this.binding.spaceId, expectedHead.sequence] }, + { sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }, + { sql: PURGE_ROW_SELECT, args: [this.binding.spaceId] }, + ], "read"); + if (results.length !== 4) throw new OhIntegrityError("The remote authority returned an incomplete reachability proof."); + const [chainResult, terminalResult, headResult, purgeResult] = results as + [OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1, OhLibSqlResultV1]; + const headRow = headResult.rows[0]; + if (headRow === undefined) { + const purgeRow = purgeResult.rows[0]; + if (purgeRow !== undefined) { + const purge = parsePurgeReceiptRow(purgeRow, this.binding.spaceId, this.binding.bindingSha256); + this.#purged = purge; + throw new OhPurgedSpaceError(purge); + } + throw new OhIntegrityError("The remote space disappeared during an idempotency proof."); + } + const current = parseHeadRow(headRow); + if (canonicalJson(current) !== canonicalJson(expectedHead)) { + throw new OhConflictError("The remote space head changed during an idempotency proof."); + } + const chain = chainResult.rows[0]; + const count = chain === undefined ? null : integer(rowValue(chain, "count", 0)); + const minimum = chain === undefined ? null : integer(rowValue(chain, "minimum", 1)); + const maximum = chain === undefined ? null : integer(rowValue(chain, "maximum", 2)); + const terminalSha256 = chain === undefined ? null : rowValue(chain, "terminal_sha256", 3); + if (count !== expectedHead.sequence - operation.sequence + 1 + || minimum !== operation.sequence || maximum !== expectedHead.sequence + || terminalSha256 !== expectedHead.operationSha256) { + throw new OhIntegrityError("A remote idempotent operation has no exact path to the current head."); + } + const terminalRow = terminalResult.rows[0]; + if (terminalRow === undefined || expectedHead.operationSha256 === null) { + throw new OhIntegrityError("The remote current head operation is missing."); + } + const terminal = parseOperationRow(terminalRow, { operationSha256: expectedHead.operationSha256, + spaceId: this.binding.spaceId }); + if (terminal.sequence !== expectedHead.sequence + || terminal.graphRevisionSha256 !== expectedHead.graphRevisionSha256 + || terminal.recordsSha256 !== expectedHead.recordsSha256) { + throw new OhIntegrityError("The remote space head differs from its terminal canonical operation."); + } + } + + async commit(input: OhCommitInputV1): Promise { + this.#assertOpen(); + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || changes.length === 0) throw new TypeError("Invalid Oh commit input."); + if (changes.length > OH_LIBSQL_STORE_LIMITS_V1.changesPerCommit) { + throw new RangeError("A direct libSQL commit exceeds its change-count bound."); + } + const dependencies = changes.reduce((count, change) => count + + (change.kind === "put" ? change.record.dependencies.length : 0), 0); + if (dependencies > OH_LIBSQL_STORE_LIMITS_V1.dependenciesPerCommit) { + throw new RangeError("A direct libSQL commit exceeds its dependency-count bound."); + } + const { current, duplicate } = await this.#commitPreflight(operationId); + if (duplicate !== null) { + await this.#assertOperationReachable(duplicate, current); + if (duplicate.actorId !== actorId || canonicalJson(duplicate.changes) !== canonicalJson(changes)) { + throw new OhConflictError("The operation ID is already bound to different content."); + } + return duplicate; + } + if (!Number.isSafeInteger(input.expectedHead.generation) || input.expectedHead.generation < 0 + || current.generation !== input.expectedHead.generation + || current.operationSha256 !== input.expectedHead.operationSha256) { + throw new OhConflictError("The expected head does not match the current remote space head."); + } + const snapshot = await this.#currentMaterializedSnapshot(current, OH_GRAPH_LIMITS_V1.recordsPerSnapshot); + const transition = transitionOhSnapshotV1({ actorId, changes, + instant: input.instant ?? canonicalNow(), operationId, snapshot, spaceId: this.binding.spaceId }); + const operation = transition.operation; + if (utf8ByteLength(canonicalJson(operation)) > OH_LIBSQL_STORE_LIMITS_V1.operationBytes) { + throw new RangeError("A direct libSQL operation exceeds its canonical byte bound."); + } + const existsOperation = "EXISTS (SELECT 1 FROM oh_authority_operations WHERE operation_sha256 = ?)"; + const statements: OhLibSqlStatementV1[] = [{ + sql: `INSERT INTO oh_authority_operations(operation_sha256, space_id, sequence, + operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, + operation_json, instant) + SELECT ?, ?, ?, ?, ?, ?, ?, ?, ? + WHERE EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?)`, + args: [operation.operationSha256, this.binding.spaceId, operation.sequence, operation.operationId, + operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256, + canonicalJson(operation), operation.instant, this.binding.spaceId, current.generation, + current.operationSha256], + }]; + for (const [ordinal, change] of operation.changes.entries()) { + const key = change.kind === "put" ? change.record.key : change.key; + const digest = change.kind === "put" ? change.record.recordSha256 : change.priorSha256; + statements.push({ sql: `INSERT INTO oh_authority_operation_records(space_id, operation_sha256, + ordinal, record_key, change_kind, record_sha256) + SELECT ?, ?, ?, ?, ?, ? WHERE ${existsOperation}`, + args: [this.binding.spaceId, operation.operationSha256, ordinal, key, change.kind, digest, + operation.operationSha256] }); + statements.push({ sql: `DELETE FROM oh_authority_dependencies WHERE space_id = ? AND record_key = ? + AND ${existsOperation}`, args: [this.binding.spaceId, key, operation.operationSha256] }); + if (change.kind === "put") { + statements.push({ sql: `INSERT INTO oh_authority_records(space_id, record_key, kind, + record_sha256, record_json, operation_sha256, sequence) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE ${existsOperation} + ON CONFLICT(space_id, record_key) DO UPDATE SET kind = excluded.kind, + record_sha256 = excluded.record_sha256, record_json = excluded.record_json, + operation_sha256 = excluded.operation_sha256, sequence = excluded.sequence`, + args: [this.binding.spaceId, key, change.record.kind, change.record.recordSha256, + canonicalJson(change.record), operation.operationSha256, operation.sequence, + operation.operationSha256] }); + } else { + statements.push({ sql: `DELETE FROM oh_authority_records WHERE space_id = ? AND record_key = ? + AND record_sha256 = ? AND ${existsOperation}`, + args: [this.binding.spaceId, key, change.priorSha256, operation.operationSha256] }); + } + } + for (const change of operation.changes) { + if (change.kind !== "put") continue; + for (const dependency of change.record.dependencies) { + statements.push({ sql: `INSERT INTO oh_authority_dependencies(space_id, record_key, dependency_key) + SELECT ?, ?, ? WHERE ${existsOperation}`, + args: [this.binding.spaceId, change.record.key, dependency, operation.operationSha256] }); + } + } + statements.push({ sql: `UPDATE oh_authority_spaces SET generation = ?, head_operation_sha256 = ?, + graph_revision_sha256 = ?, records_sha256 = ?, sequence = ?, updated_at = ? + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ? AND ${existsOperation}`, + args: [operation.sequence, operation.operationSha256, operation.graphRevisionSha256, + operation.recordsSha256, operation.sequence, operation.instant, this.binding.spaceId, + current.generation, current.operationSha256, operation.operationSha256] }); + statements.push({ sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE NOT EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 = ?)`, + args: [this.binding.spaceId, operation.sequence, operation.operationSha256] }); + statements.push({ sql: `SELECT ${OPERATION_ROW_COLUMNS} FROM oh_authority_operations + WHERE space_id = ? AND operation_id = ?`, args: [this.binding.spaceId, operationId] }); + statements.push({ sql: `SELECT generation, graph_revision_sha256, head_operation_sha256, records_sha256, sequence + FROM oh_authority_spaces WHERE space_id = ?`, args: [this.binding.spaceId] }); + let writeResults: readonly OhLibSqlResultV1[]; + try { + writeResults = await this.#client.batch(statements, "write"); + } catch (error) { + const raced = await this.#operationById(operationId); + const head = await this.head(); + if (raced !== null && raced.actorId === actorId + && canonicalJson(raced.changes) === canonicalJson(changes)) { + await this.#assertOperationReachable(raced, head); + return raced; + } + if (head.operationSha256 !== current.operationSha256) { + throw new OhConflictError("The remote space head changed while committing."); + } + throw error; + } + if (writeResults.length !== statements.length) { + throw new OhIntegrityError("The remote authority returned an incomplete commit result batch."); + } + const persistedRow = writeResults.at(-2)?.rows[0]; + const persistedHeadRow = writeResults.at(-1)?.rows[0]; + if (persistedRow === undefined || persistedHeadRow === undefined) { + throw new OhIntegrityError("The remote authority omitted its persisted commit result."); + } + const persisted = parseOperationRow(persistedRow, { operationId, spaceId: this.binding.spaceId }); + const persistedHead = parseHeadRow(persistedHeadRow); + if (canonicalJson(persisted) !== canonicalJson(operation) + || persistedHead.operationSha256 !== operation.operationSha256 + || persistedHead.sequence !== operation.sequence + || persistedHead.graphRevisionSha256 !== operation.graphRevisionSha256 + || persistedHead.recordsSha256 !== operation.recordsSha256) { + throw new OhIntegrityError("The remote authority did not persist the committed operation exactly."); + } + return persisted; + } + + async exportDependencyClosure(input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): Promise { + if (!this.binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This remote profile does not permit dependency-closure export."); + } + const snapshot = await this.snapshot({ ...(input.head === undefined ? {} : { head: input.head }), + ...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }) }); + return createOhDependencyClosureV1({ binding: this.binding, + ...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }), + roots: input.roots, snapshot }); + } + + async verify(): Promise { + this.#assertOpen(); + const snapshot = await this.snapshot(); + await this.#assertMaterializedSnapshot(snapshot); + return { head: snapshot.head, integrity: "verified", operations: snapshot.head.sequence, + records: snapshot.records.length, v: 1 }; + } + + async #assertPurgeComplete(expected: OhSpacePurgeReceiptV1): Promise { + const tables = ["oh_authority_spaces", "oh_authority_bindings", "oh_authority_operations", + "oh_authority_operation_records", "oh_authority_records", "oh_authority_dependencies"] as const; + const results = await this.#client.batch([ + { sql: PURGE_ROW_SELECT, + args: [this.binding.spaceId] }, + ...tables.map((table) => ({ sql: `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`, + args: [this.binding.spaceId] })), + { sql: `SELECT count(*) AS count FROM oh_authority_operation_records AS materialized + LEFT JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id` }, + ], "read"); + const receiptRow = results[0]?.rows[0]; + if (receiptRow === undefined + || canonicalJson(parsePurgeReceiptRow(receiptRow, this.binding.spaceId, + this.binding.bindingSha256)) !== canonicalJson(expected)) { + throw new OhIntegrityError("The remote purge receipt differs from the requested purge."); + } + for (let index = 0; index < tables.length; index += 1) { + const countRow = results[index + 1]?.rows[0]; + if (countRow === undefined || integer(rowValue(countRow, "count", 0)) !== 0) { + throw new OhIntegrityError(`Remote purge left rows in ${tables[index]}.`); + } + } + const orphanRow = results[tables.length + 1]?.rows[0]; + if (orphanRow === undefined || integer(rowValue(orphanRow, "count", 0)) !== 0) { + throw new OhIntegrityError("Remote purge left an orphaned or cross-space operation record."); + } + } + + async purgeWorkingSpace(purgedAt: string): Promise { + this.#assertOpen(); + if (this.binding.profile.profileKind !== "working" + || !this.binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + for (let attempt = 0; attempt < 3; attempt += 1) { + const existing = await this.#readPurge(); + if (existing !== null) { + await this.#assertPurgeComplete(existing); + this.#purged = existing; + return existing; + } + const head = await this.head(); + const receipt = createOhSpacePurgeReceiptV1({ binding: this.binding, priorHead: head, purgedAt }); + const receiptExists = "EXISTS (SELECT 1 FROM oh_authority_purges WHERE space_id = ? AND receipt_sha256 = ?)"; + const statements: OhLibSqlStatementV1[] = [{ sql: `INSERT INTO oh_authority_purges(space_id, + binding_sha256, prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM oh_authority_spaces + WHERE space_id = ? AND generation = ? AND head_operation_sha256 IS ?) + AND EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ? AND binding_sha256 = ?)`, + args: [this.binding.spaceId, this.binding.bindingSha256, head.operationSha256, head.sequence, + receipt.purgedAt, receipt.receiptSha256, canonicalJson(receipt), this.binding.spaceId, + head.generation, head.operationSha256, this.binding.spaceId, this.binding.bindingSha256] }]; + const guardedDelete = (table: string): OhLibSqlStatementV1 => ({ + sql: `DELETE FROM ${table} WHERE space_id = ? AND ${receiptExists}`, + args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256], + }); + statements.push({ sql: `DELETE FROM oh_authority_operation_records + WHERE operation_sha256 IN (SELECT operation_sha256 FROM oh_authority_operations WHERE space_id = ?) + AND ${receiptExists}`, + args: [this.binding.spaceId, this.binding.spaceId, receipt.receiptSha256] }); + statements.push(guardedDelete("oh_authority_dependencies")); + statements.push(guardedDelete("oh_authority_records")); + statements.push(guardedDelete("oh_authority_operations")); + statements.push(guardedDelete("oh_authority_bindings")); + statements.push(guardedDelete("oh_authority_spaces")); + statements.push({ sql: `INSERT INTO oh_authority_commit_guards(value) + SELECT 'invalid' WHERE EXISTS (SELECT 1 FROM oh_authority_spaces WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_bindings WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operations WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operation_records WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_operation_records AS materialized + LEFT JOIN oh_authority_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.operation_sha256 IS NULL OR operation.space_id <> materialized.space_id) + OR EXISTS (SELECT 1 FROM oh_authority_records WHERE space_id = ?) + OR EXISTS (SELECT 1 FROM oh_authority_dependencies WHERE space_id = ?) + OR NOT ${receiptExists}`, + args: [this.binding.spaceId, this.binding.spaceId, this.binding.spaceId, + this.binding.spaceId, this.binding.spaceId, this.binding.spaceId, + this.binding.spaceId, receipt.receiptSha256] }); + try { await this.#client.batch(statements, "write"); } catch { + const raced = await this.#readPurge(); + if (raced !== null) { + await this.#assertPurgeComplete(raced); + this.#purged = raced; + return raced; + } + continue; + } + const persisted = await this.#readPurge(); + if (persisted !== null) { + await this.#assertPurgeComplete(persisted); + this.#purged = persisted; + return persisted; + } + } + throw new OhConflictError("The remote working space changed repeatedly while purging."); + } + + async close(): Promise { + if (this.#closed) return; + this.#closed = true; + if (this.#closeClient) this.#client.close?.(); + } +} + +/** Opens a direct libSQL/Turso authority; this is not operation-log sync. */ +export async function createOhLibSqlStoreAuthorityV1( + client: OhLibSqlClientV1, + options: OhLibSqlStoreAuthorityOptionsV1 = {}, +): Promise { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) throw new TypeError("Invalid libSQL store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ profile, + realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1 }); + await verifyAuthoritySchema(client); + await initializeSpace(client, binding); + const authority = new OhLibSqlStoreV1(client, binding, options.closeClient ?? false); + const store: OhStoreV1 = Object.freeze({ + binding, + changesSince: (from: OhHeadRefV1, changeOptions?: Readonly<{ limit?: number; through?: OhHeadRefV1 }>) => + authority.changesSince(from, changeOptions), + close: () => authority.close(), + commit: (input: OhCommitInputV1) => authority.commit(input), + exportDependencyClosure: (input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>) => authority.exportDependencyClosure(input), + head: () => authority.head(), + snapshot: (snapshotOptions?: Readonly<{ head?: OhHeadRefV1; maximumRecords?: number }>) => + authority.snapshot(snapshotOptions), + verify: () => authority.verify(), + }); + let purge: OhSpacePurgeReceiptV1 | null = null; + const host: OhStoreHostControlV1 = Object.freeze({ + binding, + purgeWorkingSpace: async (input: Readonly<{ purgedAt?: string }>) => { + if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("This host handle is not bound to a purgeable working profile."); + } + if (purge !== null) return purge; + purge = await authority.purgeWorkingSpace(input.purgedAt ?? canonicalNow()); + return purge; + }, + }); + return Object.freeze({ host, store }); +} diff --git a/src/memory.test.ts b/src/memory.test.ts new file mode 100644 index 0000000..2f8cc24 --- /dev/null +++ b/src/memory.test.ts @@ -0,0 +1,376 @@ +import { describe, expect, test } from "bun:test"; + +import { canonicalSha256, type JsonValue } from "./canonical"; +import { OhRecordCodecRegistry } from "./contract"; +import { createKnowledgeGraphRecordV1 } from "./graph"; +import { + createOhMemoryAgentV1, + OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1, + type OhMemoryAuthoritySourceV1, + type OhMemoryProofV1, +} from "./memory"; +import { + createOhProjectionLiteralV1, + createOhProjectionQueryV1, + createOhProjectionRulePackV1, + createOhProjectionRuleV1, + ohProjectionVariableV1, +} from "./projection"; +import { createOhSqliteStoreAuthorityV1 } from "./sqlite/port"; +import { + OH_CANONICAL_STORE_PROFILE_V1, + OH_WORKING_STORE_PROFILE_V1, + OhIntegrityError, + OhProfileError, + type OhStoreV1, +} from "./store"; + +function entity(key: string, name: string) { + return createKnowledgeGraphRecordV1({ dependencies: [], key, kind: "entity", v: 1, + value: { name } }); +} + +async function put(store: OhStoreV1, key: string, name: string, operationId: string) { + return await store.commit({ actorId: "test.host", changes: [ + { kind: "put", record: entity(key, name), v: 1 }, + ], expectedHead: await store.head(), instant: "2026-08-29T12:00:00.000Z", operationId }); +} + +function entityCodecs() { + return new OhRecordCodecRegistry().register({ kind: "entity", parse: (value) => { + if (typeof value !== "object" || value === null || Array.isArray(value) + || typeof (value as { name?: unknown }).name !== "string") return null; + return { name: (value as { name: string }).name } satisfies JsonValue; + } }); +} + +function visibleProgram() { + const lane = ohProjectionVariableV1("lane"); + const key = ohProjectionVariableV1("key"); + const kind = ohProjectionVariableV1("kind"); + const digest = ohProjectionVariableV1("digest"); + const literal = (relation: string, ...terms: ReturnType[]) => + createOhProjectionLiteralV1({ relation, terms }); + return { + programId: "memory.visible-records", + purpose: "answer.research", + query: createOhProjectionQueryV1({ find: ["lane", "key", "digest"], limit: 50, + queryId: "memory.visible-records", where: [literal("memory.visible", lane, key, digest)] }), + rulePack: createOhProjectionRulePackV1({ rulePackId: "memory.visible-records", + rulePackRevision: 1, rules: [createOhProjectionRuleV1({ + body: [literal("memory.record", lane, key, kind, digest)], + head: literal("memory.visible", lane, key, digest), ruleId: "memory.visible-records", })] }), + } as const; +} + +function nameProgram() { + const lane = ohProjectionVariableV1("lane"); + const key = ohProjectionVariableV1("key"); + const name = ohProjectionVariableV1("name"); + const literal = (relation: string, ...terms: ReturnType[]) => + createOhProjectionLiteralV1({ relation, terms }); + return { + programId: "memory.domain-names", + purpose: "answer.domain", + query: createOhProjectionQueryV1({ find: ["lane", "key", "name"], limit: 50, + queryId: "memory.domain-names", where: [literal("domain.visible-name", lane, key, name)] }), + rulePack: createOhProjectionRulePackV1({ rulePackId: "memory.domain-names", + rulePackRevision: 1, rules: [createOhProjectionRuleV1({ + body: [literal("domain.name", lane, key, name)], + head: literal("domain.visible-name", lane, key, name), ruleId: "memory.domain-names", })] }), + } as const; +} + +function physicalSources(proofs: readonly OhMemoryProofV1[]) { + const sources: OhMemoryAuthoritySourceV1[] = []; + const visit = (proof: OhMemoryProofV1) => { + if (proof.kind === "fact") sources.push(...proof.sources); + else if (proof.kind === "derived") proof.premises.forEach(visit); + }; + proofs.forEach(visit); + return sources; +} + +async function fixture() { + const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:canonical", spaceId: "canonical" }); + const working = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:working", spaceId: "working" }); + await put(canonical.store, "entity:shared", "Reviewed", "op_canonical_shared"); + await put(canonical.store, "entity:canonical", "Canonical", "op_canonical_only"); + const canonicalHead = await canonical.store.head(); + let now = new Date("2026-08-29T12:00:00.000Z"); + const clockOrigin = now.getTime(); + let monotonicNow = 0; + const baseProgram = visibleProgram(); + const agent = await createOhMemoryAgentV1({ + actorId: "test.memory-agent", + canonical: { authorityId: "authority.canonical", expectedBindingSha256: canonical.store.binding.bindingSha256, + expectedHead: canonicalHead, store: canonical.store }, + now: () => now, + nominationRoutes: [{ destinationPurpose: "kb.review", nominationId: "kb.review" }], + monotonicNow: () => monotonicNow, + programs: [baseProgram, { ...baseProgram, programId: "memory.visible-records-alternate", + purpose: "answer.alternate" }, { ...baseProgram, + evaluation: { maximumRounds: 2 }, programId: "memory.visible-records-bounded" }], + working: { authorityId: "authority.working", codecs: entityCodecs(), + expectedBindingSha256: working.store.binding.bindingSha256, store: working.store }, + }); + return { agent, canonical, canonicalHead, setNow(value: string) { + now = new Date(value); monotonicNow = now.getTime() - clockOrigin; + }, working }; +} + +describe("experimental composite Oh memory", () => { + test("keeps two physical authorities explicit and makes working conflicts visible", async () => { + expect(Object.isFrozen(OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.relations)).toBe(true); + const value = await fixture(); + const emptyWorkingHead = await value.working.store.head(); + const firstRemember = { expectedHead: { + generation: emptyWorkingHead.generation, operationSha256: emptyWorkingHead.operationSha256, + }, requestId: "op_working_shared", + puts: [{ dependencies: [], key: "entity:shared", kind: "entity", v: 1, + value: { name: "Proposed correction" } }], tombstones: [], v: 1 } as const; + const receipt = await value.agent.remember(firstRemember); + expect(receipt).toMatchObject({ actorId: "test.memory-agent", authorityId: "authority.working", + lane: "working", requestId: "op_working_shared", status: "committed" }); + expect(Object.keys(receipt)).not.toContain("changes"); + expect(await value.agent.remember(firstRemember)).toEqual(receipt); + const head = await value.working.store.head(); + await value.agent.remember({ expectedHead: { + generation: head.generation, operationSha256: head.operationSha256, + }, requestId: "op_working_only", + puts: [{ dependencies: [], key: "entity:working", kind: "entity", v: 1, + value: { name: "Working" } }], tombstones: [], v: 1 }); + + const result = await value.agent.query({ programId: "memory.visible-records", v: 1 }); + expect(result.authority).toBe("derived"); + expect(result.rows.map(({ values }) => values.slice(0, 2))).toEqual([ + ["canonical", "entity:canonical"], ["canonical", "entity:shared"], + ["working", "entity:shared"], ["working", "entity:working"], + ]); + expect(result.conflicts).toHaveLength(1); + expect(result.conflicts[0]?.key).toBe("entity:shared"); + expect(result.identity.canonical.authorityId).toBe("authority.canonical"); + expect(result.identity.working.authorityId).toBe("authority.working"); + expect(result.identity.canonical.head).toEqual(value.canonicalHead); + expect(result.identity.working.head).toEqual(await value.working.store.head()); + expect(result.identity.purpose).toBe("answer.research"); + expect(Object.isFrozen(result.rows)).toBe(true); + expect(Object.isFrozen(result.rows[0]?.values)).toBe(true); + expect(() => (result.rows as unknown as unknown[]).splice(0, 1)).toThrow(); + expect(() => (result.rows[0]!.values as unknown as unknown[]).push("tamper")).toThrow(); + + const workingRow = result.rows.findIndex(({ values }) => values[0] === "working"); + const explanation = await value.agent.explain({ resultSha256: result.resultSha256, + row: workingRow, token: result.explainCapability.token, v: 1 }); + expect(explanation.premiseAuthority).toBe("working"); + expect(physicalSources(explanation.proofs)).toEqual([expect.objectContaining({ + authorityId: "authority.working", bindingSha256: value.working.store.binding.bindingSha256, + head: await value.working.store.head(), key: "entity:shared", lane: "working", + })]); + await value.canonical.store.close(); await value.working.store.close(); + }); + + test("binds both heads and host-owned program purpose and evaluation into identity", async () => { + const value = await fixture(); + const first = await value.agent.query({ programId: "memory.visible-records", v: 1 }); + const purposeChanged = await value.agent.query({ + programId: "memory.visible-records-alternate", v: 1 }); + const evaluationChanged = await value.agent.query({ + programId: "memory.visible-records-bounded", v: 1 }); + expect(purposeChanged.identity.purpose).toBe("answer.alternate"); + expect(evaluationChanged.identity.evaluationSha256).not.toBe(first.identity.evaluationSha256); + expect(new Set([first.identity.memorySha256, purposeChanged.identity.memorySha256, + evaluationChanged.identity.memorySha256]).size).toBe(3); + + const workingHead = await value.working.store.head(); + await value.agent.remember({ expectedHead: { + generation: workingHead.generation, operationSha256: workingHead.operationSha256, + }, requestId: "op_identity_change", + puts: [{ dependencies: [], key: "entity:new", kind: "entity", v: 1, + value: { name: "New" } }], tombstones: [], v: 1 }); + const headChanged = await value.agent.query({ programId: "memory.visible-records", v: 1 }); + expect(headChanged.identity.memorySha256).not.toBe(first.identity.memorySha256); + expect(headChanged.identity.working.head.sequence).toBe(1); + await value.canonical.store.close(); await value.working.store.close(); + }); + + test("binds host-owned domain extractors while retaining physical sources", async () => { + const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:domain-c", spaceId: "domain-c" }); + const working = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:domain-w", spaceId: "domain-w" }); + await put(canonical.store, "entity:domain", "Domain name", "op_domain_name"); + const agent = await createOhMemoryAgentV1({ actorId: "test.domain-agent", + canonical: { authorityId: "authority.domain-c", + expectedBindingSha256: canonical.store.binding.bindingSha256, + expectedHead: await canonical.store.head(), store: canonical.store }, extractors: [{ + extractorId: "domain.mutator", + extractorSha256: canonicalSha256({ extractor: "domain.mutator", v: 1 }), + relations: ["domain.mutation-attempt"], + extract: ({ lane, record }) => { + try { (record.value as { name: string }).name = "Mutated"; } catch { /* frozen input */ } + return [{ relation: "domain.mutation-attempt", tuple: [lane, record.key], v: 1 }]; + }, + }, { extractorId: "domain.names", + extractorSha256: canonicalSha256({ extractor: "domain.names", v: 1 }), + relations: ["domain.name"], extract: ({ lane, record }) => [{ relation: "domain.name", + tuple: [lane, record.key, (record.value as { name: string }).name], v: 1 }], + }], programs: [nameProgram()], working: { authorityId: "authority.domain-w", + codecs: entityCodecs(), expectedBindingSha256: working.store.binding.bindingSha256, + store: working.store } }); + const result = await agent.query({ programId: "memory.domain-names", v: 1 }); + expect(result.rows.map(({ values }) => values)).toEqual([ + ["canonical", "entity:domain", "Domain name"], + ]); + const explanation = await agent.explain({ resultSha256: result.resultSha256, row: 0, + token: result.explainCapability.token, v: 1 }); + expect(physicalSources(explanation.proofs)[0]).toMatchObject({ key: "entity:domain", lane: "canonical" }); + const topProof = explanation.proofs[0]; + expect(topProof?.kind).toBe("derived"); + if (topProof?.kind !== "derived") throw new Error("Expected a derived domain proof."); + const domainProof = topProof.premises[0]; + expect(domainProof?.kind).toBe("fact"); + if (domainProof?.kind !== "fact") throw new Error("Expected a domain fact premise."); + expect(domainProof.factPolicy).toMatchObject({ extractorId: "domain.names", kind: "domain" }); + await canonical.store.close(); await working.store.close(); + }); + + test("captures host options and detaches store-owned snapshots before the first await", async () => { + const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:captured-c", spaceId: "captured-c" }); + const working = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:captured-w", spaceId: "captured-w" }); + const swapped = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:swapped-w", spaceId: "swapped-w" }); + await put(swapped.store, "entity:swapped", "Swapped", "op_swapped"); + let leakedSnapshot: { head: unknown; records: ReturnType[]; v: 1 } | undefined; + const canonicalStore = { + binding: canonical.store.binding, + head: () => canonical.store.head(), + snapshot: async (options?: Parameters[0]) => { + const snapshot = await canonical.store.snapshot(options); + leakedSnapshot = { head: { ...snapshot.head }, records: [...snapshot.records], v: 1 }; + return leakedSnapshot; + }, + } as unknown as OhStoreV1; + const program = visibleProgram(); + const mutableOptions = { actorId: "test.captured-agent", canonical: { + authorityId: "authority.captured-c", expectedBindingSha256: canonical.store.binding.bindingSha256, + expectedHead: await canonical.store.head(), store: canonicalStore }, + nominationRoutes: [{ destinationPurpose: "kb.review", nominationId: "kb.review" }], + programs: [program], working: { authorityId: "authority.captured-w", codecs: entityCodecs(), + expectedBindingSha256: working.store.binding.bindingSha256, store: working.store } }; + const pending = createOhMemoryAgentV1(mutableOptions); + mutableOptions.working.store = swapped.store; + (mutableOptions.programs[0] as { purpose: string }).purpose = "attacker.relabel"; + const agent = await pending; + if (leakedSnapshot === undefined) throw new Error("Expected the test store to return a snapshot."); + leakedSnapshot.records.push(entity("entity:mutated-snapshot", "Mutated")); + const head = await working.store.head(); + const receipt = await agent.remember({ expectedHead: { generation: head.generation, + operationSha256: head.operationSha256 }, puts: [{ dependencies: [], key: "entity:bound", + kind: "entity", v: 1, value: { name: "Bound" } }], requestId: "captured-remember", + tombstones: [], v: 1 }); + expect(receipt.bindingSha256).toBe(working.store.binding.bindingSha256); + const result = await agent.query({ programId: "memory.visible-records", v: 1 }); + expect(result.identity.purpose).toBe("answer.research"); + expect(result.rows.map(({ values }) => values.slice(0, 2))).toEqual([ + ["working", "entity:bound"], + ]); + const nomination = await agent.nominate({ nominationId: "kb.review", + roots: ["entity:bound"], v: 1 }); + expect(nomination.closure.records.map(({ key }) => key)).toEqual(["entity:bound"]); + await canonical.store.close(); await working.store.close(); await swapped.store.close(); + }); + + test("rejects query injection and misbound or expired explanation capabilities", async () => { + const value = await fixture(); + expect(Object.keys(value.agent).sort()).toEqual(["explain", "nominate", "query", "remember"]); + await expect(value.agent.query({ programId: "memory.visible-records", + query: { relation: "attacker" }, v: 1 })) + .rejects.toThrow(TypeError); + await expect(value.agent.query({ programId: "memory.visible-records", + purpose: "attacker.label", v: 1 })).rejects.toThrow(TypeError); + await expect(value.agent.query({ programId: "unknown", v: 1 })).rejects.toThrow("Unknown named"); + const result = await value.agent.query({ programId: "memory.visible-records", v: 1 }); + await expect(value.agent.explain({ resultSha256: "f".repeat(64), row: 0, + token: result.explainCapability.token, v: 1 })).rejects.toThrow(OhProfileError); + await expect(value.agent.explain({ resultSha256: result.resultSha256, row: 0, + token: "A".repeat(43), v: 1 })).rejects.toThrow(OhProfileError); + value.setNow("2026-08-29T12:10:00.000Z"); + const later = await value.agent.query({ programId: "memory.visible-records", v: 1 }); + value.setNow("2026-08-29T12:05:00.000Z"); + await expect(value.agent.explain({ resultSha256: later.resultSha256, row: 0, + token: later.explainCapability.token, v: 1 })).rejects.toThrow("monotonic clock regressed"); + value.setNow("2026-08-29T12:15:00.000Z"); + await expect(value.agent.explain({ resultSha256: result.resultSha256, row: 0, + token: result.explainCapability.token, v: 1 })).rejects.toThrow(OhProfileError); + value.setNow("invalid"); + await expect(value.agent.explain({ resultSha256: result.resultSha256, row: 0, + token: result.explainCapability.token, v: 1 })).rejects.toThrow(TypeError); + await value.canonical.store.close(); await value.working.store.close(); + }); + + test("prepares an exact working closure without writing canonical authority", async () => { + const value = await fixture(); + const beforeCanonical = await value.canonical.store.head(); + const head = await value.working.store.head(); + await value.agent.remember({ expectedHead: { + generation: head.generation, operationSha256: head.operationSha256, + }, requestId: "op_nomination", + puts: [{ dependencies: [], key: "entity:candidate", kind: "entity", v: 1, + value: { name: "Candidate" } }, { dependencies: [], key: "entity:other", kind: "entity", v: 1, + value: { name: "Other" } }], tombstones: [], v: 1 }); + const nomination = await value.agent.nominate({ nominationId: "kb.review", + roots: ["entity:candidate"], v: 1 }); + expect(nomination.status).toBe("prepared"); + expect(nomination.destinationPurpose).toBe("kb.review"); + expect(nomination.source.lane).toBe("working"); + expect(nomination.closure.records.map(({ key }) => key)).toEqual(["entity:candidate"]); + expect(await value.canonical.store.head()).toEqual(beforeCanonical); + const exactHead = await value.working.store.head(); + const originalExport = value.working.store.exportDependencyClosure.bind(value.working.store); + const substituted = await originalExport({ head: { operationSha256: exactHead.operationSha256, + sequence: exactHead.sequence }, roots: ["entity:other"] }); + (value.working.store as unknown as { exportDependencyClosure: () => Promise }) + .exportDependencyClosure = async () => substituted; + await expect(value.agent.nominate({ nominationId: "kb.review", + roots: ["entity:candidate"], v: 1 })).rejects.toThrow("substituted different roots"); + await expect(value.agent.nominate({ nominationId: "attacker.destination", + roots: ["entity:candidate"], v: 1 })).rejects.toThrow("Unknown named"); + await expect(value.agent.nominate({ nominationId: "kb.review", + roots: Array.from({ length: 1_025 }, (_, index) => `entity:${index}`), v: 1 })) + .rejects.toThrow(TypeError); + await value.canonical.store.close(); await value.working.store.close(); + }); + + test("fails closed on host binding, profile, and pinned-head mistakes", async () => { + const canonical = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:c", spaceId: "c" }); + const working = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:w", spaceId: "w" }); + const head = await canonical.store.head(); + await expect(createOhMemoryAgentV1({ actorId: "test.agent", + canonical: { authorityId: "authority.c", + expectedBindingSha256: working.store.binding.bindingSha256, expectedHead: head, + store: canonical.store }, programs: [visibleProgram()], working: { authorityId: "authority.w", + codecs: entityCodecs(), expectedBindingSha256: working.store.binding.bindingSha256, + store: working.store } })).rejects.toThrow(OhIntegrityError); + await expect(createOhMemoryAgentV1({ actorId: "test.agent", + canonical: { authorityId: "authority.c", + expectedBindingSha256: canonical.store.binding.bindingSha256, + expectedHead: { ...head, recordsSha256: "f".repeat(64) as typeof head.recordsSha256 }, + store: canonical.store }, programs: [visibleProgram()], working: { authorityId: "authority.w", + codecs: entityCodecs(), expectedBindingSha256: working.store.binding.bindingSha256, + store: working.store } })).rejects.toThrow(); + await expect(createOhMemoryAgentV1({ actorId: "test.agent", + canonical: { authorityId: "same", + expectedBindingSha256: canonical.store.binding.bindingSha256, expectedHead: head, + store: canonical.store }, programs: [visibleProgram()], working: { authorityId: "same", + codecs: entityCodecs(), expectedBindingSha256: working.store.binding.bindingSha256, + store: working.store } })).rejects.toThrow(OhProfileError); + await canonical.store.close(); await working.store.close(); + }); +}); diff --git a/src/memory.ts b/src/memory.ts new file mode 100644 index 0000000..0a78b19 --- /dev/null +++ b/src/memory.ts @@ -0,0 +1,957 @@ +import { randomBytes } from "node:crypto"; + +import { + canonicalJson, + canonicalSha256, + hasExactKeys, + isPlainRecord, + parseCanonicalInstantV1, + parseSha256Hex, + safeCode, + utf8ByteLength, + type JsonPrimitive, + type Sha256Hex, +} from "./canonical"; +import { type OhRecordCodecRegistry } from "./contract"; +import { + createKnowledgeGraphRecordV1, + knowledgeGraphRecordRefV1, + type KnowledgeGraphRecordV1, +} from "./graph"; +import { + OH_PROJECTION_SEMANTICS_V1, + OH_PROJECTION_LIMITS_V1, + createOhProjectionDatasetV1, + createOhProjectionFactV1, + createOhProjectionRecordFactsV1, + createOhProjectionSnapshotV1, + evaluateOhProjectionV1, + parseOhProjectionQueryV1, + parseOhProjectionRulePackV1, + type OhProjectionAtomV1, + type OhProjectionDatasetV1, + type OhProjectionEvaluationOptionsV1, + type OhProjectionFactV1, + type OhProjectionProofV1, + type OhProjectionQueryV1, + type OhProjectionResultRowV1, + type OhProjectionRulePackV1, + type OhProjectionSnapshotV1, +} from "./projection"; +import { + OhIntegrityError, + OH_DEPENDENCY_CLOSURE_LIMITS_V1, + OhProfileError, + OhSemanticBundleIngressV1, + parseOhHeadV1, + parseOhStoreBindingV1, + verifyOhDependencyClosureAgainstV1, + type OhDependencyClosureV1, + type OhHeadV1, + type OhSnapshotV1, + type OhStoreBindingV1, + type OhStoreV1, +} from "./store"; +export const OH_MEMORY_FORMAT_VERSION_V1 = 1 as const; +export const OH_MEMORY_CONFLICT_POLICY_V1 = "visible-conflicts.v1" as const; +export const OH_MEMORY_LIMITS_V1 = Object.freeze({ + explainCapabilityEntryBytes: 32 * 1024 * 1024, + explainCapabilities: 256, + explainCapabilityLifetimeMs: 15 * 60 * 1_000, + explainCapabilityTotalBytes: 64 * 1024 * 1024, + factsPerRecordPerExtractor: 512, + maximumExtractorInvocations: 262_144, + maximumExtractors: 32, + maximumNominationRoutes: 64, + maximumPrograms: 128, + maximumRecordsPerLane: 8_192, + maximumSyntheticRecords: 16_384, + rememberBytes: 8 * 1024 * 1024, + resultBytes: 32 * 1024 * 1024, + snapshotBytesPerLane: 32 * 1024 * 1024, + relationsPerExtractor: 64, +}); + +const memoryFactPackPayload = Object.freeze({ + factPackId: "oh.memory.composite-facts", + factPackRevision: 1, + relations: Object.freeze(["memory.agreement", "memory.conflict", "memory.dependency", "memory.record"]), + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1 as const, +}); + +export const OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1 = Object.freeze({ + ...memoryFactPackPayload, + extractorSha256: canonicalSha256(memoryFactPackPayload), +}); + +export type OhMemoryLaneV1 = "canonical" | "working"; + +export type OhMemoryAuthoritySourceV1 = Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + key: string; + lane: OhMemoryLaneV1; + recordSha256: Sha256Hex; + snapshotSha256: Sha256Hex; + v: 1; +}>; + +export type OhMemoryProofV1 = + | Readonly<{ + factPolicy: OhMemoryFactPolicyV1; + kind: "fact"; + relation: string; + sources: readonly OhMemoryAuthoritySourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }> + | Readonly<{ + kind: "derived"; + premises: readonly OhMemoryProofV1[]; + premisesTruncated: boolean; + relation: string; + ruleId: string; + ruleSha256: Sha256Hex; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }> + | Readonly<{ + kind: "truncated"; + reason: "cycle" | "depth" | "nodes"; + relation: string; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }>; + +export type OhMemoryLaneIdentityV1 = Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + datasetSha256: Sha256Hex; + head: OhHeadV1; + lane: OhMemoryLaneV1; + snapshotSha256: Sha256Hex; + v: 1; +}>; + +export type OhMemoryIdentityV1 = Readonly<{ + canonical: OhMemoryLaneIdentityV1; + compositeDatasetSha256: Sha256Hex; + conflictPolicy: typeof OH_MEMORY_CONFLICT_POLICY_V1; + evaluationSha256: Sha256Hex; + memorySha256: Sha256Hex; + programId: string; + projectionSha256: Sha256Hex; + purpose: string; + querySha256: Sha256Hex; + rulePackSha256: Sha256Hex; + v: 1; + working: OhMemoryLaneIdentityV1; +}>; + +export type OhMemoryConflictV1 = Readonly<{ + canonicalRecordSha256: Sha256Hex; + key: string; + v: 1; + workingRecordSha256: Sha256Hex; +}>; + +export type OhMemoryResultRowV1 = Readonly<{ + premiseAuthority: "canonical" | "unknown" | "working"; + premiseLanes: readonly OhMemoryLaneV1[]; + proofsTruncated: boolean; + resultRowSha256: Sha256Hex; + supportCount: number; + v: 1; + values: readonly OhProjectionAtomV1[]; +}>; + +export type OhMemoryQueryResultV1 = Readonly<{ + authority: "derived"; + conflicts: readonly OhMemoryConflictV1[]; + explainCapability: Readonly<{ expiresAt: string; token: string; v: 1 }>; + identity: OhMemoryIdentityV1; + projectionResultSha256: Sha256Hex; + resultSha256: Sha256Hex; + rows: readonly OhMemoryResultRowV1[]; + v: 1; +}>; + +export type OhMemoryRememberReceiptV1 = Readonly<{ + actorId: string; + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + instant: string; + lane: "working"; + operationSha256: Sha256Hex; + receiptSha256: Sha256Hex; + requestId: string; + status: "committed"; + v: 1; +}>; + +export type OhMemoryExplanationV1 = Readonly<{ + authority: "derived"; + explanationSha256: Sha256Hex; + identity: OhMemoryIdentityV1; + premiseAuthority: OhMemoryResultRowV1["premiseAuthority"]; + premiseLanes: readonly OhMemoryLaneV1[]; + proofs: readonly OhMemoryProofV1[]; + proofsTruncated: boolean; + resultRowSha256: Sha256Hex; + resultSha256: Sha256Hex; + supportCount: number; + v: 1; + values: readonly OhProjectionAtomV1[]; +}>; + +export type OhMemoryNominationV1 = Readonly<{ + closure: OhDependencyClosureV1; + destinationPurpose: string; + nominationId: string; + nominationSha256: Sha256Hex; + source: Readonly<{ + authorityId: string; + bindingSha256: Sha256Hex; + head: OhHeadV1; + lane: "working"; + v: 1; + }>; + status: "prepared"; + v: 1; +}>; + +export type OhMemoryNamedProgramV1 = Readonly<{ + evaluation?: OhProjectionEvaluationOptionsV1; + programId: string; + purpose: string; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; +}>; + +export type OhMemoryNominationRouteV1 = Readonly<{ + destinationPurpose: string; + nominationId: string; +}>; + +export type OhMemoryFactPolicyV1 = + | Readonly<{ + extractorSha256: Sha256Hex; + factPackId: string; + kind: "built-in"; + v: 1; + }> + | Readonly<{ + extractorId: string; + extractorSha256: Sha256Hex; + kind: "domain"; + v: 1; + }>; + +export type OhMemoryFactDeclarationV1 = Readonly<{ + relation: string; + tuple: readonly JsonPrimitive[]; + v: 1; +}>; + +/** Host-owned, digest-identified domain projection; it cannot choose sources. */ +export type OhMemoryFactExtractorV1 = Readonly<{ + extract(input: Readonly<{ + lane: OhMemoryLaneV1; + record: KnowledgeGraphRecordV1; + }>): readonly OhMemoryFactDeclarationV1[]; + extractorId: string; + extractorSha256: Sha256Hex; + relations: readonly string[]; +}>; + +export type OhMemoryFacadeOptionsV1 = Readonly<{ + actorId: string; + canonical: Readonly<{ + authorityId: string; + expectedBindingSha256: Sha256Hex; + expectedHead: OhHeadV1; + store: OhStoreV1; + }>; + explainCapabilityLifetimeMs?: number; + extractors?: readonly OhMemoryFactExtractorV1[]; + monotonicNow?: () => number; + nominationRoutes?: readonly OhMemoryNominationRouteV1[]; + now?: () => Date; + programs: readonly OhMemoryNamedProgramV1[]; + working: Readonly<{ + authorityId: string; + codecs: OhRecordCodecRegistry; + expectedBindingSha256: Sha256Hex; + store: OhStoreV1; + }>; +}>; + +export interface OhMemoryAgentV1 { + explain(value: unknown): Promise; + nominate(value: unknown): Promise; + query(value: unknown): Promise; + remember(value: unknown): Promise; +} + +type LaneSnapshot = Readonly<{ + authorityId: string; + binding: OhStoreBindingV1; + dataset: OhProjectionDatasetV1; + lane: OhMemoryLaneV1; + projectionSnapshot: OhProjectionSnapshotV1; + snapshot: OhSnapshotV1; +}>; + +type SyntheticSource = Readonly<{ + physical: OhMemoryAuthoritySourceV1; + record: KnowledgeGraphRecordV1; +}>; + +type StoredExplanation = Readonly<{ + bytes: number; + expiresAtMonotonicMs: number; + identity: OhMemoryIdentityV1; + proofs: readonly (readonly OhMemoryProofV1[])[]; + resultSha256: Sha256Hex; + rows: readonly OhMemoryResultRowV1[]; +}>; + +const builtInFactPolicy: OhMemoryFactPolicyV1 = Object.freeze({ + extractorSha256: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.extractorSha256, + factPackId: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackId, + kind: "built-in", + v: 1, +}); + +function immutableClone(value: T): T { + if (Array.isArray(value)) { + return Object.freeze(value.map((item) => immutableClone(item))) as T; + } + if (value !== null && typeof value === "object") { + if (!isPlainRecord(value)) throw new TypeError("Memory output contains a non-JSON object."); + const cloned: Record = {}; + for (const key of Object.keys(value)) { + Object.defineProperty(cloned, key, { configurable: false, enumerable: true, + value: immutableClone(value[key]), writable: false }); + } + return Object.freeze(cloned) as T; + } + return value; +} + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +function exactHead(left: OhHeadV1, right: OhHeadV1): boolean { + return canonicalJson(left) === canonicalJson(right); +} + +function authorityId(value: unknown): string { + const parsed = safeCode(value, 128); + if (parsed === null) throw new TypeError("Invalid memory authority ID."); + return parsed; +} + +function bindingFor(store: OhStoreV1, expected: Sha256Hex, lane: OhMemoryLaneV1): OhStoreBindingV1 { + const binding = parseOhStoreBindingV1(store.binding); + if (binding === null || binding.bindingSha256 !== parseSha256Hex(expected)) { + throw new OhIntegrityError(`The ${lane} store is not the host-bound authority.`); + } + if (binding.profile.profileKind !== lane) { + throw new OhProfileError(`The ${lane} memory lane has the wrong store profile.`); + } + return binding; +} + +function laneIdentity(value: LaneSnapshot): OhMemoryLaneIdentityV1 { + return Object.freeze({ + authorityId: value.authorityId, + bindingSha256: value.binding.bindingSha256, + datasetSha256: value.dataset.datasetSha256, + head: value.snapshot.head, + lane: value.lane, + snapshotSha256: value.projectionSnapshot.snapshotSha256, + v: 1, + }); +} + +function datasetForSnapshot(binding: OhStoreBindingV1, snapshot: OhSnapshotV1): Readonly<{ + dataset: OhProjectionDatasetV1; + projectionSnapshot: OhProjectionSnapshotV1; +}> { + const projectionSnapshot = createOhProjectionSnapshotV1({ + head: snapshot.head, + records: snapshot.records, + spaceId: binding.spaceId, + }); + const dataset = createOhProjectionDatasetV1({ + extractorSha256: canonicalSha256({ extractor: "oh.memory.lane-structural", v: 1 }), + factPackId: "oh.memory.lane-structural", + factPackRevision: 1, + facts: createOhProjectionRecordFactsV1(snapshot.records), + snapshot: projectionSnapshot, + }); + return { dataset, projectionSnapshot }; +} + +async function readLane( + authority: Readonly<{ authorityId: string; binding: OhStoreBindingV1; store: OhStoreV1 }>, + lane: OhMemoryLaneV1, + expectedHead?: OhHeadV1, +): Promise { + const returnedHead = expectedHead ?? await authority.store.head(); + const head = parseOhHeadV1(immutableClone(returnedHead)); + if (head === null) throw new OhIntegrityError(`The ${lane} store returned an invalid head.`); + const returnedSnapshot = await authority.store.snapshot({ + head: { operationSha256: head.operationSha256, sequence: head.sequence }, + maximumRecords: OH_MEMORY_LIMITS_V1.maximumRecordsPerLane, + }); + if (!isPlainRecord(returnedSnapshot) + || !hasExactKeys(returnedSnapshot, ["head", "records", "v"]) + || returnedSnapshot.v !== 1 || !Array.isArray(returnedSnapshot.records)) { + throw new OhIntegrityError(`The ${lane} store returned an invalid snapshot envelope.`); + } + const detached = immutableClone(returnedSnapshot); + const detachedHead = parseOhHeadV1(detached.head); + if (detachedHead === null) throw new OhIntegrityError(`The ${lane} store returned an invalid snapshot head.`); + const snapshot: OhSnapshotV1 = immutableClone({ head: detachedHead, + records: detached.records, v: 1 }); + if (!exactHead(snapshot.head, head)) { + throw new OhIntegrityError(`The ${lane} snapshot differs from its pinned head.`); + } + if (utf8ByteLength(canonicalJson(snapshot)) > OH_MEMORY_LIMITS_V1.snapshotBytesPerLane) { + throw new RangeError(`The ${lane} memory snapshot exceeds its canonical byte bound.`); + } + const projected = datasetForSnapshot(authority.binding, snapshot); + return Object.freeze({ authorityId: authority.authorityId, binding: authority.binding, + dataset: projected.dataset, lane, projectionSnapshot: projected.projectionSnapshot, snapshot }); +} + +function syntheticKey(lane: OhMemoryLaneV1, recordSha256: Sha256Hex): string { + return `memory-source:${lane}:${recordSha256}`; +} + +function createSyntheticSources(lanes: readonly LaneSnapshot[]): Readonly<{ + records: readonly KnowledgeGraphRecordV1[]; + sources: ReadonlyMap; +}> { + const sources = new Map(); + for (const lane of lanes) { + for (const physicalRecord of lane.snapshot.records) { + const key = syntheticKey(lane.lane, physicalRecord.recordSha256); + const record = createKnowledgeGraphRecordV1({ dependencies: [], key, kind: "view", v: 1, + value: { authorityId: lane.authorityId, bindingSha256: lane.binding.bindingSha256, + key: physicalRecord.key, lane: lane.lane, recordSha256: physicalRecord.recordSha256, + snapshotSha256: lane.projectionSnapshot.snapshotSha256, v: 1 } }); + const physical = Object.freeze({ authorityId: lane.authorityId, + bindingSha256: lane.binding.bindingSha256, + head: lane.snapshot.head, key: physicalRecord.key, lane: lane.lane, + recordSha256: physicalRecord.recordSha256, + snapshotSha256: lane.projectionSnapshot.snapshotSha256, v: 1 as const }); + if (sources.has(key)) throw new OhIntegrityError("A memory lane contains a duplicate source digest."); + sources.set(key, Object.freeze({ physical, record })); + } + } + if (sources.size > OH_MEMORY_LIMITS_V1.maximumSyntheticRecords) { + throw new RangeError("The composite memory snapshot has too many records."); + } + const records = [...sources.values()].map(({ record }) => record) + .sort((left, right) => compareText(left.key, right.key)); + return { records, sources }; +} + +function sourceFor( + sources: ReadonlyMap, + lane: OhMemoryLaneV1, + record: KnowledgeGraphRecordV1, +) { + const source = sources.get(syntheticKey(lane, record.recordSha256)); + if (source === undefined) throw new OhIntegrityError("A composite memory source is missing."); + return [{ key: source.record.key, recordSha256: source.record.recordSha256, v: 1 as const }]; +} + +function createCompositeDataset(canonical: LaneSnapshot, working: LaneSnapshot, + extractors: readonly OhMemoryFactExtractorV1[]): Readonly<{ + conflicts: readonly OhMemoryConflictV1[]; + dataset: OhProjectionDatasetV1; + factPolicies: ReadonlyMap; + snapshot: OhProjectionSnapshotV1; + sources: ReadonlyMap; +}> { + const synthetic = createSyntheticSources([canonical, working]); + const extractorInvocations = synthetic.records.length * extractors.length; + if (extractorInvocations > OH_MEMORY_LIMITS_V1.maximumExtractorInvocations) { + throw new RangeError("The composite memory extractor invocation count exceeds its explicit bound."); + } + const facts: OhProjectionFactV1[] = []; + const factDigests = new Set(); + const factPolicies = new Map(); + const addFact = (fact: OhProjectionFactV1, policy: OhMemoryFactPolicyV1) => { + if (facts.length >= OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("The composite memory fact set exceeds its explicit bound."); + } + if (factDigests.has(fact.factSha256)) { + throw new OhIntegrityError("A memory fact extractor emitted the same exact fact twice."); + } + const priorPolicy = factPolicies.get(fact.relation); + if (priorPolicy !== undefined && canonicalJson(priorPolicy) !== canonicalJson(policy)) { + throw new OhIntegrityError("A memory relation has more than one fact policy."); + } + facts.push(fact); + factDigests.add(fact.factSha256); + factPolicies.set(fact.relation, policy); + }; + const byLane = new Map>([ + ["canonical", new Map(canonical.snapshot.records.map((record) => [record.key, record]))], + ["working", new Map(working.snapshot.records.map((record) => [record.key, record]))], + ]); + for (const lane of [canonical, working] as const) { + for (const record of lane.snapshot.records) { + const extractorRecord = immutableClone(record); + const source = sourceFor(synthetic.sources, lane.lane, record); + addFact(createOhProjectionFactV1({ relation: "memory.record", sources: source, + tuple: [lane.lane, record.key, record.kind, record.recordSha256] }), builtInFactPolicy); + for (const dependency of record.dependencies) { + addFact(createOhProjectionFactV1({ relation: "memory.dependency", sources: source, + tuple: [lane.lane, record.key, dependency] }), builtInFactPolicy); + } + for (const extractor of extractors) { + const declared = extractor.extract(Object.freeze({ lane: lane.lane, record: extractorRecord })); + if (!Array.isArray(declared) + || declared.length > OH_MEMORY_LIMITS_V1.factsPerRecordPerExtractor) { + throw new RangeError("A memory fact extractor exceeded its per-record bound."); + } + for (const fact of declared) { + if (!isPlainRecord(fact) || !hasExactKeys(fact, ["relation", "tuple", "v"]) + || fact.v !== 1 || !Array.isArray(fact.tuple) || typeof fact.relation !== "string" + || !extractor.relations.includes(fact.relation)) { + throw new TypeError("A memory fact extractor returned an invalid or reserved fact."); + } + addFact(createOhProjectionFactV1({ relation: fact.relation, sources: source, tuple: fact.tuple }), + Object.freeze({ extractorId: extractor.extractorId, + extractorSha256: extractor.extractorSha256, kind: "domain", v: 1 })); + } + } + } + } + const conflicts: OhMemoryConflictV1[] = []; + const canonicalByKey = byLane.get("canonical")!; + const workingByKey = byLane.get("working")!; + for (const key of [...canonicalByKey.keys()].filter((candidate) => workingByKey.has(candidate)).sort()) { + const canonicalRecord = canonicalByKey.get(key)!; + const workingRecord = workingByKey.get(key)!; + const sources = [ + ...sourceFor(synthetic.sources, "canonical", canonicalRecord), + ...sourceFor(synthetic.sources, "working", workingRecord), + ]; + if (canonicalRecord.recordSha256 === workingRecord.recordSha256) { + addFact(createOhProjectionFactV1({ relation: "memory.agreement", sources, + tuple: [key, canonicalRecord.recordSha256] }), builtInFactPolicy); + } else { + addFact(createOhProjectionFactV1({ relation: "memory.conflict", sources, + tuple: [key, canonicalRecord.recordSha256, workingRecord.recordSha256] }), builtInFactPolicy); + conflicts.push(Object.freeze({ canonicalRecordSha256: canonicalRecord.recordSha256, + key, v: 1, workingRecordSha256: workingRecord.recordSha256 })); + } + } + const recordRefs = synthetic.records.map(knowledgeGraphRecordRefV1) + .sort((left, right) => compareText(left.key, right.key)); + const sourceIdentity = { + canonical: laneIdentity(canonical), + conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1, + recordRefs, + v: 1 as const, + working: laneIdentity(working), + }; + const head: OhHeadV1 = Object.freeze({ generation: 1, + graphRevisionSha256: canonicalSha256({ kind: "oh.memory.composite-graph", sourceIdentity }), + operationSha256: canonicalSha256({ kind: "oh.memory.composite-operation", sourceIdentity }), + recordsSha256: canonicalSha256(recordRefs), sequence: 1, v: 1 }); + const snapshot = createOhProjectionSnapshotV1({ head, records: synthetic.records, + spaceId: "oh.memory.composite" }); + const dataset = createOhProjectionDatasetV1({ + extractorSha256: canonicalSha256({ + builtIn: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.extractorSha256, + extensions: extractors.map(({ extractorId, extractorSha256, relations }) => ({ + extractorId, extractorSha256, relations, + })), + v: 1, + }), + factPackId: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackId, + factPackRevision: OH_MEMORY_COMPOSITE_FACT_EXTRACTOR_V1.factPackRevision, + facts, + snapshot, + }); + return Object.freeze({ conflicts: Object.freeze(conflicts), dataset, factPolicies, snapshot, + sources: synthetic.sources }); +} + +function mapProof(proof: OhProjectionProofV1, + sources: ReadonlyMap, + factPolicies: ReadonlyMap): OhMemoryProofV1 { + if (proof.kind === "truncated") return Object.freeze({ ...proof }); + if (proof.kind === "derived") { + return Object.freeze({ ...proof, + premises: Object.freeze(proof.premises.map((premise) => mapProof(premise, sources, factPolicies))) }); + } + const physical = proof.sources.map((source) => { + const mapped = sources.get(source.key); + if (mapped === undefined || mapped.record.recordSha256 !== source.recordSha256) { + throw new OhIntegrityError("A projection proof has no exact physical memory source."); + } + return mapped.physical; + }).sort((left, right) => compareText(canonicalJson(left), canonicalJson(right))); + const factPolicy = factPolicies.get(proof.relation); + if (factPolicy === undefined) throw new OhIntegrityError("A projection proof has no memory fact policy."); + return Object.freeze({ factPolicy, kind: "fact", relation: proof.relation, + sources: Object.freeze(physical), tuple: proof.tuple, v: 1 }); +} + +function collectLanes(proof: OhMemoryProofV1, lanes: Set): boolean { + if (proof.kind === "truncated") return true; + if (proof.kind === "fact") { + for (const source of proof.sources) lanes.add(source.lane); + return false; + } + let unknown = proof.premisesTruncated; + for (const premise of proof.premises) unknown = collectLanes(premise, lanes) || unknown; + return unknown; +} + +function publicRow(row: OhProjectionResultRowV1, + proofs: readonly OhMemoryProofV1[]): OhMemoryResultRowV1 { + const lanes = new Set(); + let unknown = row.proofsTruncated; + for (const proof of proofs) unknown = collectLanes(proof, lanes) || unknown; + const premiseLanes = [...lanes].sort() as readonly OhMemoryLaneV1[]; + const premiseAuthority: OhMemoryResultRowV1["premiseAuthority"] = unknown || premiseLanes.length === 0 + ? "unknown" : premiseLanes.includes("working") ? "working" : "canonical"; + const payload = { premiseAuthority, premiseLanes, proofsTruncated: row.proofsTruncated, + supportCount: row.supportCount, v: 1 as const, values: row.values }; + return Object.freeze({ ...payload, resultRowSha256: canonicalSha256(payload) }); +} + +function resolvePrograms(programs: readonly OhMemoryNamedProgramV1[]): ReadonlyMap { + if (programs.length < 1 || programs.length > OH_MEMORY_LIMITS_V1.maximumPrograms) { + throw new RangeError("Memory requires a bounded nonempty named program registry."); + } + const resolved = new Map(); + for (const program of programs) { + const programId = safeCode(program.programId, 128); + const purpose = safeCode(program.purpose, 256); + const query = parseOhProjectionQueryV1(program.query); + const rulePack = parseOhProjectionRulePackV1(program.rulePack); + if (programId === null || purpose === null || query === null || rulePack === null + || resolved.has(programId)) { + throw new TypeError("Invalid or duplicate named memory program."); + } + resolved.set(programId, immutableClone({ ...(program.evaluation === undefined ? {} + : { evaluation: { ...program.evaluation } }), programId, purpose, query, rulePack })); + } + return resolved; +} + +function resolveExtractors(extractors: readonly OhMemoryFactExtractorV1[]): readonly OhMemoryFactExtractorV1[] { + if (extractors.length > OH_MEMORY_LIMITS_V1.maximumExtractors) { + throw new RangeError("The memory domain extractor registry is too large."); + } + const claimedRelations = new Set(); + const resolved = extractors.map((extractor) => { + const extractorId = safeCode(extractor.extractorId, 128); + const extractorSha256 = parseSha256Hex(extractor.extractorSha256); + if (extractorId === null || extractorSha256 === null || typeof extractor.extract !== "function" + || !Array.isArray(extractor.relations) || extractor.relations.length < 1 + || extractor.relations.length > OH_MEMORY_LIMITS_V1.relationsPerExtractor) { + throw new TypeError("Invalid memory domain fact extractor."); + } + const relations = extractor.relations.map((relation) => safeCode(relation, 128)).sort(); + if (relations.some((relation) => relation === null + || relation.startsWith("memory.") || relation.startsWith("oh.")) + || new Set(relations).size !== relations.length) { + throw new TypeError("A memory domain fact extractor has invalid or reserved relations."); + } + for (const relation of relations as string[]) { + if (claimedRelations.has(relation)) { + throw new TypeError("Memory domain fact extractor relations must have one owner."); + } + claimedRelations.add(relation); + } + return Object.freeze({ extract: extractor.extract, extractorId, extractorSha256, + relations: Object.freeze(relations as string[]) }); + }).sort((left, right) => compareText(left.extractorId, right.extractorId)); + if (new Set(resolved.map(({ extractorId }) => extractorId)).size !== resolved.length) { + throw new TypeError("Duplicate memory domain fact extractor ID."); + } + return Object.freeze(resolved); +} + +function resolveNominationRoutes(routes: readonly OhMemoryNominationRouteV1[]): +ReadonlyMap { + if (routes.length > OH_MEMORY_LIMITS_V1.maximumNominationRoutes) { + throw new RangeError("The memory nomination route registry is too large."); + } + const resolved = new Map(); + for (const route of routes) { + const nominationId = safeCode(route.nominationId, 128); + const destinationPurpose = safeCode(route.destinationPurpose, 256); + if (nominationId === null || destinationPurpose === null || resolved.has(nominationId)) { + throw new TypeError("Invalid or duplicate memory nomination route."); + } + resolved.set(nominationId, Object.freeze({ destinationPurpose, nominationId })); + } + return resolved; +} + +function parseQueryRequest(value: unknown): Readonly<{ programId: string }> { + if (!isPlainRecord(value) || !hasExactKeys(value, ["programId", "v"]) + || value.v !== 1) throw new TypeError("Invalid named memory query."); + const programId = safeCode(value.programId, 128); + if (programId === null) throw new TypeError("Invalid named memory query identity."); + return { programId }; +} + +function parseExplainRequest(value: unknown): Readonly<{ + row: number; + resultSha256: Sha256Hex; + token: string; +}> { + if (!isPlainRecord(value) || !hasExactKeys(value, ["resultSha256", "row", "token", "v"]) + || value.v !== 1 || typeof value.token !== "string" || value.token.length !== 43 + || !Number.isSafeInteger(value.row) || (value.row as number) < 0) { + throw new TypeError("Invalid memory explanation request."); + } + const resultSha256 = parseSha256Hex(value.resultSha256); + if (resultSha256 === null) throw new TypeError("Invalid memory explanation result identity."); + return { resultSha256, row: value.row as number, token: value.token }; +} + +function parseNominationRequest(value: unknown): Readonly<{ + nominationId: string; + roots: readonly string[]; +}> { + if (!isPlainRecord(value) || !hasExactKeys(value, ["nominationId", "roots", "v"]) + || value.v !== 1 || !Array.isArray(value.roots) || value.roots.length < 1 + || value.roots.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new TypeError("Invalid memory nomination request."); + } + const nominationId = safeCode(value.nominationId, 128); + const roots = value.roots.map((root) => safeCode(root, 512)).sort(); + if (nominationId === null || roots.some((root) => root === null) + || new Set(roots).size !== roots.length) throw new TypeError("Invalid memory nomination identity."); + return { nominationId, roots: roots as readonly string[] }; +} + +function isoInstant(date: Date): string { + const value = date.toISOString(); + if (parseCanonicalInstantV1(value) === null) throw new TypeError("The memory clock returned an invalid instant."); + return value; +} + +function clockMilliseconds(now: () => Date): number { + const milliseconds = now().getTime(); + if (!Number.isFinite(milliseconds)) throw new TypeError("The memory clock returned an invalid date."); + return milliseconds; +} + +function monotonicMilliseconds(now: () => number): number { + const milliseconds = now(); + if (!Number.isFinite(milliseconds) || milliseconds < 0) { + throw new TypeError("The memory monotonic clock returned an invalid value."); + } + return milliseconds; +} + +/** + * Creates a model-facing memory surface over two host-bound physical Oh + * authorities. The returned object has no store, locator, rule, sync, canonical + * write, or purge handle. + */ +export async function createOhMemoryAgentV1(options: OhMemoryFacadeOptionsV1): Promise { + const memoryActorId = safeCode(options.actorId, 128); + if (memoryActorId === null) throw new TypeError("Invalid host-bound memory actor ID."); + const canonicalStore = options.canonical.store; + const workingStore = options.working.store; + const workingCodecs = options.working.codecs; + const canonicalAuthorityId = authorityId(options.canonical.authorityId); + const workingAuthorityId = authorityId(options.working.authorityId); + if (canonicalAuthorityId === workingAuthorityId) { + throw new OhProfileError("Working and canonical memory must be distinct physical authorities."); + } + const canonicalBinding = bindingFor(canonicalStore, + options.canonical.expectedBindingSha256, "canonical"); + const workingBinding = bindingFor(workingStore, + options.working.expectedBindingSha256, "working"); + const expectedCanonicalHead = parseOhHeadV1(options.canonical.expectedHead); + if (expectedCanonicalHead === null) throw new TypeError("Invalid pinned canonical memory head."); + const programs = resolvePrograms(options.programs); + const extractors = resolveExtractors(options.extractors ?? []); + const nominationRoutes = resolveNominationRoutes(options.nominationRoutes ?? []); + const ingress = new OhSemanticBundleIngressV1(workingStore, workingCodecs); + const now = options.now ?? (() => new Date()); + const monotonicNow = options.monotonicNow ?? (() => performance.now()); + const capabilityLifetime = options.explainCapabilityLifetimeMs + ?? OH_MEMORY_LIMITS_V1.explainCapabilityLifetimeMs; + if (!Number.isSafeInteger(capabilityLifetime) || capabilityLifetime < 1_000 + || capabilityLifetime > 60 * 60 * 1_000) { + throw new RangeError("Invalid memory explanation capability lifetime."); + } + const canonical = await readLane({ authorityId: canonicalAuthorityId, + binding: canonicalBinding, store: canonicalStore }, "canonical", expectedCanonicalHead); + const explanations = new Map(); + let explanationBytes = 0; + let lastMonotonicMs = -1; + let lastWallClockMs = Number.NEGATIVE_INFINITY; + const wallClock = () => { + const milliseconds = clockMilliseconds(now); + if (milliseconds < lastWallClockMs) throw new OhProfileError("The memory wall clock regressed."); + lastWallClockMs = milliseconds; + return milliseconds; + }; + const monotonicClock = () => { + const milliseconds = monotonicMilliseconds(monotonicNow); + if (milliseconds < lastMonotonicMs) throw new OhProfileError("The memory monotonic clock regressed."); + lastMonotonicMs = milliseconds; + return milliseconds; + }; + const deleteExplanation = (token: string) => { + const stored = explanations.get(token); + if (stored !== undefined && explanations.delete(token)) explanationBytes -= stored.bytes; + }; + + const remember = async (value: unknown): Promise => { + if (utf8ByteLength(canonicalJson(value)) > OH_MEMORY_LIMITS_V1.rememberBytes) { + throw new RangeError("The memory semantic bundle exceeds its canonical byte bound."); + } + if (!isPlainRecord(value) || !hasExactKeys(value, + ["expectedHead", "puts", "requestId", "tombstones", "v"]) || value.v !== 1) { + throw new TypeError("Invalid memory remember request."); + } + const requestId = safeCode(value.requestId, 128); + if (requestId === null) throw new TypeError("Invalid memory remember request identity."); + const operationId = `memory_${canonicalSha256({ actorId: memoryActorId, + bindingSha256: workingBinding.bindingSha256, requestId, v: 1 }).slice(0, 48)}`; + const operation = await ingress.commit({ actorId: memoryActorId, + expectedHead: value.expectedHead, instant: isoInstant(new Date(wallClock())), operationId, + puts: value.puts, tombstones: value.tombstones, v: 1 }); + const head: OhHeadV1 = { generation: operation.sequence, + graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256, + sequence: operation.sequence, v: 1 }; + const payload = { actorId: operation.actorId, authorityId: workingAuthorityId, + bindingSha256: workingBinding.bindingSha256, head, instant: operation.instant, + lane: "working" as const, operationSha256: operation.operationSha256, requestId, + status: "committed" as const, v: 1 as const }; + return immutableClone({ ...payload, receiptSha256: canonicalSha256(payload) }); + }; + + const query = async (value: unknown): Promise => { + const request = parseQueryRequest(value); + const program = programs.get(request.programId); + if (program === undefined) throw new TypeError("Unknown named memory program."); + const working = await readLane({ authorityId: workingAuthorityId, + binding: workingBinding, store: workingStore }, "working"); + const composite = createCompositeDataset(canonical, working, extractors); + const projection = evaluateOhProjectionV1({ dataset: composite.dataset, + ...(program.evaluation === undefined ? {} : { options: program.evaluation }), + query: program.query, rulePack: program.rulePack, snapshot: composite.snapshot }); + const identityPayload = { + canonical: laneIdentity(canonical), + compositeDatasetSha256: composite.dataset.datasetSha256, + conflictPolicy: OH_MEMORY_CONFLICT_POLICY_V1, + evaluationSha256: projection.identity.evaluationSha256, + programId: program.programId, + projectionSha256: projection.identity.projectionSha256, + purpose: program.purpose, + querySha256: program.query.querySha256, + rulePackSha256: program.rulePack.rulePackSha256, + v: 1 as const, + working: laneIdentity(working), + }; + const identity: OhMemoryIdentityV1 = immutableClone({ ...identityPayload, + memorySha256: canonicalSha256(identityPayload) }); + const proofs = immutableClone(projection.rows.map((row) => + row.proofs.map((proof) => mapProof(proof, composite.sources, composite.factPolicies)))); + const rows = immutableClone(projection.rows.map((row, index) => publicRow(row, proofs[index]!))); + const resultPayload = immutableClone({ authority: "derived" as const, + conflicts: composite.conflicts, identity, projectionResultSha256: projection.resultSha256, + rows, v: 1 as const }); + const resultSha256 = canonicalSha256(resultPayload); + const issuedAt = wallClock(); + const issuedAtMonotonic = monotonicClock(); + const expiresAtMs = issuedAt + capabilityLifetime; + const expiresAtMonotonicMs = issuedAtMonotonic + capabilityLifetime; + const expiresAt = isoInstant(new Date(expiresAtMs)); + for (const [existingToken, stored] of explanations) { + if (issuedAtMonotonic >= stored.expiresAtMonotonicMs) deleteExplanation(existingToken); + } + const storedPayload = immutableClone({ expiresAtMonotonicMs, identity, proofs, resultSha256, rows }); + const storedBytes = utf8ByteLength(canonicalJson(storedPayload)) + 128; + if (storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityEntryBytes + || storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) { + throw new RangeError("The memory explanation exceeds its retained capability bound."); + } + while (explanations.size >= OH_MEMORY_LIMITS_V1.explainCapabilities + || explanationBytes + storedBytes > OH_MEMORY_LIMITS_V1.explainCapabilityTotalBytes) { + const oldest = explanations.keys().next().value as string | undefined; + if (oldest === undefined) break; + deleteExplanation(oldest); + } + let token = randomBytes(32).toString("base64url"); + while (explanations.has(token)) token = randomBytes(32).toString("base64url"); + explanations.set(token, immutableClone({ ...storedPayload, bytes: storedBytes })); + explanationBytes += storedBytes; + const result = immutableClone({ ...resultPayload, + explainCapability: { expiresAt, token, v: 1 as const }, resultSha256 }); + if (utf8ByteLength(canonicalJson(result)) > OH_MEMORY_LIMITS_V1.resultBytes) { + deleteExplanation(token); + throw new RangeError("The composite memory result exceeds its canonical byte bound."); + } + return result; + }; + + const explain = async (value: unknown): Promise => { + const request = parseExplainRequest(value); + const stored = explanations.get(request.token); + const currentTime = monotonicClock(); + if (stored === undefined || stored.resultSha256 !== request.resultSha256 + || currentTime >= stored.expiresAtMonotonicMs) { + deleteExplanation(request.token); + throw new OhProfileError("The memory explanation capability is absent, expired, or misbound."); + } + const row = stored.rows[request.row]; + const proofs = stored.proofs[request.row]; + if (row === undefined || proofs === undefined) throw new RangeError("The explanation row is out of bounds."); + const payload = { authority: "derived" as const, identity: stored.identity, + premiseAuthority: row.premiseAuthority, premiseLanes: row.premiseLanes, proofs, + proofsTruncated: row.proofsTruncated, resultRowSha256: row.resultRowSha256, + resultSha256: stored.resultSha256, supportCount: row.supportCount, v: 1 as const, + values: row.values }; + return immutableClone({ ...payload, explanationSha256: canonicalSha256(payload) }); + }; + + const nominate = async (value: unknown): Promise => { + const request = parseNominationRequest(value); + const route = nominationRoutes.get(request.nominationId); + if (route === undefined) throw new TypeError("Unknown named memory nomination route."); + const head = parseOhHeadV1(immutableClone(await workingStore.head())); + if (head === null) throw new OhIntegrityError("The working nomination store returned an invalid head."); + const closure = await workingStore.exportDependencyClosure({ head: { + operationSha256: head.operationSha256, sequence: head.sequence }, roots: request.roots }); + const verified = verifyOhDependencyClosureAgainstV1(closure, { binding: workingBinding, head }); + if (!verified.ok) throw new OhIntegrityError("The working nomination closure failed exact verification."); + if (canonicalJson(verified.closure.roots) !== canonicalJson(request.roots)) { + throw new OhIntegrityError("The working nomination closure substituted different roots."); + } + const source = Object.freeze({ authorityId: workingAuthorityId, + bindingSha256: workingBinding.bindingSha256, head, lane: "working" as const, v: 1 as const }); + const payload = { closure: verified.closure, destinationPurpose: route.destinationPurpose, + nominationId: route.nominationId, source, status: "prepared" as const, v: 1 as const }; + return immutableClone({ ...payload, nominationSha256: canonicalSha256(payload) }); + }; + + return Object.freeze({ explain, nominate, query, remember }); +} diff --git a/src/projection-public.ts b/src/projection-public.ts new file mode 100644 index 0000000..812b093 --- /dev/null +++ b/src/projection-public.ts @@ -0,0 +1,53 @@ +import * as Projection from "./projection"; + +export const OH_PROJECTION_FORMAT_VERSION_V1 = Projection.OH_PROJECTION_FORMAT_VERSION_V1; +export const OH_PROJECTION_INTERNAL_ENGINE_V1 = Projection.OH_PROJECTION_INTERNAL_ENGINE_V1; +export const OH_PROJECTION_LIMITS_V1 = Projection.OH_PROJECTION_LIMITS_V1; +export const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Projection.OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1; +export const OH_PROJECTION_SEMANTICS_V1 = Projection.OH_PROJECTION_SEMANTICS_V1; +export const createOhProjectionDatasetV1 = Projection.createOhProjectionDatasetV1; +export const createOhProjectionFactV1 = Projection.createOhProjectionFactV1; +export const createOhProjectionIdentityV1 = Projection.createOhProjectionIdentityV1; +export const createOhProjectionLiteralV1 = Projection.createOhProjectionLiteralV1; +export const createOhProjectionQueryV1 = Projection.createOhProjectionQueryV1; +export const createOhProjectionRecordFactsV1 = Projection.createOhProjectionRecordFactsV1; +export const createOhProjectionRulePackV1 = Projection.createOhProjectionRulePackV1; +export const createOhProjectionRuleV1 = Projection.createOhProjectionRuleV1; +export const createOhProjectionSnapshotV1 = Projection.createOhProjectionSnapshotV1; +export const evaluateOhProjectionV1 = Projection.evaluateOhProjectionV1; +export const invalidationForOhProjectionV1 = Projection.invalidationForOhProjectionV1; +export const isOhProjectionRecordKindV1 = Projection.isOhProjectionRecordKindV1; +export const ohProjectionConstantV1 = Projection.ohProjectionConstantV1; +export const ohProjectionVariableV1 = Projection.ohProjectionVariableV1; +export const parseOhProjectionDatasetV1 = Projection.parseOhProjectionDatasetV1; +export const parseOhProjectionFactV1 = Projection.parseOhProjectionFactV1; +export const parseOhProjectionIdentityV1 = Projection.parseOhProjectionIdentityV1; +export const parseOhProjectionLiteralV1 = Projection.parseOhProjectionLiteralV1; +export const parseOhProjectionProofV1 = Projection.parseOhProjectionProofV1; +export const parseOhProjectionQueryV1 = Projection.parseOhProjectionQueryV1; +export const parseOhProjectionResultV1 = Projection.parseOhProjectionResultV1; +export const parseOhProjectionRulePackV1 = Projection.parseOhProjectionRulePackV1; +export const parseOhProjectionRuleV1 = Projection.parseOhProjectionRuleV1; +export const parseOhProjectionSnapshotV1 = Projection.parseOhProjectionSnapshotV1; +export const parseOhProjectionTermV1 = Projection.parseOhProjectionTermV1; + +export type { + OhProjectionAtomV1, + OhProjectionDatasetV1, + OhProjectionEvaluationOptionsV1, + OhProjectionFactSourceV1, + OhProjectionFactV1, + OhProjectionIdentityV1, + OhProjectionInvalidationReasonV1, + OhProjectionInvalidationV1, + OhProjectionLiteralV1, + OhProjectionProofV1, + OhProjectionQueryV1, + OhProjectionRecordFactOptionsV1, + OhProjectionResultRowV1, + OhProjectionResultV1, + OhProjectionRulePackV1, + OhProjectionRuleV1, + OhProjectionSnapshotV1, + OhProjectionTermV1, +} from "./projection"; diff --git a/src/projection-suss.ts b/src/projection-suss.ts new file mode 100644 index 0000000..1982773 --- /dev/null +++ b/src/projection-suss.ts @@ -0,0 +1,129 @@ +import { + Database, + constant, + evaluate, + lit, + rule, + variable, + type Rule, + type Term, +} from "@suss/datalog"; + +import { canonicalJson, parseCanonicalJson } from "./canonical"; +import { + evaluateOhProjectionWithMaterializerV1, + type OhProjectionAtomV1, + type OhProjectionDatasetV1, + type OhProjectionEvaluationOptionsV1, + type OhProjectionLiteralV1, + type OhProjectionQueryV1, + type OhProjectionResultV1, + type OhProjectionRulePackV1, + type OhProjectionSnapshotV1, + type OhProjectionTermV1, +} from "./projection"; + +export const OH_PROJECTION_SUSS_VERSION_V1 = "0.20.0" as const; +export const OH_PROJECTION_SUSS_ENGINE_V1 = "suss.datalog.v0-20-0.equivalence" as const; + +function encodeAtom(value: OhProjectionAtomV1): string { + return canonicalJson(value); +} + +function decodeAtom(value: string | number): OhProjectionAtomV1 { + if (typeof value !== "string") throw new TypeError("Suss returned a non-encoded projection atom."); + const decoded = parseCanonicalJson(value, 16 * 1024); + if (decoded !== null && typeof decoded === "object") { + throw new TypeError("Suss returned a non-atomic projection value."); + } + return decoded; +} + +function sussTerm(term: OhProjectionTermV1): Term { + return term.kind === "constant" ? constant(encodeAtom(term.value)) : variable(term.name); +} + +function sussLiteral(literal: OhProjectionLiteralV1) { + return lit(literal.relation, ...literal.terms.map(sussTerm)); +} + +function sussRule(input: OhProjectionRulePackV1["rules"][number]): Rule { + return rule(input.head.relation, input.head.terms.map(sussTerm), + input.body.map(sussLiteral), input.ruleId); +} + +function projectionDomainSize(dataset: OhProjectionDatasetV1, rulePack: OhProjectionRulePackV1, + query: OhProjectionQueryV1): number { + const atoms = new Set(); + for (const fact of dataset.facts) for (const value of fact.tuple) atoms.add(encodeAtom(value)); + const addTerms = (literal: OhProjectionLiteralV1): void => { + for (const term of literal.terms) if (term.kind === "constant") atoms.add(encodeAtom(term.value)); + }; + for (const item of rulePack.rules) { + addTerms(item.head); + for (const literal of item.body) addTerms(literal); + } + for (const literal of query.where) addTerms(literal); + return atoms.size; +} + +function assertConservativeOutputBound(input: Readonly<{ + dataset: OhProjectionDatasetV1; + maximumDerivedTuples: number; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; +}>): void { + const domainSize = BigInt(projectionDomainSize(input.dataset, input.rulePack, input.query)); + const heads = new Map(); + for (const item of input.rulePack.rules) heads.set(item.head.relation, item.head.terms.length); + const baseCounts = new Map(); + for (const fact of input.dataset.facts) { + if (heads.has(fact.relation)) baseCounts.set(fact.relation, (baseCounts.get(fact.relation) ?? 0) + 1); + } + let possible = 0n; + const maximum = BigInt(input.maximumDerivedTuples); + for (const [relation, arity] of heads) { + const relationSpace = domainSize ** BigInt(arity); + const existing = BigInt(baseCounts.get(relation) ?? 0); + possible += relationSpace > existing ? relationSpace - existing : 0n; + if (possible > maximum) { + throw new RangeError("The Suss equivalence adapter cannot prove the requested derived-tuple bound before evaluation; use the bounded Oh evaluator."); + } + } +} + +/** + * Evaluates the positive rule pack with Suss 0.20.0, then requires its complete + * relation sets to equal Oh's bounded reference semantics before returning the + * canonical derived-only result. The conservative admission check keeps Suss, + * whose public evaluator has no execution-budget hook, inside Oh's tuple bound. + */ +export function evaluateOhProjectionWithSussV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1 { + return evaluateOhProjectionWithMaterializerV1({ + dataset: input.dataset, + engine: OH_PROJECTION_SUSS_ENGINE_V1, + ...(input.options === undefined ? {} : { options: input.options }), + query: input.query, + rulePack: input.rulePack, + snapshot: input.snapshot, + materialize: (program) => { + assertConservativeOutputBound(program); + const database = new Database(); + for (const fact of program.dataset.facts) { + database.add(fact.relation, fact.tuple.map(encodeAtom)); + } + evaluate(database, program.rulePack.rules.map(sussRule)); + const relationFacts = new Map(); + for (const relation of database.relationNames()) { + relationFacts.set(relation, database.facts(relation).map((values) => values.map(decodeAtom))); + } + return { relationFacts }; + }, + }); +} diff --git a/src/projection.test.ts b/src/projection.test.ts new file mode 100644 index 0000000..40c031d --- /dev/null +++ b/src/projection.test.ts @@ -0,0 +1,418 @@ +import { describe, expect, test } from "bun:test"; + +import { canonicalJson, canonicalSha256, type Sha256Hex } from "./canonical"; +import { createKnowledgeGraphRecordV1, knowledgeGraphRecordRefV1, + type KnowledgeGraphRecordV1 } from "./graph"; +import { + createOhProjectionDatasetV1, + createOhProjectionFactV1, + createOhProjectionIdentityV1, + createOhProjectionLiteralV1, + createOhProjectionQueryV1, + createOhProjectionRecordFactsV1, + createOhProjectionRulePackV1, + createOhProjectionRuleV1, + createOhProjectionSnapshotV1, + evaluateOhProjectionV1, + invalidationForOhProjectionV1, + OH_PROJECTION_LIMITS_V1, + OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1, + ohProjectionConstantV1, + ohProjectionVariableV1, + parseOhProjectionIdentityV1, + parseOhProjectionProofV1, + parseOhProjectionQueryV1, + parseOhProjectionResultV1, + parseOhProjectionSnapshotV1, + type OhProjectionAtomV1, + type OhProjectionDatasetV1, + type OhProjectionProofV1, + type OhProjectionQueryV1, + type OhProjectionResultV1, + type OhProjectionRulePackV1, + type OhProjectionSnapshotV1, +} from "./projection"; +import { evaluateOhProjectionWithSussV1, OH_PROJECTION_SUSS_ENGINE_V1, + OH_PROJECTION_SUSS_VERSION_V1 } from "./projection-suss"; + +const extractorSha256 = "e".repeat(64) as Sha256Hex; + +const v = ohProjectionVariableV1; +const c = ohProjectionConstantV1; +const literal = (relation: string, ...terms: ReturnType[]) => + createOhProjectionLiteralV1({ relation, terms }); + +function edgeRecord(from: string, to: string): KnowledgeGraphRecordV1 { + return createKnowledgeGraphRecordV1({ dependencies: [], key: `view:edge-${from}-${to}`, + kind: "view", v: 1, value: { from, to } }); +} + +function snapshot(records: readonly KnowledgeGraphRecordV1[], sequence = 1): OhProjectionSnapshotV1 { + const refs = [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0) + .map(knowledgeGraphRecordRefV1); + return createOhProjectionSnapshotV1({ + head: { + generation: sequence, + graphRevisionSha256: `${sequence.toString(16).padStart(64, "0")}` as Sha256Hex, + operationSha256: `${(sequence + 100).toString(16).padStart(64, "0")}` as Sha256Hex, + recordsSha256: canonicalSha256(refs), + sequence, + }, + records, + spaceId: "session.test", + }); +} + +function dataset(records: readonly KnowledgeGraphRecordV1[], exactSnapshot: OhProjectionSnapshotV1, + reverse = false): OhProjectionDatasetV1 { + const ordered = reverse ? [...records].reverse() : records; + const facts = ordered.map((record) => createOhProjectionFactV1({ relation: "edge", + sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }], + tuple: [(record.value as { from: string }).from, (record.value as { to: string }).to] })); + return createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges", + factPackRevision: 1, facts, snapshot: exactSnapshot }); +} + +function reachabilityRules(reverse = false): OhProjectionRulePackV1 { + const x = v("x"); + const y = v("y"); + const z = v("z"); + const direct = createOhProjectionRuleV1({ body: [literal("edge", x, y)], + head: literal("path", x, y), ruleId: "path.direct" }); + const transitive = createOhProjectionRuleV1({ body: [literal("path", x, y), literal("edge", y, z)], + head: literal("path", x, z), ruleId: "path.transitive" }); + return createOhProjectionRulePackV1({ rulePackId: "test.reachability", rulePackRevision: 1, + rules: reverse ? [transitive, direct] : [direct, transitive] }); +} + +function allPathsQuery(limit = 1_000): OhProjectionQueryV1 { + return createOhProjectionQueryV1({ find: ["x", "z"], limit, queryId: "all.paths", + where: [literal("path", v("x"), v("z"))] }); +} + +function countProofNodes(proof: OhProjectionProofV1): number { + return proof.kind === "derived" + ? 1 + proof.premises.reduce((sum, premise) => sum + countProofNodes(premise), 0) + : 1; +} + +function expectedClosure(edges: readonly (readonly [string, string])[]): readonly OhProjectionAtomV1[][] { + const nodes = [...new Set(edges.flat())].sort(); + const reachable = new Set(edges.map((edge) => canonicalJson(edge))); + for (const through of nodes) { + for (const from of nodes) { + for (const to of nodes) { + if (reachable.has(canonicalJson([from, through])) && reachable.has(canonicalJson([through, to]))) { + reachable.add(canonicalJson([from, to])); + } + } + } + } + return [...reachable].sort().map((value) => JSON.parse(value) as OhProjectionAtomV1[]); +} + +function resignProjectionResult(value: Readonly>): Readonly> { + const { resultSha256: _resultSha256, ...payload } = value; + return { ...payload, resultSha256: canonicalSha256(payload) }; +} + +function resultWithEvaluation(result: OhProjectionResultV1, + evaluation: OhProjectionResultV1["evaluation"]): Readonly> { + const { projectionSha256: _projectionSha256, ...identityPayload } = { + ...result.identity, + evaluationSha256: canonicalSha256(evaluation), + }; + const identity = { ...identityPayload, projectionSha256: canonicalSha256(identityPayload) }; + return resignProjectionResult({ ...result, evaluation, identity }); +} + +describe("projection identity and validation", () => { + test("binds facts to an exact graph head without changing V1 graph bytes", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + expect(parseOhProjectionSnapshotV1(exact)).toEqual(exact); + expect(exact.recordsSha256).toBe(canonicalSha256(records + .map(knowledgeGraphRecordRefV1).sort((left, right) => left.key.localeCompare(right.key)))); + + const facts = dataset(records, exact); + expect(facts.snapshotSha256).toBe(exact.snapshotSha256); + expect(facts.facts).toHaveLength(2); + expect(() => createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges", + factPackRevision: 1, facts: [createOhProjectionFactV1({ relation: "edge", + sources: [{ key: records[0]!.key, recordSha256: records[1]!.recordSha256, v: 1 }], + tuple: ["a", "b"] })], snapshot: exact })).toThrow("exact input snapshot"); + + const forged = { ...facts, factsSha256: "f".repeat(64) as Sha256Hex }; + expect(() => evaluateOhProjectionV1({ dataset: forged, query: allPathsQuery(), + rulePack: reachabilityRules(), snapshot: exact })).toThrow("Invalid projection snapshot"); + }); + + test("canonicalizes declaration order and rejects non-positive AST shapes", () => { + const forward = reachabilityRules(); + const reverse = reachabilityRules(true); + expect(reverse).toEqual(forward); + expect(parseOhProjectionQueryV1({ ...allPathsQuery(), where: [{ + ...allPathsQuery().where[0], negated: true, + }] })).toBeNull(); + expect(() => createOhProjectionRuleV1({ body: [literal("edge", v("x"), v("y"))], + head: literal("path", v("x"), v("unbound")), ruleId: "unsafe" })).toThrow("must be bound"); + }); + + test("keeps every JSON primitive atom unambiguous", () => { + const record = edgeRecord("a", "b"); + const exact = snapshot([record]); + const fact = createOhProjectionFactV1({ relation: "primitive", + sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }], + tuple: [null, false, 0, "null"] }); + const primitives = createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.primitives", + factPackRevision: 1, facts: [fact], snapshot: exact }); + const query = createOhProjectionQueryV1({ find: ["n", "b", "z", "s"], queryId: "all.primitives", + where: [createOhProjectionLiteralV1({ relation: "primitive", + terms: [v("n"), v("b"), v("z"), v("s")] })] }); + const passthrough = createOhProjectionRulePackV1({ rulePackId: "test.primitives", + rulePackRevision: 1, rules: [createOhProjectionRuleV1({ body: query.where, + head: createOhProjectionLiteralV1({ relation: "copy", + terms: [v("n"), v("b"), v("z"), v("s")] }), ruleId: "copy.primitives" })] }); + const input = { dataset: primitives, query, rulePack: passthrough, snapshot: exact }; + const internal = evaluateOhProjectionV1(input); + expect(internal.rows[0]?.values).toEqual([null, false, 0, "null"]); + expect(evaluateOhProjectionWithSussV1(input).rows).toEqual(internal.rows); + }); + + test("coalesces duplicate tuples while retaining every exact source", () => { + const records = [edgeRecord("a", "b"), createKnowledgeGraphRecordV1({ dependencies: [], + key: "view:edge-a-b-second", kind: "view", v: 1, value: { from: "a", to: "b" } })]; + const exact = snapshot(records); + const facts = records.map((record) => createOhProjectionFactV1({ relation: "edge", + sources: [{ key: record.key, recordSha256: record.recordSha256, v: 1 }], tuple: ["a", "b"] })); + const merged = createOhProjectionDatasetV1({ extractorSha256, factPackId: "test.edges", + factPackRevision: 1, facts, snapshot: exact }); + expect(merged.facts).toHaveLength(1); + expect(merged.facts[0]?.sources).toHaveLength(2); + }); + + test("marks every identity change as an explicit full rebuild", () => { + const records = [edgeRecord("a", "b")]; + const firstSnapshot = snapshot(records, 1); + const secondSnapshot = snapshot(records, 2); + const firstDataset = dataset(records, firstSnapshot); + const secondDataset = dataset(records, secondSnapshot); + const rules = reachabilityRules(); + const query = allPathsQuery(); + const first = createOhProjectionIdentityV1({ dataset: firstDataset, query, rulePack: rules, + snapshot: firstSnapshot }); + expect(parseOhProjectionIdentityV1(first)).toEqual(first); + expect(parseOhProjectionIdentityV1({ ...first, querySha256: "f".repeat(64) })).toBeNull(); + expect(invalidationForOhProjectionV1(first, first)).toEqual({ kind: "reusable", v: 1 }); + const second = createOhProjectionIdentityV1({ dataset: secondDataset, query, rulePack: rules, + snapshot: secondSnapshot }); + expect(invalidationForOhProjectionV1(first, second)).toEqual({ kind: "full-rebuild", + reasons: ["snapshot-changed", "dataset-changed"], v: 1 }); + const changedQuery = createOhProjectionQueryV1({ ...query, limit: 1 }); + const third = createOhProjectionIdentityV1({ dataset: firstDataset, query: changedQuery, + rulePack: rules, snapshot: firstSnapshot }); + expect(invalidationForOhProjectionV1(first, third)).toEqual({ kind: "full-rebuild", + reasons: ["query-changed"], v: 1 }); + const fourth = createOhProjectionIdentityV1({ dataset: firstDataset, + options: { maximumProofNodes: 1 }, query, rulePack: rules, snapshot: firstSnapshot }); + expect(invalidationForOhProjectionV1(first, fourth)).toEqual({ kind: "full-rebuild", + reasons: ["evaluation-changed"], v: 1 }); + }); +}); + +describe("cached projection ingress", () => { + test("round-trips complete result and proof envelopes", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), + query: allPathsQuery(), rulePack: reachabilityRules(), snapshot: exact }); + expect(parseOhProjectionResultV1(result)).toEqual(result); + expect(parseOhProjectionResultV1(result, result.identity.projectionSha256)).toEqual(result); + expect(parseOhProjectionResultV1(result, "f".repeat(64) as Sha256Hex)).toBeNull(); + const proof = result.rows[0]?.proofs[0] as OhProjectionProofV1; + expect(parseOhProjectionProofV1(proof)).toEqual(proof); + }); + + test("rejects cache tampering even when an attacker recomputes the outer digest", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), + query: allPathsQuery(), rulePack: reachabilityRules(), snapshot: exact }); + expect(parseOhProjectionResultV1({ ...result, resultSha256: "f".repeat(64) })).toBeNull(); + const first = result.rows[0] as OhProjectionResultV1["rows"][number]; + const rows = [{ ...first, supportCount: first.supportCount + 1 }, ...result.rows.slice(1)]; + expect(parseOhProjectionResultV1(resignProjectionResult({ ...result, rows }))).toBeNull(); + expect(parseOhProjectionResultV1(resignProjectionResult({ ...result, + engine: "oh.attacker.engine.v1" }))).toBeNull(); + }); + + test("applies declared per-row proof limits and public aggregate ceilings on ingress", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + const query = createOhProjectionQueryV1({ find: ["z"], queryId: "paths.from-a", + where: [createOhProjectionLiteralV1({ relation: "path", terms: [c("a"), v("z")] })] }); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), query, + rulePack: reachabilityRules(), snapshot: exact }); + const smallerEvaluation = { ...result.evaluation, maximumProofNodes: 1 }; + expect(parseOhProjectionResultV1(resultWithEvaluation(result, smallerEvaluation))).toBeNull(); + expect(parseOhProjectionResultV1(resignProjectionResult({ ...result, + stats: { ...result.stats, proofNodes: OH_PROJECTION_LIMITS_V1.totalProofNodes + 1 } }))).toBeNull(); + expect(parseOhProjectionResultV1(resignProjectionResult({ ...result, + evaluation: { ...result.evaluation, maximumWorkUnits: OH_PROJECTION_LIMITS_V1.workUnits + 1 } }))) + .toBeNull(); + }); + + test("rejects malformed and over-depth standalone proof trees", () => { + const digest = "a".repeat(64) as Sha256Hex; + const malformed = { kind: "fact", relation: "edge", + sources: [{ key: "view:one", recordSha256: digest, v: 1 }], tuple: ["a", "b"], + unexpected: true, v: 1 }; + expect(parseOhProjectionProofV1(malformed)).toBeNull(); + expect(parseOhProjectionProofV1({ kind: "fact", relation: "edge", + sources: [{ key: "view:one", recordSha256: digest, v: 1 }, + { key: "view:one", recordSha256: "b".repeat(64), v: 1 }], + tuple: ["a", "b"], v: 1 })).toBeNull(); + + let proof: unknown = { kind: "fact", relation: "edge", + sources: [{ key: "view:one", recordSha256: digest, v: 1 }], tuple: ["a", "b"], v: 1 }; + for (let depth = 0; depth <= OH_PROJECTION_LIMITS_V1.proofDepth; depth += 1) { + proof = { kind: "derived", premises: [proof], premisesTruncated: false, + relation: "path", ruleId: "path.deep", ruleSha256: digest, tuple: ["a", "b"], v: 1 }; + } + expect(parseOhProjectionProofV1(proof)).toBeNull(); + }); +}); + +describe("positive recursive evaluation", () => { + test("computes a deterministic fixpoint and bounded proof trees", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c"), edgeRecord("c", "d")]; + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact, true), + options: { maximumProofNodes: 7 }, query: createOhProjectionQueryV1({ find: ["z"], + queryId: "paths.from-a", where: [createOhProjectionLiteralV1({ relation: "path", + terms: [c("a"), v("z")] })] }), rulePack: reachabilityRules(true), snapshot: exact }); + expect(result.authority).toBe("derived"); + expect(result.cache.strategy).toBe("full-rebuild"); + expect(result.rows.map((row) => row.values)).toEqual([["b"], ["c"], ["d"]]); + expect(result.stats).toMatchObject({ baseFacts: 3, derivedFacts: 6, rounds: 3, + truncated: false }); + expect(result.rows.every((row) => row.proofs.reduce((sum, proof) => sum + countProofNodes(proof), 0) <= 7)) + .toBe(true); + expect(canonicalSha256((({ resultSha256: _digest, ...payload }) => payload)(result))) + .toBe(result.resultSha256); + }); + + test("fails closed at evaluation and arity bounds", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact), + options: { maximumDerivedTuples: 1 }, query: allPathsQuery(), + rulePack: reachabilityRules(), snapshot: exact })).toThrow("derived tuple bound"); + const badQuery = createOhProjectionQueryV1({ find: ["x"], queryId: "bad.arity", + where: [literal("path", v("x"))] }); + expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact), query: badQuery, + rulePack: reachabilityRules(), snapshot: exact })).toThrow("conflicting arities"); + expect(() => evaluateOhProjectionV1({ dataset: dataset(records, exact), + options: { maximumWorkUnits: 1 }, query: allPathsQuery(), + rulePack: reachabilityRules(), snapshot: exact })).toThrow("work-unit bound"); + }); + + test("marks proof-prefix exhaustion at both the row and result level", () => { + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), + options: { maximumProofNodes: 1 }, query: createOhProjectionQueryV1({ find: ["x", "z"], + queryId: "two.edges", where: [literal("edge", v("x"), v("y")), + literal("edge", v("y"), v("z"))] }), rulePack: reachabilityRules(), snapshot: exact }); + expect(result.rows).toHaveLength(1); + expect(result.rows[0]).toMatchObject({ proofsTruncated: true, values: ["a", "c"] }); + expect(result.rows[0]?.proofs).toHaveLength(1); + expect(result.stats.proofsTruncated).toBe(true); + expect(parseOhProjectionResultV1(result)).toEqual(result); + }); + + test("bounds proofs across rows and reports collapsed alternative support", () => { + const records = [edgeRecord("a", "b"), edgeRecord("a", "c"), edgeRecord("d", "e")]; + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact), + options: { maximumTotalProofNodes: 1 }, query: createOhProjectionQueryV1({ find: ["x"], + queryId: "edge.sources", where: [literal("edge", v("x"), v("y"))] }), + rulePack: reachabilityRules(), snapshot: exact }); + expect(result.rows).toHaveLength(2); + expect(result.rows[0]).toMatchObject({ proofsTruncated: false, supportCount: 2, values: ["a"] }); + expect(result.rows[1]).toMatchObject({ proofs: [], proofsTruncated: true, + supportCount: 1, values: ["d"] }); + expect(result.stats).toMatchObject({ proofNodes: 1, proofsTruncated: true }); + expect(parseOhProjectionResultV1(result)).toEqual(result); + }); + + test("matches graph reachability across generated input orders", () => { + for (let seed = 1; seed <= 40; seed += 1) { + const edges: [string, string][] = []; + for (let from = 0; from < 7; from += 1) { + for (let to = from + 1; to < 7; to += 1) { + if (((seed * 31 + from * 17 + to * 13) % 5) < 2) edges.push([`n${from}`, `n${to}`]); + } + } + if (edges.length === 0) edges.push(["n0", "n1"]); + const records = edges.map(([from, to]) => edgeRecord(from, to)); + const exact = snapshot(records); + const result = evaluateOhProjectionV1({ dataset: dataset(records, exact, seed % 2 === 0), + query: allPathsQuery(), rulePack: reachabilityRules(seed % 3 === 0), snapshot: exact }); + expect(result.rows.map((row) => row.values)).toEqual([...expectedClosure(edges)]); + } + }); + + test("exposes stable structural record and dependency facts", () => { + const parent = createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:parent", + kind: "entity", v: 1, value: { name: "parent" } }); + const child = createKnowledgeGraphRecordV1({ dependencies: [parent.key], key: "statement:child", + kind: "statement", v: 1, value: { name: "child" } }); + expect(OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1).toMatchObject({ factPackId: "oh.record-facts", + factPackRevision: 1 }); + expect(createOhProjectionRecordFactsV1([child, parent]).map((fact) => [fact.relation, fact.tuple])) + .toEqual([ + ["oh.dependency", ["statement:child", "entity:parent"]], + ["oh.record", ["entity:parent", "entity", parent.recordSha256]], + ["oh.record", ["statement:child", "statement", child.recordSha256]], + ]); + }); +}); + +describe("optional Suss equivalence adapter", () => { + test("pins the evaluated package and agrees on complete relation sets", () => { + expect(OH_PROJECTION_SUSS_VERSION_V1).toBe("0.20.0"); + const records = [edgeRecord("a", "b"), edgeRecord("b", "c"), edgeRecord("a", "d")]; + const exact = snapshot(records); + const input = { dataset: dataset(records, exact), query: allPathsQuery(), + rulePack: reachabilityRules(), snapshot: exact }; + const internal = evaluateOhProjectionV1(input); + const external = evaluateOhProjectionWithSussV1(input); + expect(external.engine).toBe(OH_PROJECTION_SUSS_ENGINE_V1); + expect(external.identity).toMatchObject({ datasetSha256: internal.identity.datasetSha256, + evaluationSha256: internal.identity.evaluationSha256, + querySha256: internal.identity.querySha256, rulePackSha256: internal.identity.rulePackSha256, + snapshotSha256: internal.identity.snapshotSha256 }); + expect(invalidationForOhProjectionV1(internal.identity, external.identity)).toEqual({ + kind: "full-rebuild", reasons: ["engine-changed"], v: 1 }); + expect(external.rows).toEqual(internal.rows); + expect(external.stats).toEqual(internal.stats); + expect(parseOhProjectionResultV1(external)).toEqual(external); + }); + + test("rejects programs Suss cannot prove within the requested bound", () => { + const single = [edgeRecord("a", "b")]; + const singleSnapshot = snapshot(single); + const direct = createOhProjectionRulePackV1({ rulePackId: "test.direct", rulePackRevision: 1, + rules: [createOhProjectionRuleV1({ body: [literal("edge", v("x"), v("y"))], + head: literal("path", v("x"), v("y")), ruleId: "path.direct" })] }); + expect(() => evaluateOhProjectionWithSussV1({ dataset: dataset(single, singleSnapshot), + options: { maximumDerivedTuples: 1 }, query: allPathsQuery(), + rulePack: direct, snapshot: singleSnapshot })).toThrow("cannot prove"); + const records = [edgeRecord("a", "b"), edgeRecord("b", "c")]; + const exact = snapshot(records); + expect(() => evaluateOhProjectionWithSussV1({ dataset: dataset(records, exact), + options: { maximumRounds: 1 }, query: allPathsQuery(), + rulePack: reachabilityRules(), snapshot: exact })).toThrow("evaluation round bound"); + }); +}); diff --git a/src/projection.ts b/src/projection.ts new file mode 100644 index 0000000..f565ced --- /dev/null +++ b/src/projection.ts @@ -0,0 +1,1457 @@ +import { + canonicalJson, + canonicalSha256, + hasExactKeys, + isPlainRecord, + orderedUnique, + parseSha256Hex, + safeCode, + sha256Hex, + sortUnique, + utf8ByteLength, + type JsonPrimitive, + type Sha256Hex, +} from "./canonical"; +import { OH_CONTRACT_MANIFEST_V1 } from "./contract"; +import { + knowledgeGraphRecordRefV1, + OH_GRAPH_LIMITS_V1, + OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + parseKnowledgeGraphRecordV1, + type KnowledgeGraphRecordKindV1, + type KnowledgeGraphRecordRefV1, + type KnowledgeGraphRecordV1, +} from "./graph"; + +export const OH_PROJECTION_FORMAT_VERSION_V1 = 1 as const; +export const OH_PROJECTION_SEMANTICS_V1 = "oh.projection.positive-datalog.v1" as const; +export const OH_PROJECTION_INTERNAL_ENGINE_V1 = "oh.naive.positive.v1" as const; + +export const OH_PROJECTION_LIMITS_V1 = Object.freeze({ + arity: 32, + atomBytes: 16 * 1024, + derivedTuples: 262_144, + facts: 262_144, + literalsPerRule: 64, + proofDepth: 128, + proofNodes: 4_096, + queryLiterals: 64, + queryMatches: 262_144, + queryResults: 65_536, + relations: 4_096, + resultBytes: 16 * 1024 * 1024, + rounds: 1_024, + rules: 1_024, + sourcesPerFact: 64, + totalProofNodes: 65_536, + variables: 256, + workUnits: 16_777_216, +}); + +export type OhProjectionAtomV1 = JsonPrimitive; + +export type OhProjectionSnapshotV1 = Readonly<{ + contractSha256: Sha256Hex; + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordRefs: readonly KnowledgeGraphRecordRefV1[]; + recordsSha256: Sha256Hex; + sequence: number; + snapshotSha256: Sha256Hex; + spaceId: string; + v: 1; +}>; + +export type OhProjectionFactSourceV1 = Readonly<{ + key: string; + recordSha256: Sha256Hex; + v: 1; +}>; + +export type OhProjectionFactV1 = Readonly<{ + factSha256: Sha256Hex; + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; +}>; + +export type OhProjectionDatasetV1 = Readonly<{ + datasetSha256: Sha256Hex; + extractorSha256: Sha256Hex; + factPackId: string; + factPackRevision: number; + factPackSha256: Sha256Hex; + facts: readonly OhProjectionFactV1[]; + factsSha256: Sha256Hex; + snapshotSha256: Sha256Hex; + v: 1; +}>; + +const recordFactExtractorPayloadV1 = { + factPackId: "oh.record-facts", + factPackRevision: 1, + relations: ["oh.dependency", "oh.record"], + semantics: OH_PROJECTION_SEMANTICS_V1, + v: 1, +} as const; + +export const OH_PROJECTION_RECORD_FACT_EXTRACTOR_V1 = Object.freeze({ + ...recordFactExtractorPayloadV1, + extractorSha256: canonicalSha256(recordFactExtractorPayloadV1), +}); + +export type OhProjectionTermV1 = + | Readonly<{ kind: "constant"; v: 1; value: OhProjectionAtomV1 }> + | Readonly<{ kind: "variable"; name: string; v: 1 }>; + +export type OhProjectionLiteralV1 = Readonly<{ + relation: string; + terms: readonly OhProjectionTermV1[]; + v: 1; +}>; + +export type OhProjectionRuleV1 = Readonly<{ + body: readonly OhProjectionLiteralV1[]; + head: OhProjectionLiteralV1; + ruleId: string; + ruleSha256: Sha256Hex; + v: 1; +}>; + +export type OhProjectionRulePackV1 = Readonly<{ + rulePackId: string; + rulePackRevision: number; + rulePackSha256: Sha256Hex; + rules: readonly OhProjectionRuleV1[]; + rulesSha256: Sha256Hex; + semantics: typeof OH_PROJECTION_SEMANTICS_V1; + v: 1; +}>; + +export type OhProjectionQueryV1 = Readonly<{ + find: readonly string[]; + limit: number; + queryId: string; + querySha256: Sha256Hex; + where: readonly OhProjectionLiteralV1[]; + v: 1; +}>; + +export type OhProjectionIdentityV1 = Readonly<{ + contractSha256: Sha256Hex; + datasetSha256: Sha256Hex; + engineSha256: Sha256Hex; + evaluationSha256: Sha256Hex; + projectionSha256: Sha256Hex; + querySha256: Sha256Hex; + rulePackSha256: Sha256Hex; + semantics: typeof OH_PROJECTION_SEMANTICS_V1; + snapshotSha256: Sha256Hex; + v: 1; +}>; + +export type OhProjectionProofV1 = + | Readonly<{ + kind: "fact"; + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }> + | Readonly<{ + kind: "derived"; + premises: readonly OhProjectionProofV1[]; + premisesTruncated: boolean; + relation: string; + ruleId: string; + ruleSha256: Sha256Hex; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }> + | Readonly<{ + kind: "truncated"; + reason: "cycle" | "depth" | "nodes"; + relation: string; + tuple: readonly OhProjectionAtomV1[]; + v: 1; + }>; + +export type OhProjectionResultRowV1 = Readonly<{ + proofs: readonly OhProjectionProofV1[]; + proofsTruncated: boolean; + supportCount: number; + values: readonly OhProjectionAtomV1[]; + v: 1; +}>; + +export type OhProjectionResultV1 = Readonly<{ + authority: "derived"; + cache: Readonly<{ strategy: "full-rebuild"; v: 1 }>; + engine: string; + evaluation: Readonly<{ + maximumDerivedTuples: number; + maximumProofDepth: number; + maximumProofNodes: number; + maximumResultBytes: number; + maximumRounds: number; + maximumTotalProofNodes: number; + maximumWorkUnits: number; + v: 1; + }>; + identity: OhProjectionIdentityV1; + resultSha256: Sha256Hex; + rows: readonly OhProjectionResultRowV1[]; + stats: Readonly<{ + baseFacts: number; + derivedFacts: number; + proofNodes: number; + queryMatches: number; + relations: number; + rounds: number; + proofsTruncated: boolean; + truncated: boolean; + truncationReasons: readonly ("query-limit" | "result-bytes")[]; + workUnits: number; + v: 1; + }>; + v: 1; +}>; + +export type OhProjectionEvaluationOptionsV1 = Readonly<{ + maximumDerivedTuples?: number; + maximumProofDepth?: number; + maximumProofNodes?: number; + maximumResultBytes?: number; + maximumRounds?: number; + maximumTotalProofNodes?: number; + maximumWorkUnits?: number; +}>; + +export type OhProjectionInvalidationReasonV1 = + | "dataset-changed" + | "engine-changed" + | "evaluation-changed" + | "query-changed" + | "rule-pack-changed" + | "snapshot-changed"; + +export type OhProjectionInvalidationV1 = + | Readonly<{ kind: "reusable"; v: 1 }> + | Readonly<{ + kind: "full-rebuild"; + reasons: readonly OhProjectionInvalidationReasonV1[]; + v: 1; + }>; + +type ProjectionHeadInputV1 = Readonly<{ + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordsSha256: Sha256Hex; + sequence: number; +}>; + +function nonnegativeInteger(value: unknown): number | null { + return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : null; +} + +function positiveInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER): number | null { + return Number.isSafeInteger(value) && (value as number) >= 1 && (value as number) <= maximum + ? value as number : null; +} + +function projectionName(value: unknown, maximumLength = 128): string | null { + return safeCode(value, maximumLength); +} + +function compareCanonical(left: unknown, right: unknown): number { + const leftKey = canonicalJson(left); + const rightKey = canonicalJson(right); + return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0; +} + +function compareProjectionFacts(left: OhProjectionFactV1, right: OhProjectionFactV1): number { + return compareCanonical([left.relation, left.tuple], [right.relation, right.tuple]); +} + +const INVALID_PROJECTION_ATOM = Symbol("invalid-projection-atom"); + +function atom(value: unknown): OhProjectionAtomV1 | typeof INVALID_PROJECTION_ATOM { + if (value !== null && typeof value !== "boolean" && typeof value !== "number" && typeof value !== "string") { + return INVALID_PROJECTION_ATOM; + } + try { + const encoded = canonicalJson(value); + return utf8ByteLength(encoded) <= OH_PROJECTION_LIMITS_V1.atomBytes + ? value as OhProjectionAtomV1 : INVALID_PROJECTION_ATOM; + } catch { + return INVALID_PROJECTION_ATOM; + } +} + +function tuple(value: unknown): readonly OhProjectionAtomV1[] | null { + if (!Array.isArray(value) || value.length < 1 || value.length > OH_PROJECTION_LIMITS_V1.arity) return null; + const parsed = value.map(atom); + return parsed.some((item) => item === INVALID_PROJECTION_ATOM) + ? null : parsed as readonly OhProjectionAtomV1[]; +} + +function parseRecordRef(value: unknown): KnowledgeGraphRecordRefV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["dependencies", "key", "kind", "sha256", "v"]) + || value.v !== 1 || !Array.isArray(value.dependencies)) return null; + const key = safeCode(value.key, 512); + const kind = OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.find((candidate) => candidate === value.kind); + const sha256 = parseSha256Hex(value.sha256); + const dependencies = value.dependencies.map((dependency) => safeCode(dependency, 512)); + if (key === null || kind === undefined || sha256 === null + || dependencies.length > OH_GRAPH_LIMITS_V1.dependenciesPerRecord + || dependencies.some((dependency) => dependency === null) + || !orderedUnique(dependencies as readonly string[], String) + || dependencies.includes(key)) return null; + return { dependencies: dependencies as readonly string[], key, kind, sha256, v: 1 }; +} + +export function createOhProjectionSnapshotV1(input: Readonly<{ + head: ProjectionHeadInputV1; + records: readonly KnowledgeGraphRecordV1[]; + spaceId: string; +}>): OhProjectionSnapshotV1 { + const spaceId = projectionName(input.spaceId); + const generation = nonnegativeInteger(input.head.generation); + const sequence = nonnegativeInteger(input.head.sequence); + const operationSha256 = input.head.operationSha256 === null ? null : parseSha256Hex(input.head.operationSha256); + const graphRevisionSha256 = input.head.graphRevisionSha256 === null + ? null : parseSha256Hex(input.head.graphRevisionSha256); + const declaredRecordsSha256 = parseSha256Hex(input.head.recordsSha256); + if (spaceId === null || generation === null || sequence === null || generation !== sequence + || (input.head.operationSha256 !== null && operationSha256 === null) + || (input.head.graphRevisionSha256 !== null && graphRevisionSha256 === null) + || ((sequence === 0) !== (operationSha256 === null)) + || ((sequence === 0) !== (graphRevisionSha256 === null)) + || declaredRecordsSha256 === null || input.records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid projection snapshot head."); + } + const records = input.records.map(parseKnowledgeGraphRecordV1); + if (records.some((record) => record === null)) throw new TypeError("Invalid record in projection snapshot."); + const recordRefs = sortUnique( + (records as readonly KnowledgeGraphRecordV1[]).map(knowledgeGraphRecordRefV1), + (reference) => reference.key, + ); + const recordsSha256 = canonicalSha256(recordRefs); + if (recordsSha256 !== declaredRecordsSha256) { + throw new TypeError("Projection snapshot records do not reproduce the declared head."); + } + const keys = new Set(recordRefs.map((reference) => reference.key)); + if (recordRefs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) { + throw new TypeError("Projection snapshot has a missing record dependency."); + } + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + generation, + graphRevisionSha256, + operationSha256, + recordRefs, + recordsSha256, + sequence, + spaceId, + v: 1 as const, + }; + return { ...payload, snapshotSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionSnapshotV1(value: unknown): OhProjectionSnapshotV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["contractSha256", "generation", + "graphRevisionSha256", "operationSha256", "recordRefs", "recordsSha256", "sequence", + "snapshotSha256", "spaceId", "v"]) || value.v !== 1 || !Array.isArray(value.recordRefs) + || value.recordRefs.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const generation = nonnegativeInteger(value.generation); + const sequence = nonnegativeInteger(value.sequence); + const graphRevisionSha256 = value.graphRevisionSha256 === null + ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + const spaceId = projectionName(value.spaceId); + const recordRefs = value.recordRefs.map(parseRecordRef); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || generation === null || sequence === null + || generation !== sequence || spaceId === null || recordsSha256 === null || snapshotSha256 === null + || (value.graphRevisionSha256 !== null && graphRevisionSha256 === null) + || (value.operationSha256 !== null && operationSha256 === null) + || ((sequence === 0) !== (operationSha256 === null)) + || ((sequence === 0) !== (graphRevisionSha256 === null)) + || recordRefs.some((reference) => reference === null)) return null; + const refs = recordRefs as readonly KnowledgeGraphRecordRefV1[]; + if (!orderedUnique(refs, (reference) => reference.key) || canonicalSha256(refs) !== recordsSha256) return null; + const keys = new Set(refs.map((reference) => reference.key)); + if (refs.some((reference) => reference.dependencies.some((dependency) => !keys.has(dependency)))) return null; + const payload = { contractSha256, generation, graphRevisionSha256, operationSha256, + recordRefs: refs, recordsSha256, sequence, spaceId, v: 1 as const }; + return canonicalSha256(payload) === snapshotSha256 ? { ...payload, snapshotSha256 } : null; +} + +export function createOhProjectionFactV1(input: Readonly<{ + relation: string; + sources: readonly OhProjectionFactSourceV1[]; + tuple: readonly OhProjectionAtomV1[]; +}>): OhProjectionFactV1 { + const relation = projectionName(input.relation); + const parsedTuple = tuple(input.tuple); + if (relation === null || parsedTuple === null || input.sources.length < 1 + || input.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) { + throw new TypeError("Invalid projection fact."); + } + const sources = input.sources.map((source) => { + if (!isPlainRecord(source) || !hasExactKeys(source, ["key", "recordSha256", "v"]) || source.v !== 1) { + throw new TypeError("Invalid projection fact source."); + } + const key = safeCode(source.key, 512); + const recordSha256 = parseSha256Hex(source.recordSha256); + if (key === null || recordSha256 === null) throw new TypeError("Invalid projection fact source."); + return { key, recordSha256, v: 1 as const }; + }).sort(compareCanonical); + if (!orderedUnique(sources, (source) => source.key)) { + throw new TypeError("Projection fact sources must have unique record keys."); + } + const payload = { relation, sources, tuple: parsedTuple, v: 1 as const }; + return { ...payload, factSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionFactV1(value: unknown): OhProjectionFactV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["factSha256", "relation", "sources", "tuple", "v"]) + || value.v !== 1 || !Array.isArray(value.sources) || !Array.isArray(value.tuple)) return null; + const factSha256 = parseSha256Hex(value.factSha256); + try { + const fact = createOhProjectionFactV1({ relation: value.relation as string, + sources: value.sources as OhProjectionFactSourceV1[], tuple: value.tuple as OhProjectionAtomV1[] }); + return factSha256 !== null && fact.factSha256 === factSha256 ? fact : null; + } catch { + return null; + } +} + +function mergeProjectionFacts(facts: readonly OhProjectionFactV1[]): readonly OhProjectionFactV1[] { + const grouped = new Map; + tuple: readonly OhProjectionAtomV1[] }>(); + for (const fact of facts) { + const identity = canonicalJson([fact.relation, fact.tuple]); + let group = grouped.get(identity); + if (group === undefined) { + group = { relation: fact.relation, sources: new Map(), tuple: fact.tuple }; + grouped.set(identity, group); + } + for (const source of fact.sources) { + const existing = group.sources.get(source.key); + if (existing !== undefined && existing.recordSha256 !== source.recordSha256) { + throw new TypeError("One fact source key is bound to multiple record digests."); + } + group.sources.set(source.key, source); + } + } + return [...grouped.values()].map((group) => createOhProjectionFactV1({ relation: group.relation, + sources: [...group.sources.values()], tuple: group.tuple })).sort(compareProjectionFacts); +} + +export function createOhProjectionDatasetV1(input: Readonly<{ + extractorSha256: Sha256Hex; + factPackId: string; + factPackRevision: number; + facts: readonly OhProjectionFactV1[]; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionDatasetV1 { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const extractorSha256 = parseSha256Hex(input.extractorSha256); + const factPackId = projectionName(input.factPackId); + const factPackRevision = positiveInteger(input.factPackRevision); + if (snapshot === null || extractorSha256 === null || factPackId === null || factPackRevision === null + || input.facts.length > OH_PROJECTION_LIMITS_V1.facts) throw new TypeError("Invalid projection dataset."); + const parsedFacts = input.facts.map(parseOhProjectionFactV1); + if (parsedFacts.some((fact) => fact === null)) throw new TypeError("Invalid fact in projection dataset."); + const facts = mergeProjectionFacts(parsedFacts as readonly OhProjectionFactV1[]); + if (facts.length > OH_PROJECTION_LIMITS_V1.facts) throw new RangeError("Projection dataset has too many facts."); + const refs = new Map(snapshot.recordRefs.map((reference) => [reference.key, reference.sha256])); + for (const fact of facts) { + for (const source of fact.sources) { + if (refs.get(source.key) !== source.recordSha256) { + throw new TypeError("Projection fact source is not present at the exact input snapshot."); + } + } + } + const factPackPayload = { extractorSha256, factPackId, factPackRevision, + semantics: OH_PROJECTION_SEMANTICS_V1, v: 1 as const }; + const factPackSha256 = canonicalSha256(factPackPayload); + const factsSha256 = canonicalSha256(facts); + const payload = { extractorSha256, factPackId, factPackRevision, factPackSha256, facts, + factsSha256, snapshotSha256: snapshot.snapshotSha256, v: 1 as const }; + return { ...payload, datasetSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionDatasetV1(value: unknown, + snapshot: OhProjectionSnapshotV1): OhProjectionDatasetV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["datasetSha256", "extractorSha256", "factPackId", + "factPackRevision", "factPackSha256", "facts", "factsSha256", "snapshotSha256", "v"]) + || value.v !== 1 || !Array.isArray(value.facts)) return null; + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const declaredFactPackSha256 = parseSha256Hex(value.factPackSha256); + const declaredFactsSha256 = parseSha256Hex(value.factsSha256); + try { + const dataset = createOhProjectionDatasetV1({ extractorSha256: value.extractorSha256 as Sha256Hex, + factPackId: value.factPackId as string, factPackRevision: value.factPackRevision as number, + facts: value.facts as OhProjectionFactV1[], snapshot }); + return datasetSha256 !== null && declaredFactPackSha256 === dataset.factPackSha256 + && declaredFactsSha256 === dataset.factsSha256 && value.snapshotSha256 === dataset.snapshotSha256 + && dataset.datasetSha256 === datasetSha256 + ? dataset : null; + } catch { + return null; + } +} + +export function ohProjectionVariableV1(name: string): OhProjectionTermV1 { + const parsed = projectionName(name); + if (parsed === null) throw new TypeError("Invalid projection variable name."); + return { kind: "variable", name: parsed, v: 1 }; +} + +export function ohProjectionConstantV1(value: OhProjectionAtomV1): OhProjectionTermV1 { + const parsed = atom(value); + if (parsed === INVALID_PROJECTION_ATOM) throw new TypeError("Invalid projection constant."); + return { kind: "constant", v: 1, value: parsed }; +} + +export function createOhProjectionLiteralV1(input: Readonly<{ + relation: string; + terms: readonly OhProjectionTermV1[]; +}>): OhProjectionLiteralV1 { + const relation = projectionName(input.relation); + if (relation === null || input.terms.length < 1 || input.terms.length > OH_PROJECTION_LIMITS_V1.arity) { + throw new TypeError("Invalid projection literal."); + } + const terms = input.terms.map((term) => parseOhProjectionTermV1(term)); + if (terms.some((term) => term === null)) throw new TypeError("Invalid term in projection literal."); + return { relation, terms: terms as readonly OhProjectionTermV1[], v: 1 }; +} + +export function parseOhProjectionTermV1(value: unknown): OhProjectionTermV1 | null { + if (!isPlainRecord(value) || value.v !== 1) return null; + if (value.kind === "variable" && hasExactKeys(value, ["kind", "name", "v"])) { + const name = projectionName(value.name); + return name === null ? null : { kind: "variable", name, v: 1 }; + } + if (value.kind === "constant" && hasExactKeys(value, ["kind", "v", "value"])) { + const parsed = atom(value.value); + return parsed === INVALID_PROJECTION_ATOM ? null : { kind: "constant", v: 1, value: parsed }; + } + return null; +} + +export function parseOhProjectionLiteralV1(value: unknown): OhProjectionLiteralV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["relation", "terms", "v"]) + || value.v !== 1 || !Array.isArray(value.terms)) return null; + try { + return createOhProjectionLiteralV1({ relation: value.relation as string, + terms: value.terms as OhProjectionTermV1[] }); + } catch { + return null; + } +} + +function literalVariables(literal: OhProjectionLiteralV1): readonly string[] { + return literal.terms.flatMap((term) => term.kind === "variable" ? [term.name] : []); +} + +export function createOhProjectionRuleV1(input: Readonly<{ + body: readonly OhProjectionLiteralV1[]; + head: OhProjectionLiteralV1; + ruleId: string; +}>): OhProjectionRuleV1 { + const ruleId = projectionName(input.ruleId); + const head = parseOhProjectionLiteralV1(input.head); + if (ruleId === null || head === null || input.body.length < 1 + || input.body.length > OH_PROJECTION_LIMITS_V1.literalsPerRule) throw new TypeError("Invalid projection rule."); + const body = input.body.map(parseOhProjectionLiteralV1); + if (body.some((literal) => literal === null)) throw new TypeError("Invalid body literal in projection rule."); + const bound = new Set((body as readonly OhProjectionLiteralV1[]).flatMap(literalVariables)); + if (literalVariables(head).some((variable) => !bound.has(variable)) + || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projection rule head variable must be bound in its body."); + } + const payload = { body: body as readonly OhProjectionLiteralV1[], head, ruleId, v: 1 as const }; + return { ...payload, ruleSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionRuleV1(value: unknown): OhProjectionRuleV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["body", "head", "ruleId", "ruleSha256", "v"]) + || value.v !== 1 || !Array.isArray(value.body)) return null; + const ruleSha256 = parseSha256Hex(value.ruleSha256); + try { + const rule = createOhProjectionRuleV1({ body: value.body as OhProjectionLiteralV1[], + head: value.head as OhProjectionLiteralV1, ruleId: value.ruleId as string }); + return ruleSha256 !== null && rule.ruleSha256 === ruleSha256 ? rule : null; + } catch { + return null; + } +} + +export function createOhProjectionRulePackV1(input: Readonly<{ + rulePackId: string; + rulePackRevision: number; + rules: readonly OhProjectionRuleV1[]; +}>): OhProjectionRulePackV1 { + const rulePackId = projectionName(input.rulePackId); + const rulePackRevision = positiveInteger(input.rulePackRevision); + if (rulePackId === null || rulePackRevision === null || input.rules.length < 1 + || input.rules.length > OH_PROJECTION_LIMITS_V1.rules) throw new TypeError("Invalid projection rule pack."); + const parsedRules = input.rules.map(parseOhProjectionRuleV1); + if (parsedRules.some((rule) => rule === null)) throw new TypeError("Invalid rule in projection rule pack."); + const rules = sortUnique(parsedRules as readonly OhProjectionRuleV1[], (rule) => rule.ruleId); + const rulesSha256 = canonicalSha256(rules); + const payload = { rulePackId, rulePackRevision, rules, rulesSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, v: 1 as const }; + return { ...payload, rulePackSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionRulePackV1(value: unknown): OhProjectionRulePackV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["rulePackId", "rulePackRevision", + "rulePackSha256", "rules", "rulesSha256", "semantics", "v"]) || value.v !== 1 + || value.semantics !== OH_PROJECTION_SEMANTICS_V1 || !Array.isArray(value.rules)) return null; + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const rulesSha256 = parseSha256Hex(value.rulesSha256); + try { + const pack = createOhProjectionRulePackV1({ rulePackId: value.rulePackId as string, + rulePackRevision: value.rulePackRevision as number, rules: value.rules as OhProjectionRuleV1[] }); + return rulePackSha256 === pack.rulePackSha256 && rulesSha256 === pack.rulesSha256 ? pack : null; + } catch { + return null; + } +} + +export function createOhProjectionQueryV1(input: Readonly<{ + find: readonly string[]; + limit?: number; + queryId: string; + where: readonly OhProjectionLiteralV1[]; +}>): OhProjectionQueryV1 { + const queryId = projectionName(input.queryId); + const limit = positiveInteger(input.limit ?? 1_000, OH_PROJECTION_LIMITS_V1.queryResults); + if (queryId === null || limit === null || input.find.length < 1 + || input.find.length > OH_PROJECTION_LIMITS_V1.arity || input.where.length < 1 + || input.where.length > OH_PROJECTION_LIMITS_V1.queryLiterals) throw new TypeError("Invalid projection query."); + const find = input.find.map((name) => projectionName(name)); + const where = input.where.map(parseOhProjectionLiteralV1); + if (find.some((name) => name === null) || !orderedUnique([...find as string[]].sort(), String) + || where.some((literal) => literal === null)) throw new TypeError("Invalid projection query variables or literals."); + const bound = new Set((where as readonly OhProjectionLiteralV1[]).flatMap(literalVariables)); + if ((find as readonly string[]).some((name) => !bound.has(name)) || bound.size > OH_PROJECTION_LIMITS_V1.variables) { + throw new TypeError("Every projected query variable must be bound in the query body."); + } + const payload = { find: find as readonly string[], limit, queryId, + where: where as readonly OhProjectionLiteralV1[], v: 1 as const }; + return { ...payload, querySha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionQueryV1(value: unknown): OhProjectionQueryV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["find", "limit", "queryId", "querySha256", "where", "v"]) + || value.v !== 1 || !Array.isArray(value.find) || !Array.isArray(value.where)) return null; + const querySha256 = parseSha256Hex(value.querySha256); + try { + const query = createOhProjectionQueryV1({ find: value.find as string[], limit: value.limit as number, + queryId: value.queryId as string, where: value.where as OhProjectionLiteralV1[] }); + return querySha256 !== null && query.querySha256 === querySha256 ? query : null; + } catch { + return null; + } +} + +export function createOhProjectionIdentityV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + engine?: string; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionIdentityV1 { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const query = parseOhProjectionQueryV1(input.query); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + if (snapshot === null || dataset === null || query === null || rulePack === null) { + throw new TypeError("Invalid projection identity input."); + } + const engine = safeCode(input.engine ?? OH_PROJECTION_INTERNAL_ENGINE_V1, 256); + if (engine === null) throw new TypeError("Invalid projection engine identity."); + const evaluation = { ...resolveEvaluationOptions(input.options ?? {}), v: 1 as const }; + const payload = { + contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + datasetSha256: dataset.datasetSha256, + engineSha256: canonicalSha256({ engine, v: 1 }), + evaluationSha256: canonicalSha256(evaluation), + querySha256: query.querySha256, + rulePackSha256: rulePack.rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, + snapshotSha256: snapshot.snapshotSha256, + v: 1 as const, + }; + return { ...payload, projectionSha256: canonicalSha256(payload) }; +} + +export function parseOhProjectionIdentityV1(value: unknown): OhProjectionIdentityV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["contractSha256", "datasetSha256", + "engineSha256", "evaluationSha256", "projectionSha256", "querySha256", "rulePackSha256", + "semantics", "snapshotSha256", "v"]) + || value.v !== 1 || value.semantics !== OH_PROJECTION_SEMANTICS_V1) return null; + const contractSha256 = parseSha256Hex(value.contractSha256); + const datasetSha256 = parseSha256Hex(value.datasetSha256); + const engineSha256 = parseSha256Hex(value.engineSha256); + const evaluationSha256 = parseSha256Hex(value.evaluationSha256); + const projectionSha256 = parseSha256Hex(value.projectionSha256); + const querySha256 = parseSha256Hex(value.querySha256); + const rulePackSha256 = parseSha256Hex(value.rulePackSha256); + const snapshotSha256 = parseSha256Hex(value.snapshotSha256); + if (contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256 || datasetSha256 === null + || engineSha256 === null || evaluationSha256 === null || projectionSha256 === null + || querySha256 === null || rulePackSha256 === null + || snapshotSha256 === null) return null; + const payload = { contractSha256, datasetSha256, engineSha256, evaluationSha256, + querySha256, rulePackSha256, + semantics: OH_PROJECTION_SEMANTICS_V1, snapshotSha256, v: 1 as const }; + return canonicalSha256(payload) === projectionSha256 ? { ...payload, projectionSha256 } : null; +} + +export function invalidationForOhProjectionV1(previous: OhProjectionIdentityV1, + next: OhProjectionIdentityV1): OhProjectionInvalidationV1 { + const parsedPrevious = parseOhProjectionIdentityV1(previous); + const parsedNext = parseOhProjectionIdentityV1(next); + if (parsedPrevious === null || parsedNext === null) throw new TypeError("Invalid projection identity."); + if (parsedPrevious.projectionSha256 === parsedNext.projectionSha256) return { kind: "reusable", v: 1 }; + const reasons: OhProjectionInvalidationReasonV1[] = []; + if (parsedPrevious.snapshotSha256 !== parsedNext.snapshotSha256) reasons.push("snapshot-changed"); + if (parsedPrevious.datasetSha256 !== parsedNext.datasetSha256) reasons.push("dataset-changed"); + if (parsedPrevious.engineSha256 !== parsedNext.engineSha256) reasons.push("engine-changed"); + if (parsedPrevious.evaluationSha256 !== parsedNext.evaluationSha256) reasons.push("evaluation-changed"); + if (parsedPrevious.rulePackSha256 !== parsedNext.rulePackSha256) reasons.push("rule-pack-changed"); + if (parsedPrevious.querySha256 !== parsedNext.querySha256) reasons.push("query-changed"); + return { kind: "full-rebuild", reasons, v: 1 }; +} + +type TupleReference = Readonly<{ relation: string; tuple: readonly OhProjectionAtomV1[] }>; +type FactWitness = Readonly<{ kind: "fact"; sources: readonly OhProjectionFactSourceV1[] }>; +type DerivedWitness = Readonly<{ + kind: "derived"; + premises: readonly TupleReference[]; + rule: OhProjectionRuleV1; +}>; +type TupleState = Readonly<{ + tuple: readonly OhProjectionAtomV1[]; + witness: FactWitness | DerivedWitness; +}>; +type RelationState = Map; +type MaterializedProjection = Readonly<{ + baseFacts: number; + derivedFacts: number; + relations: Map; + rounds: number; +}>; + +function tupleKey(value: readonly OhProjectionAtomV1[]): string { + return canonicalJson(value); +} + +function referenceKey(reference: TupleReference): string { + return canonicalJson([reference.relation, reference.tuple]); +} + +function relationTuples(relations: Map, relation: string): readonly TupleState[] { + return [...(relations.get(relation)?.values() ?? [])] + .sort((left, right) => compareCanonical(left.tuple, right.tuple)); +} + +function setArity(arities: Map, relation: string, arity: number): void { + const existing = arities.get(relation); + if (existing !== undefined && existing !== arity) { + throw new TypeError(`Projection relation ${relation} is used with conflicting arities.`); + } + arities.set(relation, arity); + if (arities.size > OH_PROJECTION_LIMITS_V1.relations) throw new RangeError("Projection uses too many relations."); +} + +function validateProgramArities(dataset: OhProjectionDatasetV1, rulePack: OhProjectionRulePackV1, + query: OhProjectionQueryV1): void { + const arities = new Map(); + for (const fact of dataset.facts) setArity(arities, fact.relation, fact.tuple.length); + for (const rule of rulePack.rules) { + setArity(arities, rule.head.relation, rule.head.terms.length); + for (const literal of rule.body) setArity(arities, literal.relation, literal.terms.length); + } + for (const literal of query.where) setArity(arities, literal.relation, literal.terms.length); +} + +type Binding = Map; + +function sameAtom(left: OhProjectionAtomV1, right: OhProjectionAtomV1): boolean { + return left === right; +} + +function unifyLiteral(literal: OhProjectionLiteralV1, state: TupleState, binding: Binding): Binding | null { + const next = new Map(binding); + for (let index = 0; index < literal.terms.length; index += 1) { + const term = literal.terms[index] as OhProjectionTermV1; + const value = state.tuple[index] as OhProjectionAtomV1; + if (term.kind === "constant") { + if (!sameAtom(term.value, value)) return null; + continue; + } + if (next.has(term.name)) { + if (!sameAtom(next.get(term.name) as OhProjectionAtomV1, value)) return null; + } else next.set(term.name, value); + } + return next; +} + +type BodyMatch = Readonly<{ binding: Binding; premises: readonly TupleReference[] }>; + +type ProjectionWorkBudget = { maximum: number; units: number }; + +function consumeWorkUnit(budget: ProjectionWorkBudget): void { + if (budget.units >= budget.maximum) throw new RangeError("Projection exceeds its work-unit bound."); + budget.units += 1; +} + +function matchBody(relations: Map, body: readonly OhProjectionLiteralV1[], + maximumMatches: number, work: ProjectionWorkBudget): readonly BodyMatch[] { + let matches: readonly BodyMatch[] = [{ binding: new Map(), premises: [] }]; + for (const literal of body) { + const next: BodyMatch[] = []; + const candidates = relationTuples(relations, literal.relation); + for (const match of matches) { + for (const candidate of candidates) { + consumeWorkUnit(work); + const binding = unifyLiteral(literal, candidate, match.binding); + if (binding === null) continue; + next.push({ binding, premises: [...match.premises, { relation: literal.relation, + tuple: candidate.tuple }] }); + if (next.length > maximumMatches) throw new RangeError("Projection join exceeds its match bound."); + } + } + matches = next; + if (matches.length === 0) break; + } + return matches; +} + +function instantiateHead(head: OhProjectionLiteralV1, binding: Binding): readonly OhProjectionAtomV1[] { + return head.terms.map((term) => term.kind === "constant" + ? term.value : binding.get(term.name) as OhProjectionAtomV1); +} + +function canonicalWitness(witness: FactWitness | DerivedWitness): string { + if (witness.kind === "fact") return canonicalJson(witness); + return canonicalJson({ kind: witness.kind, premises: witness.premises, ruleSha256: witness.rule.ruleSha256 }); +} + +function materializeNaive(input: Readonly<{ + dataset: OhProjectionDatasetV1; + maximumDerivedTuples: number; + maximumRounds: number; + rulePack: OhProjectionRulePackV1; + work: ProjectionWorkBudget; +}>): MaterializedProjection { + const relations = new Map(); + for (const fact of input.dataset.facts) { + let relation = relations.get(fact.relation); + if (relation === undefined) { + relation = new Map(); + relations.set(fact.relation, relation); + } + relation.set(tupleKey(fact.tuple), { tuple: fact.tuple, witness: { kind: "fact", sources: fact.sources } }); + } + let derivedFacts = 0; + let rounds = 0; + while (true) { + const candidates = new Map>(); + for (const rule of input.rulePack.rules) { + for (const match of matchBody(relations, rule.body, OH_PROJECTION_LIMITS_V1.queryMatches, input.work)) { + const derivedTuple = instantiateHead(rule.head, match.binding); + const relation = relations.get(rule.head.relation); + const key = tupleKey(derivedTuple); + if (relation?.has(key) === true) continue; + const state: TupleState = { tuple: derivedTuple, + witness: { kind: "derived", premises: match.premises, rule } }; + const identity = referenceKey({ relation: rule.head.relation, tuple: derivedTuple }); + const existing = candidates.get(identity); + if (existing === undefined || canonicalWitness(state.witness) < canonicalWitness(existing.state.witness)) { + candidates.set(identity, { relation: rule.head.relation, state }); + } + } + } + if (candidates.size === 0) break; + if (rounds >= input.maximumRounds) throw new RangeError("Projection exceeds its evaluation round bound."); + if (derivedFacts + candidates.size > input.maximumDerivedTuples) { + throw new RangeError("Projection exceeds its derived tuple bound."); + } + const ordered = [...candidates.values()].sort((left, right) => compareCanonical( + [left.relation, left.state.tuple], [right.relation, right.state.tuple])); + for (const candidate of ordered) { + let relation = relations.get(candidate.relation); + if (relation === undefined) { + relation = new Map(); + relations.set(candidate.relation, relation); + } + relation.set(tupleKey(candidate.state.tuple), candidate.state); + } + derivedFacts += candidates.size; + rounds += 1; + } + return { baseFacts: input.dataset.facts.length, derivedFacts, relations, rounds }; +} + +function boundedOption(value: number | undefined, fallback: number, maximum: number, label: string): number { + const parsed = positiveInteger(value ?? fallback, maximum); + if (parsed === null) throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`); + return parsed; +} + +type ResolvedEvaluationOptions = Readonly<{ + maximumDerivedTuples: number; + maximumProofDepth: number; + maximumProofNodes: number; + maximumResultBytes: number; + maximumRounds: number; + maximumTotalProofNodes: number; + maximumWorkUnits: number; +}>; + +function resolveEvaluationOptions(options: OhProjectionEvaluationOptionsV1): ResolvedEvaluationOptions { + const resolved = { + maximumDerivedTuples: boundedOption(options.maximumDerivedTuples, OH_PROJECTION_LIMITS_V1.derivedTuples, + OH_PROJECTION_LIMITS_V1.derivedTuples, "maximumDerivedTuples"), + maximumProofDepth: boundedOption(options.maximumProofDepth, 32, + OH_PROJECTION_LIMITS_V1.proofDepth, "maximumProofDepth"), + maximumProofNodes: boundedOption(options.maximumProofNodes, 1_024, + OH_PROJECTION_LIMITS_V1.proofNodes, "maximumProofNodes"), + maximumResultBytes: boundedOption(options.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes, + OH_PROJECTION_LIMITS_V1.resultBytes, "maximumResultBytes"), + maximumRounds: boundedOption(options.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds, + OH_PROJECTION_LIMITS_V1.rounds, "maximumRounds"), + maximumTotalProofNodes: boundedOption(options.maximumTotalProofNodes, + OH_PROJECTION_LIMITS_V1.totalProofNodes, OH_PROJECTION_LIMITS_V1.totalProofNodes, + "maximumTotalProofNodes"), + maximumWorkUnits: boundedOption(options.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits, + OH_PROJECTION_LIMITS_V1.workUnits, "maximumWorkUnits"), + }; + if (resolved.maximumResultBytes < 64 * 1024) { + throw new RangeError("maximumResultBytes must be at least 65536."); + } + return resolved; +} + +type ProjectionResultBudget = { bytes: number; maximumBytes: number; nodes: number }; +type ProjectionRowProofBudget = { nodes: number; result: ProjectionResultBudget }; + +function reserveResultBytes(budget: ProjectionResultBudget, value: unknown): boolean { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) return false; + budget.bytes += bytes; + return true; +} + +function reserveProofNode(budget: ProjectionRowProofBudget, options: ResolvedEvaluationOptions, + envelope: OhProjectionProofV1): boolean { + if (budget.nodes >= options.maximumProofNodes + || budget.result.nodes >= options.maximumTotalProofNodes + || !reserveResultBytes(budget.result, envelope)) return false; + budget.nodes += 1; + budget.result.nodes += 1; + return true; +} + +function proofForReference(relations: Map, reference: TupleReference, + budget: ProjectionRowProofBudget, options: ResolvedEvaluationOptions, depth: number, + visiting: Set): OhProjectionProofV1 | null { + if (depth >= options.maximumProofDepth) { + const proof = { kind: "truncated" as const, reason: "depth" as const, + relation: reference.relation, tuple: reference.tuple, v: 1 as const }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const identity = referenceKey(reference); + if (visiting.has(identity)) { + const proof = { kind: "truncated" as const, reason: "cycle" as const, + relation: reference.relation, tuple: reference.tuple, v: 1 as const }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const state = relations.get(reference.relation)?.get(tupleKey(reference.tuple)); + if (state === undefined) throw new Error("Projection proof references a tuple outside the materialized result."); + if (state.witness.kind === "fact") { + const proof = { kind: "fact" as const, relation: reference.relation, sources: state.witness.sources, + tuple: reference.tuple, v: 1 as const }; + return reserveProofNode(budget, options, proof) ? proof : null; + } + const envelope = { kind: "derived" as const, premises: [], premisesTruncated: false, + relation: reference.relation, ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, tuple: reference.tuple, v: 1 as const }; + if (!reserveProofNode(budget, options, envelope)) return null; + visiting.add(identity); + try { + const premises: OhProjectionProofV1[] = []; + let premisesTruncated = false; + for (const premise of state.witness.premises) { + const proof = proofForReference(relations, premise, budget, options, depth + 1, visiting); + if (proof === null) { premisesTruncated = true; break; } + premises.push(proof); + } + return { kind: "derived", premises, premisesTruncated, + relation: reference.relation, ruleId: state.witness.rule.ruleId, + ruleSha256: state.witness.rule.ruleSha256, tuple: reference.tuple, v: 1 }; + } finally { + visiting.delete(identity); + } +} + +function proofIsTruncated(proof: OhProjectionProofV1): boolean { + return proof.kind === "truncated" || (proof.kind === "derived" + && (proof.premisesTruncated || proof.premises.some(proofIsTruncated))); +} + +type ProjectionParseBudget = { + bytes: number; + maximumBytes: number; + maximumDepth: number; + maximumNodes: number; + nodes: number; +}; + +function reserveProjectionParseBytes(budget: ProjectionParseBudget, value: unknown): boolean { + const bytes = utf8ByteLength(canonicalJson(value)); + if (budget.bytes + bytes > budget.maximumBytes) return false; + budget.bytes += bytes; + return true; +} + +function parseProjectionFactSource(value: unknown): OhProjectionFactSourceV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["key", "recordSha256", "v"]) || value.v !== 1) { + return null; + } + const key = safeCode(value.key, 512); + const recordSha256 = parseSha256Hex(value.recordSha256); + return key === null || recordSha256 === null ? null : { key, recordSha256, v: 1 }; +} + +function parseProjectionProofWithBudget(value: unknown, budget: ProjectionParseBudget, + depth: number): OhProjectionProofV1 | null { + if (depth > budget.maximumDepth || budget.nodes >= budget.maximumNodes + || !isPlainRecord(value) || value.v !== 1) return null; + const relation = projectionName(value.relation); + const parsedTuple = tuple(value.tuple); + if (relation === null || parsedTuple === null) return null; + + if (value.kind === "fact") { + if (!hasExactKeys(value, ["kind", "relation", "sources", "tuple", "v"]) + || !Array.isArray(value.sources) || value.sources.length < 1 + || value.sources.length > OH_PROJECTION_LIMITS_V1.sourcesPerFact) return null; + const sources = value.sources.map(parseProjectionFactSource); + if (sources.some((source) => source === null)) return null; + const parsedSources = sources as readonly OhProjectionFactSourceV1[]; + if (!orderedUnique(parsedSources, (source) => source.key)) return null; + const proof = { kind: "fact" as const, relation, sources: parsedSources, + tuple: parsedTuple, v: 1 as const }; + if (!reserveProjectionParseBytes(budget, proof)) return null; + budget.nodes += 1; + return proof; + } + + if (value.kind === "truncated") { + if (!hasExactKeys(value, ["kind", "reason", "relation", "tuple", "v"]) + || (value.reason !== "cycle" && value.reason !== "depth" && value.reason !== "nodes")) return null; + const reason = value.reason as "cycle" | "depth" | "nodes"; + const proof = { kind: "truncated" as const, reason, + relation, tuple: parsedTuple, v: 1 as const }; + if (!reserveProjectionParseBytes(budget, proof)) return null; + budget.nodes += 1; + return proof; + } + + if (value.kind !== "derived" || !hasExactKeys(value, ["kind", "premises", "premisesTruncated", + "relation", "ruleId", "ruleSha256", "tuple", "v"]) || !Array.isArray(value.premises) + || value.premises.length > OH_PROJECTION_LIMITS_V1.literalsPerRule + || typeof value.premisesTruncated !== "boolean") return null; + const ruleId = projectionName(value.ruleId); + const ruleSha256 = parseSha256Hex(value.ruleSha256); + if (ruleId === null || ruleSha256 === null + || (!value.premisesTruncated && value.premises.length === 0) + || (value.premisesTruncated && value.premises.length === OH_PROJECTION_LIMITS_V1.literalsPerRule)) { + return null; + } + const skeleton = { kind: "derived" as const, premises: [], + premisesTruncated: value.premisesTruncated, relation, ruleId, ruleSha256, + tuple: parsedTuple, v: 1 as const }; + if (!reserveProjectionParseBytes(budget, skeleton)) return null; + budget.nodes += 1; + const premises: OhProjectionProofV1[] = []; + for (const premise of value.premises) { + const parsed = parseProjectionProofWithBudget(premise, budget, depth + 1); + if (parsed === null) return null; + premises.push(parsed); + } + return { ...skeleton, premises }; +} + +/** + * Parses one untrusted proof tree under the public hard depth, node, and byte + * ceilings. Cached result envelopes should normally be parsed as a whole with + * `parseOhProjectionResultV1`, which also applies their smaller declared limits. + */ +export function parseOhProjectionProofV1(value: unknown): OhProjectionProofV1 | null { + try { + const budget: ProjectionParseBudget = { bytes: 0, + maximumBytes: OH_PROJECTION_LIMITS_V1.resultBytes, + maximumDepth: OH_PROJECTION_LIMITS_V1.proofDepth, + maximumNodes: OH_PROJECTION_LIMITS_V1.proofNodes, nodes: 0 }; + const proof = parseProjectionProofWithBudget(value, budget, 0); + return proof !== null && utf8ByteLength(canonicalJson(proof)) <= budget.maximumBytes ? proof : null; + } catch { + return null; + } +} + +type OhProjectionResolvedEvaluationV1 = ResolvedEvaluationOptions & Readonly<{ v: 1 }>; + +function parseProjectionEvaluation(value: unknown): OhProjectionResolvedEvaluationV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["maximumDerivedTuples", "maximumProofDepth", + "maximumProofNodes", "maximumResultBytes", "maximumRounds", "maximumTotalProofNodes", + "maximumWorkUnits", "v"]) || value.v !== 1) return null; + const maximumDerivedTuples = positiveInteger(value.maximumDerivedTuples, + OH_PROJECTION_LIMITS_V1.derivedTuples); + const maximumProofDepth = positiveInteger(value.maximumProofDepth, OH_PROJECTION_LIMITS_V1.proofDepth); + const maximumProofNodes = positiveInteger(value.maximumProofNodes, OH_PROJECTION_LIMITS_V1.proofNodes); + const maximumResultBytes = positiveInteger(value.maximumResultBytes, OH_PROJECTION_LIMITS_V1.resultBytes); + const maximumRounds = positiveInteger(value.maximumRounds, OH_PROJECTION_LIMITS_V1.rounds); + const maximumTotalProofNodes = positiveInteger(value.maximumTotalProofNodes, + OH_PROJECTION_LIMITS_V1.totalProofNodes); + const maximumWorkUnits = positiveInteger(value.maximumWorkUnits, OH_PROJECTION_LIMITS_V1.workUnits); + if (maximumDerivedTuples === null || maximumProofDepth === null || maximumProofNodes === null + || maximumResultBytes === null || maximumResultBytes < 64 * 1024 || maximumRounds === null + || maximumTotalProofNodes === null || maximumWorkUnits === null) return null; + return { maximumDerivedTuples, maximumProofDepth, maximumProofNodes, maximumResultBytes, + maximumRounds, maximumTotalProofNodes, maximumWorkUnits, v: 1 }; +} + +function parseProjectionResultRow(value: unknown, evaluation: OhProjectionResolvedEvaluationV1, + resultBudget: ProjectionParseBudget): Readonly<{ nodes: number; row: OhProjectionResultRowV1 }> | null { + if (!isPlainRecord(value) || !hasExactKeys(value, + ["proofs", "proofsTruncated", "supportCount", "values", "v"]) || value.v !== 1 + || !Array.isArray(value.proofs) || value.proofs.length > OH_PROJECTION_LIMITS_V1.queryLiterals + || typeof value.proofsTruncated !== "boolean") return null; + const values = tuple(value.values); + const supportCount = positiveInteger(value.supportCount, OH_PROJECTION_LIMITS_V1.queryMatches); + if (values === null || supportCount === null || (!value.proofsTruncated && value.proofs.length === 0)) return null; + if (!reserveProjectionParseBytes(resultBudget, { proofs: [], proofsTruncated: value.proofsTruncated, + supportCount, values, v: 1 })) return null; + const before = resultBudget.nodes; + resultBudget.maximumNodes = Math.min(resultBudget.maximumNodes, before + evaluation.maximumProofNodes); + const proofs: OhProjectionProofV1[] = []; + for (const proof of value.proofs) { + const parsed = parseProjectionProofWithBudget(proof, resultBudget, 0); + if (parsed === null) return null; + proofs.push(parsed); + } + resultBudget.maximumNodes = evaluation.maximumTotalProofNodes; + const containsTruncation = proofs.some(proofIsTruncated); + if ((!value.proofsTruncated && containsTruncation) + || (value.proofsTruncated && proofs.length === OH_PROJECTION_LIMITS_V1.queryLiterals + && !containsTruncation)) return null; + return { nodes: resultBudget.nodes - before, + row: { proofs, proofsTruncated: value.proofsTruncated, supportCount, values, v: 1 } }; +} + +/** + * Parses a cached projection result as untrusted data. It verifies exact keys, + * all aggregate and declared bounds, proof truncation markers, canonical row + * order, engine/evaluation identity links, and `resultSha256`. Pass the + * projection digest requested from a cache to reject identity substitution. + */ +export function parseOhProjectionResultV1(value: unknown, + expectedProjectionSha256?: Sha256Hex): OhProjectionResultV1 | null { + try { + if (!isPlainRecord(value) || !hasExactKeys(value, ["authority", "cache", "engine", "evaluation", + "identity", "resultSha256", "rows", "stats", "v"]) || value.v !== 1 + || value.authority !== "derived" || !isPlainRecord(value.cache) + || !hasExactKeys(value.cache, ["strategy", "v"]) || value.cache.strategy !== "full-rebuild" + || value.cache.v !== 1 || !Array.isArray(value.rows) + || value.rows.length > OH_PROJECTION_LIMITS_V1.queryResults || !isPlainRecord(value.stats) + || !hasExactKeys(value.stats, ["baseFacts", "derivedFacts", "proofNodes", "proofsTruncated", + "queryMatches", "relations", "rounds", "truncated", "truncationReasons", "v", "workUnits"]) + || value.stats.v !== 1 || !Array.isArray(value.stats.truncationReasons) + || typeof value.stats.proofsTruncated !== "boolean" || typeof value.stats.truncated !== "boolean") return null; + const engine = safeCode(value.engine, 256); + const evaluation = parseProjectionEvaluation(value.evaluation); + const identity = parseOhProjectionIdentityV1(value.identity); + const resultSha256 = parseSha256Hex(value.resultSha256); + const expected = expectedProjectionSha256 === undefined ? undefined + : parseSha256Hex(expectedProjectionSha256); + if (engine === null || evaluation === null || identity === null || resultSha256 === null + || (expectedProjectionSha256 !== undefined && expected === null) + || (expected !== undefined && identity.projectionSha256 !== expected) + || identity.engineSha256 !== canonicalSha256({ engine, v: 1 }) + || identity.evaluationSha256 !== canonicalSha256(evaluation)) return null; + + const baseFacts = nonnegativeInteger(value.stats.baseFacts); + const derivedFacts = nonnegativeInteger(value.stats.derivedFacts); + const proofNodes = nonnegativeInteger(value.stats.proofNodes); + const queryMatches = nonnegativeInteger(value.stats.queryMatches); + const relations = nonnegativeInteger(value.stats.relations); + const rounds = nonnegativeInteger(value.stats.rounds); + const workUnits = nonnegativeInteger(value.stats.workUnits); + if (baseFacts === null || baseFacts > OH_PROJECTION_LIMITS_V1.facts + || derivedFacts === null || derivedFacts > evaluation.maximumDerivedTuples + || proofNodes === null || proofNodes > evaluation.maximumTotalProofNodes + || queryMatches === null || queryMatches > OH_PROJECTION_LIMITS_V1.queryMatches + || relations === null || relations > OH_PROJECTION_LIMITS_V1.relations + || rounds === null || rounds > evaluation.maximumRounds + || workUnits === null || workUnits > evaluation.maximumWorkUnits + || relations > baseFacts + derivedFacts || rounds > derivedFacts + || ((rounds === 0) !== (derivedFacts === 0)) || queryMatches > workUnits) return null; + const truncationReasons = value.stats.truncationReasons; + if (truncationReasons.length > 2 + || !orderedUnique(truncationReasons, (reason) => reason === "query-limit" ? "0" : reason === "result-bytes" ? "1" : "x") + || truncationReasons.some((reason) => reason !== "query-limit" && reason !== "result-bytes") + || value.stats.truncated !== (truncationReasons.length > 0)) return null; + + const budget: ProjectionParseBudget = { bytes: 0, maximumBytes: evaluation.maximumResultBytes, + maximumDepth: evaluation.maximumProofDepth, maximumNodes: evaluation.maximumTotalProofNodes, nodes: 0 }; + const rows: OhProjectionResultRowV1[] = []; + let supportCount = 0; + for (const row of value.rows) { + const parsed = parseProjectionResultRow(row, evaluation, budget); + if (parsed === null) return null; + rows.push(parsed.row); + supportCount += parsed.row.supportCount; + if (supportCount > queryMatches) return null; + } + if (!orderedUnique(rows, (row) => canonicalJson(row.values)) || budget.nodes !== proofNodes + || value.stats.proofsTruncated !== rows.some((row) => row.proofsTruncated) + || (value.stats.truncated ? supportCount >= queryMatches : supportCount !== queryMatches)) return null; + + const reasons = truncationReasons as readonly ("query-limit" | "result-bytes")[]; + const payload = { + authority: "derived" as const, + cache: { strategy: "full-rebuild" as const, v: 1 as const }, + engine, + evaluation, + identity, + rows, + stats: { baseFacts, derivedFacts, proofNodes, proofsTruncated: value.stats.proofsTruncated, + queryMatches, relations, rounds, truncated: value.stats.truncated, + truncationReasons: reasons, v: 1 as const, workUnits }, + v: 1 as const, + }; + const serialized = canonicalJson(payload); + return utf8ByteLength(serialized) <= evaluation.maximumResultBytes + && sha256Hex(serialized) === resultSha256 + ? { ...payload, resultSha256 } : null; + } catch { + return null; + } +} + +function buildProjectionResult(input: Readonly<{ + dataset: OhProjectionDatasetV1; + engine: string; + materialized: MaterializedProjection; + options: ResolvedEvaluationOptions; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; + work: ProjectionWorkBudget; +}>): OhProjectionResultV1 { + const matches = matchBody(input.materialized.relations, input.query.where, + OH_PROJECTION_LIMITS_V1.queryMatches, input.work); + const byValues = new Map(); + for (const match of matches) { + const values = input.query.find.map((name) => match.binding.get(name) as OhProjectionAtomV1); + const key = tupleKey(values); + const existing = byValues.get(key); + if (existing === undefined) byValues.set(key, { match, supportCount: 1 }); + else byValues.set(key, { match: compareCanonical(match.premises, existing.match.premises) < 0 + ? match : existing.match, supportCount: existing.supportCount + 1 }); + } + const ordered = [...byValues.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); + const resultBudget: ProjectionResultBudget = { bytes: 0, + maximumBytes: input.options.maximumResultBytes - 64 * 1024, nodes: 0 }; + const rows: OhProjectionResultRowV1[] = []; + let resultBytesTruncated = false; + for (const [key, support] of ordered.slice(0, input.query.limit)) { + const values = JSON.parse(key) as OhProjectionAtomV1[]; + if (!reserveResultBytes(resultBudget, { proofs: [], proofsTruncated: false, + supportCount: support.supportCount, values, v: 1 })) { + resultBytesTruncated = true; + break; + } + const budget: ProjectionRowProofBudget = { nodes: 0, result: resultBudget }; + const proofs: OhProjectionProofV1[] = []; + for (const premise of support.match.premises) { + const proof = proofForReference(input.materialized.relations, premise, budget, input.options, 0, new Set()); + if (proof === null) break; + proofs.push(proof); + } + const proofsTruncated = proofs.length !== support.match.premises.length || proofs.some(proofIsTruncated); + rows.push({ proofs, proofsTruncated, supportCount: support.supportCount, values, v: 1 }); + } + const queryLimitTruncated = ordered.length > input.query.limit; + const truncationReasons = [ + ...(queryLimitTruncated ? ["query-limit" as const] : []), + ...(resultBytesTruncated ? ["result-bytes" as const] : []), + ]; + const truncated = truncationReasons.length > 0; + const identity = createOhProjectionIdentityV1({ dataset: input.dataset, query: input.query, + engine: input.engine, options: input.options, rulePack: input.rulePack, snapshot: input.snapshot }); + const payload = { + authority: "derived" as const, + cache: { strategy: "full-rebuild" as const, v: 1 as const }, + engine: input.engine, + evaluation: { ...input.options, v: 1 as const }, + identity, + rows, + stats: { baseFacts: input.materialized.baseFacts, derivedFacts: input.materialized.derivedFacts, + proofNodes: resultBudget.nodes, proofsTruncated: rows.some((row) => row.proofsTruncated), + queryMatches: matches.length, + relations: input.materialized.relations.size, rounds: input.materialized.rounds, + truncated, truncationReasons, v: 1 as const, workUnits: input.work.units }, + v: 1 as const, + }; + const serialized = canonicalJson(payload); + if (utf8ByteLength(serialized) > input.options.maximumResultBytes) { + throw new RangeError("Projection result exceeds its canonical byte bound."); + } + return { ...payload, resultSha256: sha256Hex(serialized) }; +} + +export function evaluateOhProjectionV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1 { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + if (snapshot === null || dataset === null || rulePack === null || query === null) { + throw new TypeError("Invalid projection snapshot, dataset, rule pack, or query."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const materialized = materializeNaive({ dataset, + maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, rulePack, work }); + return buildProjectionResult({ dataset, engine: OH_PROJECTION_INTERNAL_ENGINE_V1, + materialized, options, query, rulePack, snapshot, work }); +} + +/** + * Internal adapter seam. It is exported for package-owned optional engines, + * not as authority: callers receive the same derived-only result envelope. + */ +export function evaluateOhProjectionWithMaterializerV1(input: Readonly<{ + dataset: OhProjectionDatasetV1; + engine: string; + materialize: (program: Readonly<{ + dataset: OhProjectionDatasetV1; + maximumDerivedTuples: number; + maximumRounds: number; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + }>) => Readonly<{ relationFacts: ReadonlyMap }>; + options?: OhProjectionEvaluationOptionsV1; + query: OhProjectionQueryV1; + rulePack: OhProjectionRulePackV1; + snapshot: OhProjectionSnapshotV1; +}>): OhProjectionResultV1 { + const snapshot = parseOhProjectionSnapshotV1(input.snapshot); + const dataset = snapshot === null ? null : parseOhProjectionDatasetV1(input.dataset, snapshot); + const rulePack = parseOhProjectionRulePackV1(input.rulePack); + const query = parseOhProjectionQueryV1(input.query); + const engine = safeCode(input.engine, 256); + if (snapshot === null || dataset === null || rulePack === null || query === null || engine === null) { + throw new TypeError("Invalid projection adapter input."); + } + const options = resolveEvaluationOptions(input.options ?? {}); + validateProgramArities(dataset, rulePack, query); + const work = { maximum: options.maximumWorkUnits, units: 0 }; + const witnessMaterialization = materializeNaive({ dataset, + maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, rulePack, work }); + const external = input.materialize({ dataset, + maximumDerivedTuples: options.maximumDerivedTuples, maximumRounds: options.maximumRounds, query, rulePack }); + const externalCanonical = new Map(); + for (const [relationName, tuples] of external.relationFacts) { + const relation = projectionName(relationName); + if (relation === null || tuples.length > OH_PROJECTION_LIMITS_V1.facts + options.maximumDerivedTuples) { + throw new TypeError("Projection adapter returned an invalid relation."); + } + const parsed = tuples.map(tuple); + if (parsed.some((value) => value === null)) throw new TypeError("Projection adapter returned an invalid tuple."); + const keys: string[] = []; + for (const value of parsed) { + if (value === null) throw new TypeError("Projection adapter returned an invalid tuple."); + keys.push(tupleKey(value)); + } + externalCanonical.set(relation, [...new Set(keys)].sort()); + } + const expectedCanonical = new Map([...witnessMaterialization.relations.entries()].map(([relation, states]) => + [relation, [...states.values()].map((state) => tupleKey(state.tuple)).sort()] as const)); + const relationNames = [...new Set([...externalCanonical.keys(), ...expectedCanonical.keys()])].sort(); + for (const relation of relationNames) { + if (canonicalJson(externalCanonical.get(relation) ?? []) !== canonicalJson(expectedCanonical.get(relation) ?? [])) { + throw new Error(`Projection adapter disagrees with Oh semantics for relation ${relation}.`); + } + } + return buildProjectionResult({ dataset, engine, + materialized: witnessMaterialization, options, query, rulePack, snapshot, work }); +} + +export type OhProjectionRecordFactOptionsV1 = Readonly<{ + includeDependencies?: boolean; + includeRecords?: boolean; +}>; + +/** Builds the stable structural fact layer shared by every domain fact pack. */ +export function createOhProjectionRecordFactsV1(records: readonly KnowledgeGraphRecordV1[], + options: OhProjectionRecordFactOptionsV1 = {}): readonly OhProjectionFactV1[] { + if (records.length > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) throw new RangeError("Too many records for projection facts."); + const parsedRecords = [...records] + .sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0) + .map((candidate) => { + const record = parseKnowledgeGraphRecordV1(candidate); + if (record === null) throw new TypeError("Invalid graph record for projection facts."); + return record; + }); + let projectedFactCount = 0; + for (const record of parsedRecords) { + if (options.includeRecords !== false) projectedFactCount += 1; + if (options.includeDependencies !== false) projectedFactCount += record.dependencies.length; + if (projectedFactCount > OH_PROJECTION_LIMITS_V1.facts) { + throw new RangeError("Structural projection exceeds its fact bound."); + } + } + const facts: OhProjectionFactV1[] = []; + for (const record of parsedRecords) { + const source = [{ key: record.key, recordSha256: record.recordSha256, v: 1 as const }]; + if (options.includeRecords !== false) { + facts.push(createOhProjectionFactV1({ relation: "oh.record", sources: source, + tuple: [record.key, record.kind, record.recordSha256] })); + } + if (options.includeDependencies !== false) { + for (const dependency of record.dependencies) { + facts.push(createOhProjectionFactV1({ relation: "oh.dependency", sources: source, + tuple: [record.key, dependency] })); + } + } + } + return facts.sort(compareProjectionFacts); +} + +export function isOhProjectionRecordKindV1(value: unknown): value is KnowledgeGraphRecordKindV1 { + return OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === value); +} diff --git a/src/sqlite/driver.ts b/src/sqlite/driver.ts index 45e239e..81fbcf4 100644 --- a/src/sqlite/driver.ts +++ b/src/sqlite/driver.ts @@ -33,3 +33,15 @@ export function withImmediateTransaction(database: OhSqliteDatabase, work: () throw error; } } + +export function withReadTransaction(database: OhSqliteDatabase, work: () => T): T { + database.exec("BEGIN"); + try { + const result = work(); + database.exec("COMMIT"); + return result; + } catch (error) { + try { database.exec("ROLLBACK"); } catch { /* preserve the original failure */ } + throw error; + } +} diff --git a/src/sqlite/index.ts b/src/sqlite/index.ts index 2db1f2d..d6c0eae 100644 --- a/src/sqlite/index.ts +++ b/src/sqlite/index.ts @@ -1,3 +1,4 @@ export * from "./driver"; export * from "./migrations"; +export * from "./port"; export * from "./store"; diff --git a/src/sqlite/migrations.test.ts b/src/sqlite/migrations.test.ts new file mode 100644 index 0000000..1bb6055 --- /dev/null +++ b/src/sqlite/migrations.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; + +import { canonicalNow, sha256Hex } from "../canonical"; +import { openOhSqliteDatabase } from "./driver"; +import { OH_SQLITE_MIGRATIONS, OH_SQLITE_SCHEMA_VERSION } from "./migrations"; +import { OhSqliteStore } from "./store"; + +describe("SQLite schema evolution", () => { + test("preserves the released 0001 bytes and appends store realms as version 2", () => { + expect(OH_SQLITE_SCHEMA_VERSION).toBe(2); + expect(OH_SQLITE_MIGRATIONS.map(({ name, version }) => ({ name, version }))).toEqual([ + { name: "0001_oh_core", version: 1 }, + { name: "0002_store_realms", version: 2 }, + ]); + expect(String(sha256Hex(OH_SQLITE_MIGRATIONS[0]?.sql ?? ""))).toBe( + "f525fc4f521b544d8e00526f2993a3b6bf81ae936950765c51406566ea4b1b7c", + ); + }); + + test("upgrades an applied version-1 authority without rewriting its migration evidence", () => { + const database = openOhSqliteDatabase(":memory:"); + const first = OH_SQLITE_MIGRATIONS[0]; + if (first === undefined) throw new Error("Missing first migration."); + database.exec(`CREATE TABLE oh_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + migration_sha256 TEXT NOT NULL CHECK(length(migration_sha256) = 64), + applied_at TEXT NOT NULL + ) STRICT`); + database.exec(first.sql); + database.query(`INSERT INTO oh_migrations(version, name, migration_sha256, applied_at) + VALUES (?, ?, ?, ?)`).run(first.version, first.name, sha256Hex(first.sql), canonicalNow()); + + const store = new OhSqliteStore({ database, spaceId: "upgraded" }); + expect(store.contract().sqliteSchemaVersion).toBe(2); + expect(database.query<{ count: number }, []>( + "SELECT count(*) AS count FROM oh_migrations", + ).get()?.count).toBe(2); + expect(database.query<{ count: number }, []>( + "SELECT count(*) AS count FROM sqlite_schema WHERE name IN ('oh_space_bindings', 'oh_space_purges')", + ).get()?.count).toBe(2); + store.close(); + }); +}); diff --git a/src/sqlite/migrations.ts b/src/sqlite/migrations.ts index 0538b73..a741c44 100644 --- a/src/sqlite/migrations.ts +++ b/src/sqlite/migrations.ts @@ -1,7 +1,7 @@ import { canonicalNow, sha256Hex } from "../canonical"; import type { OhSqliteDatabase } from "./driver"; -export const OH_SQLITE_SCHEMA_VERSION = 1 as const; +export const OH_SQLITE_SCHEMA_VERSION = 2 as const; export type OhSqliteMigration = Readonly<{ name: string; sql: string; version: number }>; @@ -113,6 +113,32 @@ CREATE VIRTUAL TABLE oh_search_fts USING fts5( CREATE INDEX oh_operations_space_sequence ON oh_operations(space_id, sequence); CREATE INDEX oh_records_space_kind ON oh_records(space_id, kind, record_key); CREATE INDEX oh_dependencies_dependency ON oh_dependencies(space_id, dependency_key); +`, + }), + Object.freeze({ + name: "0002_store_realms", + version: 2, + sql: ` +CREATE TABLE oh_space_bindings ( + space_id TEXT PRIMARY KEY REFERENCES oh_spaces(space_id), + realm_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_kind TEXT NOT NULL CHECK(profile_kind IN ('canonical', 'working')), + profile_sha256 TEXT NOT NULL CHECK(length(profile_sha256) = 64), + binding_sha256 TEXT NOT NULL UNIQUE CHECK(length(binding_sha256) = 64), + binding_json TEXT NOT NULL CHECK(json_valid(binding_json)), + created_at TEXT NOT NULL +) STRICT; + +CREATE TABLE oh_space_purges ( + space_id TEXT PRIMARY KEY, + binding_sha256 TEXT NOT NULL CHECK(length(binding_sha256) = 64), + prior_operation_sha256 TEXT CHECK(prior_operation_sha256 IS NULL OR length(prior_operation_sha256) = 64), + prior_sequence INTEGER NOT NULL CHECK(prior_sequence >= 0), + purged_at TEXT NOT NULL, + receipt_sha256 TEXT NOT NULL UNIQUE CHECK(length(receipt_sha256) = 64), + receipt_json TEXT NOT NULL CHECK(json_valid(receipt_json)) +) STRICT; `, }), ]); diff --git a/src/sqlite/port.test.ts b/src/sqlite/port.test.ts new file mode 100644 index 0000000..f06c1d7 --- /dev/null +++ b/src/sqlite/port.test.ts @@ -0,0 +1,127 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { createKnowledgeGraphRecordV1 } from "../graph"; +import { + createOhStoreBindingV1, + OH_CANONICAL_STORE_PROFILE_V1, + OH_WORKING_STORE_PROFILE_V1, + OhProfileError, + OhPurgedSpaceError, +} from "../store"; +import { createOhSqliteStoreAuthorityV1 } from "./port"; +import { OhSqliteStore } from "./store"; + +const roots: string[] = []; +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); +}); + +async function databasePath(): Promise { + const root = await mkdtemp(join(tmpdir(), "oh-port-test-")); + roots.push(root); + return join(root, "oh.sqlite"); +} + +const entity = (key: string, name: string, dependencies: readonly string[] = []) => + createKnowledgeGraphRecordV1({ dependencies, key, kind: "entity", v: 1, value: { name } }); + +describe("promise-based SQLite store port", () => { + test("reads exact historical heads and paginates a feed through a pinned head", async () => { + const authority = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:history", spaceId: "history" }); + const first = await authority.store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: entity("entity:a", "A"), v: 1 }], expectedHead: await authority.store.head(), + instant: "2026-08-29T12:00:00.000Z", operationId: "op_a" }); + await authority.store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: entity("entity:b", "B"), v: 1 }], expectedHead: await authority.store.head(), + instant: "2026-08-29T12:01:00.000Z", operationId: "op_b" }); + const third = await authority.store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: entity("entity:c", "C"), v: 1 }], expectedHead: await authority.store.head(), + instant: "2026-08-29T12:02:00.000Z", operationId: "op_c" }); + + const firstSnapshot = await authority.store.snapshot({ head: { + operationSha256: first.operationSha256, sequence: first.sequence } }); + expect(firstSnapshot.records.map(({ key }) => key)).toEqual(["entity:a"]); + const page = await authority.store.changesSince({ operationSha256: null, sequence: 0 }, { + limit: 2, through: { operationSha256: third.operationSha256, sequence: third.sequence }, + }); + expect(page.operations.map(({ operationId }) => operationId)).toEqual(["op_a", "op_b"]); + expect(page.hasMore).toBe(true); + const final = await authority.store.changesSince(page.to, { limit: 2, + through: { operationSha256: third.operationSha256, sequence: third.sequence } }); + expect(final.operations.map(({ operationId }) => operationId)).toEqual(["op_c"]); + expect(final.hasMore).toBe(false); + await authority.store.close(); + }); + + test("exports a verified closure from the exact requested head", async () => { + const authority = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:closure", spaceId: "closure" }); + const parent = entity("entity:parent", "Parent"); + const child = entity("entity:child", "Child", [parent.key]); + await authority.store.commit({ actorId: "agent.test", changes: [ + { kind: "put", record: child, v: 1 }, { kind: "put", record: parent, v: 1 }, + ], expectedHead: await authority.store.head(), operationId: "op_closure" }); + const closure = await authority.store.exportDependencyClosure({ roots: [child.key] }); + expect(closure.records.map(({ key }) => key)).toEqual([child.key, parent.key].sort()); + expect(closure.binding.bindingSha256).toBe(authority.store.binding.bindingSha256); + await authority.store.close(); + }); + + test("persists an exact realm binding and rejects a different profile on reopen", async () => { + const path = await databasePath(); + const canonical = createOhSqliteStoreAuthorityV1({ path, profile: OH_CANONICAL_STORE_PROFILE_V1, + realmId: "realm:canonical", spaceId: "same" }); + await canonical.store.close(); + expect(() => createOhSqliteStoreAuthorityV1({ path, profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:working", spaceId: "same" })).toThrow(OhProfileError); + }); + + test("keeps purge on host control, deletes all working payload rows, and leaves a receipt", async () => { + const path = await databasePath(); + const authority = createOhSqliteStoreAuthorityV1({ path, profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:purge", spaceId: "purge" }); + expect("purgeWorkingSpace" in authority.store).toBe(false); + await authority.store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: entity("entity:private", "Private"), v: 1 }], expectedHead: await authority.store.head(), + operationId: "op_private" }); + const receipt = await authority.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" }); + expect(receipt.priorHead.sequence).toBe(1); + expect(await authority.host.purgeWorkingSpace({ purgedAt: "2026-08-29T13:00:00.000Z" })).toEqual(receipt); + + const database = new OhSqliteStore({ path, spaceId: "other" }); + for (const table of ["oh_operations", "oh_operation_records", "oh_records", "oh_dependencies", + "oh_search_documents", "oh_sync_outbox", "oh_sync_state", "oh_space_bindings", "oh_spaces"]) { + const where = table === "oh_operation_records" + ? "operation_sha256 IN (SELECT operation_sha256 FROM oh_operations WHERE space_id = 'purge')" + : "space_id = 'purge'"; + const count = database.database.query<{ count: number }, []>(`SELECT count(*) AS count FROM ${table} WHERE ${where}`).get()?.count; + expect(count, table).toBe(0); + } + expect(database.database.query<{ count: number }, []>( + "SELECT count(*) AS count FROM oh_space_purges WHERE space_id = 'purge'", + ).get()?.count).toBe(1); + database.close(); + expect(() => new OhSqliteStore({ path, spaceId: "purge" })).toThrow(OhPurgedSpaceError); + }); + + test("never gives canonical profiles a destructive host capability", async () => { + const authority = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_CANONICAL_STORE_PROFILE_V1, realmId: "realm:canonical", spaceId: "canonical" }); + await expect(authority.host.purgeWorkingSpace({})).rejects.toThrow(OhProfileError); + await authority.store.close(); + }); + + test("refuses operation replication for a bound working profile", () => { + const store = new OhSqliteStore({ path: ":memory:", spaceId: "local-only" }); + store.bind(createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:local-only", spaceId: "local-only", v: 1 })); + expect(() => store.exportOperations()).toThrow(OhProfileError); + expect(() => store.importOperation({})).toThrow(OhProfileError); + expect(store.verifyReplay()).toMatchObject({ operations: 0, records: 0 }); + store.close(); + }); +}); diff --git a/src/sqlite/port.ts b/src/sqlite/port.ts new file mode 100644 index 0000000..69a319f --- /dev/null +++ b/src/sqlite/port.ts @@ -0,0 +1,120 @@ +import { canonicalJson } from "../canonical"; +import { + createOhStoreBindingV1, + OH_CANONICAL_STORE_PROFILE_V1, + OhProfileError, + parseOhStoreProfileV1, + type OhChangesPageV1, + type OhCommitInputV1, + type OhDependencyClosureV1, + type OhHeadRefV1, + type OhHeadV1, + type OhSnapshotV1, + type OhSpacePurgeReceiptV1, + type OhStoreAuthorityV1, + type OhStoreBindingV1, + type OhStoreHostControlV1, + type OhStoreProfileV1, + type OhStoreV1, + type OhStoreVerificationV1, +} from "../store"; +import type { OhOperationV1 } from "../operation"; +import type { OhSqliteDatabase } from "./driver"; +import { OhSqliteStore } from "./store"; + +export type OhSqliteStoreAuthorityOptionsV1 = Readonly<{ + database?: OhSqliteDatabase; + path?: string; + profile?: OhStoreProfileV1; + realmId?: string; + spaceId?: string; +}>; + +export class OhSqliteStorePortV1 implements OhStoreV1 { + readonly binding: OhStoreBindingV1; + readonly #authority: OhSqliteStore; + + constructor(authority: OhSqliteStore, binding: OhStoreBindingV1) { + const persisted = authority.bind(binding); + if (canonicalJson(persisted) !== canonicalJson(binding)) { + throw new OhProfileError("The SQLite authority returned a different store binding."); + } + this.#authority = authority; + this.binding = persisted; + } + + async head(): Promise { + return this.#authority.head(); + } + + async snapshot(options: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }> = {}): Promise { + return this.#authority.snapshotAtHead(options); + } + + async changesSince( + from: OhHeadRefV1, + options: Readonly<{ limit?: number; through?: OhHeadRefV1 }> = {}, + ): Promise { + return this.#authority.changesSince(from, options); + } + + async commit(input: OhCommitInputV1): Promise { + return this.#authority.commit(input); + } + + async exportDependencyClosure(input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): Promise { + return this.#authority.exportDependencyClosure({ binding: this.binding, ...input }); + } + + async verify(): Promise { + const verified = this.#authority.verifyReplay(); + return { head: verified.head, integrity: "verified", operations: verified.operations, + records: verified.records, v: 1 }; + } + + async close(): Promise { + this.#authority.close(); + } +} + +/** + * Binds a Bun SQLite authority to the promise-based store port. Retain the + * returned `host` object in trusted control-plane code; pass only `store` to + * ordinary consumers. + */ +export function createOhSqliteStoreAuthorityV1( + options: OhSqliteStoreAuthorityOptionsV1 = {}, +): OhStoreAuthorityV1 { + const profile = parseOhStoreProfileV1(options.profile ?? OH_CANONICAL_STORE_PROFILE_V1); + if (profile === null) throw new TypeError("Invalid SQLite store profile."); + const spaceId = options.spaceId ?? "default"; + const binding = createOhStoreBindingV1({ profile, + realmId: options.realmId ?? `realm:${spaceId}`, spaceId, v: 1 }); + const authority = new OhSqliteStore({ + ...(options.database === undefined ? {} : { database: options.database }), + ...(options.path === undefined ? {} : { path: options.path }), + spaceId, + }); + const store = new OhSqliteStorePortV1(authority, binding); + let purge: OhSpacePurgeReceiptV1 | null = null; + const host: OhStoreHostControlV1 = Object.freeze({ + binding, + purgeWorkingSpace: async (input: Readonly<{ purgedAt?: string }>) => { + if (profile.profileKind !== "working" || !profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("This host handle is not bound to a purgeable working profile."); + } + if (purge !== null) return purge; + purge = authority.purgeWorkingSpace(binding, input.purgedAt); + authority.close(); + return purge; + }, + }); + return Object.freeze({ host, store }); +} diff --git a/src/sqlite/store.test.ts b/src/sqlite/store.test.ts index e874c61..431dcb2 100644 --- a/src/sqlite/store.test.ts +++ b/src/sqlite/store.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { createKnowledgeGraphRecordV1 } from "../graph"; +import { createOhStoreBindingV1, OH_WORKING_STORE_PROFILE_V1 } from "../store"; import { OhConflictError, OhDependencyError, OhIntegrityError, OhSqliteStore } from "./store"; const roots: string[] = []; @@ -63,6 +64,67 @@ describe("Oh SQLite authority", () => { store.close(); }); + test("rejects orphaned and cross-space rows as idempotent operations", () => { + const changes = [{ kind: "put" as const, record: record("entity:orphan", "Orphan"), v: 1 as const }]; + const source = new OhSqliteStore({ path: ":memory:", spaceId: "orphan-target" }); + const operation = source.commit({ actorId: "agent.test", changes, expectedHead: source.head(), + operationId: "op_orphan" }); + const target = new OhSqliteStore({ path: ":memory:", spaceId: "orphan-target" }); + target.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, + operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, + operation_json, instant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + operation.operationSha256, operation.spaceId, operation.sequence, operation.operationId, + operation.parentOperationSha256, operation.graphRevisionSha256, operation.recordsSha256, + JSON.stringify(operation), operation.instant, + ); + expect(() => target.commit({ actorId: operation.actorId, changes, expectedHead: target.head(), + operationId: operation.operationId })).toThrow(OhIntegrityError); + expect(() => target.importOperation(operation)).toThrow(OhIntegrityError); + expect(target.head().sequence).toBe(0); + source.close(); target.close(); + + const alienSource = new OhSqliteStore({ path: ":memory:", spaceId: "alien-source" }); + const alien = alienSource.commit({ actorId: "agent.test", changes, expectedHead: alienSource.head(), + operationId: "op_alien" }); + const alienTarget = new OhSqliteStore({ path: ":memory:", spaceId: "alien-target" }); + alienTarget.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, + operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, + operation_json, instant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + alien.operationSha256, alienTarget.spaceId, alien.sequence, alien.operationId, + alien.parentOperationSha256, alien.graphRevisionSha256, alien.recordsSha256, + JSON.stringify(alien), alien.instant, + ); + alienTarget.database.query(`UPDATE oh_spaces SET generation = 1, head_operation_sha256 = ?, + graph_revision_sha256 = ?, records_sha256 = ?, sequence = 1 WHERE space_id = ?`).run( + alien.operationSha256, alien.graphRevisionSha256, alien.recordsSha256, alienTarget.spaceId, + ); + expect(() => alienTarget.commit({ actorId: alien.actorId, changes, + expectedHead: alienTarget.head(), operationId: alien.operationId })).toThrow(OhIntegrityError); + alienSource.close(); alienTarget.close(); + }); + + test("refuses to advance a space whose duplicated binding or current head drifted", () => { + const store = new OhSqliteStore({ path: ":memory:", spaceId: "authority-drift" }); + const binding = createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:authority-drift", spaceId: store.spaceId, v: 1 }); + store.bind(binding); + store.database.query("UPDATE oh_space_bindings SET realm_id = ? WHERE space_id = ?") + .run("realm:alien", store.spaceId); + expect(() => store.binding()).toThrow(OhIntegrityError); + store.database.query("UPDATE oh_space_bindings SET realm_id = ? WHERE space_id = ?") + .run(binding.realmId, store.spaceId); + store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record("entity:one", "One"), v: 1 }], expectedHead: store.head(), operationId: "op_one" }); + store.database.query("UPDATE oh_spaces SET graph_revision_sha256 = ? WHERE space_id = ?") + .run("f".repeat(64), store.spaceId); + const drifted = store.head(); + expect(() => store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record("entity:two", "Two"), v: 1 }], expectedHead: drifted, + operationId: "op_two" })).toThrow(OhIntegrityError); + expect(store.head().sequence).toBe(1); + store.close(); + }); + test("checks final dependency closure before changing durable state", () => { const store = new OhSqliteStore({ path: ":memory:" }); expect(() => store.commit({ actorId: "agent.test", changes: [{ kind: "put", @@ -119,6 +181,16 @@ describe("Oh SQLite authority", () => { store.close(); }); + test("verifies duplicated operation columns against canonical envelopes", () => { + const store = new OhSqliteStore({ path: ":memory:" }); + store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record("entity:a", "A"), v: 1 }], expectedHead: store.head(), operationId: "op_columns" }); + store.database.query("UPDATE oh_operations SET operation_id = ? WHERE space_id = ?") + .run("op_alien", store.spaceId); + expect(() => store.verifyReplay()).toThrow(OhIntegrityError); + store.close(); + }); + test("checks canonical operation bytes beyond the export batch ceiling", () => { const store = new OhSqliteStore({ path: ":memory:" }); for (let index = 0; index < 1001; index += 1) { @@ -128,7 +200,69 @@ describe("Oh SQLite authority", () => { } store.database.query(`UPDATE oh_operations SET operation_json = ' ' || operation_json WHERE space_id = ? AND sequence = 1`).run(store.spaceId); - expect(() => store.verifyReplay()).toThrow("not canonical JSON"); + expect(() => store.verifyReplay()).toThrow(OhIntegrityError); + store.close(); + }); + + test("never reports a feed page that omits its tail or hides a bad sentinel", () => { + const tail = new OhSqliteStore({ path: ":memory:", spaceId: "feed-tail" }); + for (let index = 1; index <= 3; index += 1) { + tail.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record(`entity:tail-${index}`, `Tail ${index}`), v: 1 }], expectedHead: tail.head(), + operationId: `op_tail_${index}` }); + } + tail.database.exec("PRAGMA foreign_keys = OFF"); + tail.database.query("DELETE FROM oh_operations WHERE space_id = ? AND sequence = 3").run(tail.spaceId); + tail.database.exec("PRAGMA foreign_keys = ON"); + expect(() => tail.changesSince({ operationSha256: null, sequence: 0 }, { limit: 3 })) + .toThrow(OhIntegrityError); + tail.close(); + + const sentinel = new OhSqliteStore({ path: ":memory:", spaceId: "feed-sentinel" }); + for (let index = 1; index <= 3; index += 1) { + sentinel.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record(`entity:sentinel-${index}`, `Sentinel ${index}`), v: 1 }], + expectedHead: sentinel.head(), operationId: `op_sentinel_${index}` }); + } + sentinel.database.exec("PRAGMA foreign_keys = OFF"); + sentinel.database.query("DELETE FROM oh_operations WHERE space_id = ? AND sequence = 2") + .run(sentinel.spaceId); + sentinel.database.exec("PRAGMA foreign_keys = ON"); + expect(() => sentinel.changesSince({ operationSha256: null, sequence: 0 }, { limit: 1 })) + .toThrow(OhIntegrityError); + sentinel.close(); + }); + + test("rolls a working-space purge back when a payload delete is masked", () => { + const store = new OhSqliteStore({ path: ":memory:", spaceId: "purge-postcondition" }); + const binding = createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:purge-postcondition", spaceId: store.spaceId, v: 1 }); + store.bind(binding); + store.commit({ actorId: "agent.test", changes: [{ kind: "put", + record: record("entity:private", "Private"), v: 1 }], expectedHead: store.head(), + operationId: "op_private" }); + store.database.exec(`CREATE TRIGGER mask_private_operation_delete BEFORE DELETE ON oh_operations + BEGIN SELECT RAISE(IGNORE); END`); + store.database.exec(`CREATE TRIGGER mask_private_space_delete BEFORE DELETE ON oh_spaces + BEGIN SELECT RAISE(IGNORE); END`); + expect(() => store.purgeWorkingSpace(binding, "2026-08-29T13:00:00.000Z")) + .toThrow(OhIntegrityError); + expect(store.database.query<{ count: number }, []>( + "SELECT count(*) AS count FROM oh_space_purges").get()?.count).toBe(0); + expect(store.get("entity:private")).not.toBeNull(); + expect(store.head().sequence).toBe(1); + store.close(); + }); + + test("cross-checks every stored purge receipt column before refusing resurrection", () => { + const store = new OhSqliteStore({ path: ":memory:", spaceId: "purge-columns" }); + const binding = createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:purge-columns", spaceId: store.spaceId, v: 1 }); + store.bind(binding); + store.purgeWorkingSpace(binding, "2026-08-29T13:00:00.000Z"); + store.database.query("UPDATE oh_space_purges SET prior_sequence = 99 WHERE space_id = ?") + .run(store.spaceId); + expect(() => store.ensureSpace()).toThrow(OhIntegrityError); store.close(); }); diff --git a/src/sqlite/store.ts b/src/sqlite/store.ts index 5f9dbbc..50fc572 100644 --- a/src/sqlite/store.ts +++ b/src/sqlite/store.ts @@ -28,37 +28,42 @@ import { parseOhOperationV1, type OhOperationV1, } from "../operation"; -import { openOhSqliteDatabase, withImmediateTransaction, type OhSqliteDatabase } from "./driver"; +import { + createOhDependencyClosureV1, + createOhSpacePurgeReceiptV1, + emptyOhHeadV1, + OhConflictError, + OhDependencyError, + OhIntegrityError, + OhProfileError, + OhPurgedSpaceError, + parseOhHeadRefV1, + parseOhSpacePurgeReceiptV1, + parseOhStoreBindingV1, + replayOhOperationsV1, + type OhChangesPageV1, + type OhCommitInputV1, + type OhDependencyClosureV1, + type OhHeadRefV1, + type OhHeadV1, + type OhSnapshotV1, + type OhSpacePurgeReceiptV1, + type OhStoreBindingV1, +} from "../store"; +import { openOhSqliteDatabase, withImmediateTransaction, withReadTransaction, + type OhSqliteDatabase } from "./driver"; import { applyOhSqliteMigrations, OH_SQLITE_SCHEMA_VERSION } from "./migrations"; const EMPTY_RECORDS_SHA256 = canonicalSha256([]); -export class OhConflictError extends Error { - constructor(message: string) { super(message); this.name = "OhConflictError"; } -} -export class OhIntegrityError extends Error { - constructor(message: string) { super(message); this.name = "OhIntegrityError"; } -} -export class OhDependencyError extends Error { - constructor(message: string) { super(message); this.name = "OhDependencyError"; } -} - -export type OhHeadV1 = Readonly<{ - generation: number; - graphRevisionSha256: Sha256Hex | null; - operationSha256: Sha256Hex | null; - recordsSha256: Sha256Hex; - sequence: number; - v: 1; -}>; - -export type OhCommitInputV1 = Readonly<{ - actorId: string; - changes: readonly KnowledgeGraphChangeV1[]; - expectedHead: Pick; - instant?: string; - operationId: string; -}>; +export { + OhConflictError, + OhDependencyError, + OhIntegrityError, + OhProfileError, + OhPurgedSpaceError, +}; +export type { OhCommitInputV1, OhHeadV1 }; export type OhRecordListOptions = Readonly<{ kind?: KnowledgeGraphRecordKindV1; @@ -98,7 +103,100 @@ type CurrentRecordRow = { record_sha256: string; sequence: number; }; -type OperationRow = { operation_json: string }; +type OperationRow = { + graph_revision_sha256: string; + instant: string; + operation_id: string; + operation_json: string; + operation_sha256: string; + parent_operation_sha256: string | null; + records_sha256: string; + sequence: number; + space_id: string; +}; +type BindingRow = { + binding_json: string; + binding_sha256: string; + profile_id: string; + profile_kind: string; + profile_sha256: string; + realm_id: string; + space_id: string; +}; +type PurgeRow = { + binding_sha256: string; + prior_operation_sha256: string | null; + prior_sequence: number; + purged_at: string; + receipt_json: string; + receipt_sha256: string; + space_id: string; +}; + +const OPERATION_COLUMNS = `operation_sha256, space_id, sequence, operation_id, + parent_operation_sha256, graph_revision_sha256, records_sha256, operation_json, instant`; +const BINDING_COLUMNS = `space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json`; +const PURGE_COLUMNS = `space_id, binding_sha256, prior_operation_sha256, prior_sequence, + purged_at, receipt_sha256, receipt_json`; + +function parseStoredOperationRow( + row: OperationRow, + expected: Readonly<{ operationId?: string; operationSha256?: string; spaceId?: string }> = {}, +): OhOperationV1 { + let value: unknown; + try { value = JSON.parse(row.operation_json); } catch { throw new OhIntegrityError("A stored operation is not JSON."); } + const operation = parseOhOperationV1(value); + if (operation === null || canonicalJson(operation) !== row.operation_json + || row.operation_sha256 !== operation.operationSha256 + || row.space_id !== operation.spaceId + || row.sequence !== operation.sequence + || row.operation_id !== operation.operationId + || row.parent_operation_sha256 !== operation.parentOperationSha256 + || row.graph_revision_sha256 !== operation.graphRevisionSha256 + || row.records_sha256 !== operation.recordsSha256 + || row.instant !== operation.instant + || (expected.spaceId !== undefined && operation.spaceId !== expected.spaceId) + || (expected.operationId !== undefined && operation.operationId !== expected.operationId) + || (expected.operationSha256 !== undefined && operation.operationSha256 !== expected.operationSha256)) { + throw new OhIntegrityError("Stored operation columns do not match their canonical envelope."); + } + return operation; +} + +function parseStoredBindingRow(row: BindingRow, expectedSpaceId: string): OhStoreBindingV1 { + let value: unknown; + try { value = JSON.parse(row.binding_json); } catch { throw new OhIntegrityError("A store binding is not JSON."); } + const binding = parseOhStoreBindingV1(value); + if (binding === null || canonicalJson(binding) !== row.binding_json + || binding.spaceId !== expectedSpaceId + || row.space_id !== binding.spaceId + || row.realm_id !== binding.realmId + || row.profile_id !== binding.profile.profileId + || row.profile_kind !== binding.profile.profileKind + || row.profile_sha256 !== binding.profile.profileSha256 + || row.binding_sha256 !== binding.bindingSha256) { + throw new OhIntegrityError("Stored binding columns do not match their canonical envelope."); + } + return binding; +} + +function parseStoredPurgeRow(row: PurgeRow, expectedSpaceId: string): OhSpacePurgeReceiptV1 { + let value: unknown; + try { value = JSON.parse(row.receipt_json); } catch { throw new OhIntegrityError("A purge receipt is not JSON."); } + const receipt = parseOhSpacePurgeReceiptV1(value); + if (receipt === null || canonicalJson(receipt) !== row.receipt_json + || receipt.spaceId !== expectedSpaceId + || row.space_id !== receipt.spaceId + || row.binding_sha256 !== receipt.bindingSha256 + || row.prior_operation_sha256 !== receipt.priorHead.operationSha256 + || row.prior_sequence !== receipt.priorHead.sequence + || row.purged_at !== receipt.purgedAt + || row.receipt_sha256 !== receipt.receiptSha256) { + throw new OhIntegrityError("Stored purge columns do not match their canonical receipt."); + } + return receipt; +} function parseHead(row: SpaceRow): OhHeadV1 { const operationSha256 = row.head_operation_sha256 === null ? null : parseSha256Hex(row.head_operation_sha256); @@ -175,6 +273,13 @@ export class OhSqliteStore { if (this.#closed) throw new Error("The Oh store is closed."); } + #assertOperationReplication(): void { + const binding = this.binding(); + if (binding !== null && !binding.profile.capabilities.operationReplication) { + throw new OhProfileError("This bound store profile forbids operation replication."); + } + } + #registerContract(): void { const manifestJson = canonicalJson(OH_CONTRACT_MANIFEST_V1); this.database.query(`INSERT INTO oh_contracts(contract_id, contract_sha256, manifest_json, created_at) @@ -191,13 +296,55 @@ export class OhSqliteStore { ensureSpace(): OhHeadV1 { this.#assertOpen(); - const now = canonicalNow(); - this.database.query(`INSERT INTO oh_spaces( - space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, - records_sha256, sequence, created_at, updated_at - ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`) - .run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA256, now, now); - return this.head(); + return withImmediateTransaction(this.database, () => { + const purged = this.database.query( + `SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`, + ).get(this.spaceId); + if (purged !== null) { + throw new OhPurgedSpaceError(parseStoredPurgeRow(purged, this.spaceId)); + } + const now = canonicalNow(); + this.database.query(`INSERT INTO oh_spaces( + space_id, contract_id, generation, head_operation_sha256, graph_revision_sha256, + records_sha256, sequence, created_at, updated_at + ) VALUES (?, ?, 0, NULL, NULL, ?, 0, ?, ?) ON CONFLICT(space_id) DO NOTHING`) + .run(this.spaceId, OH_CONTRACT_ID_V1, EMPTY_RECORDS_SHA256, now, now); + return this.head(); + }); + } + + bind(bindingValue: OhStoreBindingV1): OhStoreBindingV1 { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId) { + throw new OhProfileError("The store binding does not identify this space."); + } + const bindingJson = canonicalJson(binding); + this.database.query(`INSERT INTO oh_space_bindings( + space_id, realm_id, profile_id, profile_kind, profile_sha256, + binding_sha256, binding_json, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(space_id) DO NOTHING`).run( + this.spaceId, binding.realmId, binding.profile.profileId, binding.profile.profileKind, + binding.profile.profileSha256, binding.bindingSha256, bindingJson, canonicalNow(), + ); + const row = this.database.query( + `SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`, + ).get(this.spaceId); + if (row === null) throw new OhIntegrityError("The persisted store binding disappeared."); + const persisted = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persisted) !== bindingJson) { + throw new OhProfileError("The space is already bound to a different realm or profile."); + } + return binding; + } + + binding(): OhStoreBindingV1 | null { + this.#assertOpen(); + const row = this.database.query( + `SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`, + ).get(this.spaceId); + if (row === null) return null; + return parseStoredBindingRow(row, this.spaceId); } head(): OhHeadV1 { @@ -257,6 +404,58 @@ export class OhSqliteStore { return { graphRevisionSha256, records, recordsSha256 }; } + #assertCurrentHeadAuthority(head: OhHeadV1): void { + const summary = this.database.query<{ count: number; maximum: number | null; minimum: number | null }, [string]>( + `SELECT count(*) AS count, min(sequence) AS minimum, max(sequence) AS maximum + FROM oh_operations WHERE space_id = ?`, + ).get(this.spaceId); + if (summary === null || summary.count !== head.sequence + || (head.sequence === 0 && (summary.minimum !== null || summary.maximum !== null)) + || (head.sequence > 0 && (summary.minimum !== 1 || summary.maximum !== head.sequence))) { + throw new OhIntegrityError("The operation history does not exactly cover the current space head."); + } + if (head.sequence === 0) return; + if (head.operationSha256 === null) throw new OhIntegrityError("A nonempty space head has no operation digest."); + const row = this.database.query( + `SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`, + ).get(this.spaceId, head.sequence); + if (row === null) throw new OhIntegrityError("The current space head operation is missing."); + const operation = parseStoredOperationRow(row, { spaceId: this.spaceId, + operationSha256: head.operationSha256 }); + if (operation.sequence !== head.sequence + || operation.graphRevisionSha256 !== head.graphRevisionSha256 + || operation.recordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The current space head differs from its canonical operation."); + } + } + + #assertOperationReachable(operation: OhOperationV1, head: OhHeadV1): void { + if (operation.sequence < 1 || operation.sequence > head.sequence) { + throw new OhIntegrityError("A stored idempotent operation is not reachable from the current head."); + } + const rows = this.database.query<{ + operation_sha256: string; parent_operation_sha256: string | null; sequence: number; + }, [string, number, number]>(`SELECT operation_sha256, parent_operation_sha256, sequence + FROM oh_operations WHERE space_id = ? AND sequence >= ? AND sequence <= ? ORDER BY sequence`) + .all(this.spaceId, operation.sequence, head.sequence); + if (rows.length !== head.sequence - operation.sequence + 1) { + throw new OhIntegrityError("A stored idempotent operation has an incomplete path to the current head."); + } + let priorSha256: string | null = operation.parentOperationSha256; + for (let index = 0; index < rows.length; index += 1) { + const row = rows[index]; + if (row === undefined || row.sequence !== operation.sequence + index + || row.parent_operation_sha256 !== priorSha256 + || (index === 0 && row.operation_sha256 !== operation.operationSha256)) { + throw new OhIntegrityError("A stored idempotent operation is not on the current authority chain."); + } + priorSha256 = row.operation_sha256; + } + if (priorSha256 !== head.operationSha256) { + throw new OhIntegrityError("A stored idempotent operation does not reach the current head digest."); + } + } + #persist(operation: OhOperationV1): void { this.database.query(`INSERT INTO oh_operations(operation_sha256, space_id, sequence, operation_id, parent_operation_sha256, graph_revision_sha256, records_sha256, @@ -326,18 +525,19 @@ export class OhSqliteStore { const changes = canonicalKnowledgeGraphChangesV1(input.changes); if (changes.length === 0 || changes.length > 8192) throw new TypeError("A commit needs 1 through 8192 changes."); return withImmediateTransaction(this.database, () => { + const head = this.head(); + this.#assertCurrentHeadAuthority(head); const duplicate = this.database.query( - "SELECT operation_json FROM oh_operations WHERE space_id = ? AND operation_id = ?", + `SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND operation_id = ?`, ).get(this.spaceId, operationId); if (duplicate !== null) { - const existing = parseOhOperationV1(JSON.parse(duplicate.operation_json)); - if (existing === null) throw new OhIntegrityError("The stored idempotent operation is invalid."); + const existing = parseStoredOperationRow(duplicate, { operationId, spaceId: this.spaceId }); + this.#assertOperationReachable(existing, head); if (existing.actorId !== actorId || canonicalJson(existing.changes) !== canonicalJson(changes)) { throw new OhConflictError("The operation ID is already bound to different content."); } return existing; } - const head = this.head(); if (head.generation !== input.expectedHead.generation || head.operationSha256 !== input.expectedHead.operationSha256) { throw new OhConflictError("The expected head does not match the current space head."); @@ -354,19 +554,24 @@ export class OhSqliteStore { importOperation(value: unknown): Readonly<{ imported: boolean; operation: OhOperationV1 }> { this.#assertOpen(); + this.#assertOperationReplication(); const operation = parseOhOperationV1(value); if (operation === null || operation.spaceId !== this.spaceId) throw new OhIntegrityError("Invalid imported operation."); return withImmediateTransaction(this.database, () => { + const head = this.head(); + this.#assertCurrentHeadAuthority(head); const duplicate = this.database.query( - "SELECT operation_json FROM oh_operations WHERE operation_sha256 = ?", + `SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE operation_sha256 = ?`, ).get(operation.operationSha256); if (duplicate !== null) { - if (canonicalJson(JSON.parse(duplicate.operation_json)) !== canonicalJson(operation)) { + const existing = parseStoredOperationRow(duplicate, { operationSha256: operation.operationSha256, + spaceId: this.spaceId }); + this.#assertOperationReachable(existing, head); + if (canonicalJson(existing) !== canonicalJson(operation)) { throw new OhIntegrityError("An operation digest is bound to different bytes."); } return { imported: false, operation }; } - const head = this.head(); if (operation.sequence !== head.sequence + 1 || operation.parentOperationSha256 !== head.operationSha256) { throw new OhConflictError("The imported operation does not extend the local head."); } @@ -382,18 +587,128 @@ export class OhSqliteStore { exportOperations(afterSequence = 0, limit = 1000): readonly OhOperationV1[] { this.#assertOpen(); + this.#assertOperationReplication(); if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) throw new RangeError("afterSequence must be nonnegative."); const boundedLimit = normalizeLimit(limit); const rows = this.database.query( - "SELECT operation_json FROM oh_operations WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?", + `SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? ORDER BY sequence LIMIT ?`, ).all(this.spaceId, afterSequence, boundedLimit); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null || canonicalJson(operation) !== row.operation_json) throw new OhIntegrityError("A stored operation is invalid."); - return operation; + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + } + + #headAt(reference: OhHeadRefV1): OhHeadV1 { + const parsed = parseOhHeadRefV1(reference); + if (parsed === null) throw new TypeError("Invalid Oh head reference."); + if (parsed.sequence === 0) return emptyOhHeadV1(); + const row = this.database.query( + `SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? AND sequence = ?`, + ).get(this.spaceId, parsed.sequence); + if (row === null) throw new OhConflictError("The requested head is not present in this space."); + const operation = parseStoredOperationRow(row, { spaceId: this.spaceId }); + if (operation.spaceId !== this.spaceId || operation.sequence !== parsed.sequence + || operation.operationSha256 !== parsed.operationSha256) { + throw new OhConflictError("The requested sequence identifies a different operation head."); + } + return { generation: operation.sequence, graphRevisionSha256: operation.graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256: operation.recordsSha256, + sequence: operation.sequence, v: 1 }; + } + + snapshotAtHead(options: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }> = {}): OhSnapshotV1 { + this.#assertOpen(); + const maximumRecords = options.maximumRecords ?? OH_GRAPH_LIMITS_V1.recordsPerSnapshot; + if (!Number.isSafeInteger(maximumRecords) || maximumRecords < 1 + || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError(`maximumRecords must be an integer from 1 through ${OH_GRAPH_LIMITS_V1.recordsPerSnapshot}.`); + } + return withReadTransaction(this.database, () => { + const current = this.head(); + const target = options.head === undefined ? current : this.#headAt(options.head); + if (target.sequence > current.sequence) throw new OhConflictError("The requested head is ahead of this space."); + const rows = this.database.query( + `SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence <= ? ORDER BY sequence`, + ).all(this.spaceId, target.sequence); + const operations = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + const snapshot = replayOhOperationsV1(this.spaceId, operations, maximumRecords); + if (snapshot.head.operationSha256 !== target.operationSha256 + || snapshot.head.recordsSha256 !== target.recordsSha256) { + throw new OhIntegrityError("Operation replay does not reproduce the requested head."); + } + return snapshot; }); } + changesSince( + fromValue: OhHeadRefV1, + options: Readonly<{ limit?: number; through?: OhHeadRefV1 }> = {}, + ): OhChangesPageV1 { + this.#assertOpen(); + const from = parseOhHeadRefV1(fromValue); + if (from === null) throw new TypeError("Invalid change-feed cursor."); + const limit = normalizeLimit(options.limit, 100, 1000); + return withReadTransaction(this.database, () => { + const current = this.head(); + const fromHead = this.#headAt(from); + const through = options.through === undefined ? current : this.#headAt(options.through); + if (fromHead.sequence > through.sequence || through.sequence > current.sequence) { + throw new OhConflictError("The change-feed bounds do not identify one local history prefix."); + } + const rows = this.database.query( + `SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? AND sequence > ? AND sequence <= ? + ORDER BY sequence LIMIT ?`, + ).all(this.spaceId, fromHead.sequence, through.sequence, limit + 1); + const parsed = rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (parsed.length > limit + 1) throw new OhIntegrityError("The change feed exceeded its requested page bound."); + const hasMore = parsed.length > limit; + const operations = parsed.slice(0, limit); + let prior: OhHeadRefV1 = fromHead; + for (const operation of parsed) { + if (operation.sequence !== prior.sequence + 1 + || operation.parentOperationSha256 !== prior.operationSha256) { + throw new OhIntegrityError("The change feed contains a gap or fork."); + } + prior = { operationSha256: operation.operationSha256, sequence: operation.sequence }; + } + if (!hasMore && (prior.sequence !== through.sequence + || prior.operationSha256 !== through.operationSha256)) { + throw new OhIntegrityError("The change feed does not reach its pinned through head."); + } + const last = operations.at(-1); + const to = last === undefined + ? { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence } + : { operationSha256: last.operationSha256, sequence: last.sequence }; + return { from: { operationSha256: fromHead.operationSha256, sequence: fromHead.sequence }, + hasMore, operations, through, to, v: 1 }; + }); + } + + exportDependencyClosure(input: Readonly<{ + binding: OhStoreBindingV1; + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): OhDependencyClosureV1 { + const binding = parseOhStoreBindingV1(input.binding); + if (binding === null || binding.spaceId !== this.spaceId + || canonicalJson(this.binding()) !== canonicalJson(binding)) { + throw new OhProfileError("Dependency closure export requires the exact persisted store binding."); + } + if (!binding.profile.capabilities.dependencyClosureExport) { + throw new OhProfileError("This store profile does not permit dependency-closure export."); + } + const snapshot = this.snapshotAtHead({ ...(input.head === undefined ? {} : { head: input.head }), + ...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }) }); + return createOhDependencyClosureV1({ binding, + ...(input.maximumRecords === undefined ? {} : { maximumRecords: input.maximumRecords }), + roots: input.roots, snapshot }); + } + get(key: string): KnowledgeGraphRecordV1 | null { this.#assertOpen(); const parsedKey = safeCode(key, 512); @@ -449,13 +764,10 @@ export class OhSqliteStore { log(limit = 50): readonly OhOperationV1[] { this.#assertOpen(); const rows = this.database.query( - "SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence DESC LIMIT ?", + `SELECT ${OPERATION_COLUMNS} FROM oh_operations + WHERE space_id = ? ORDER BY sequence DESC LIMIT ?`, ).all(this.spaceId, normalizeLimit(limit)); - return rows.map((row) => { - const operation = parseOhOperationV1(JSON.parse(row.operation_json)); - if (operation === null) throw new OhIntegrityError("The stored operation is invalid."); - return operation; - }); + return rows.map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); } searchKeyword(query: string, limit = 20): readonly OhKeywordSearchResultV1[] { @@ -510,29 +822,24 @@ export class OhSqliteStore { const storedCount = this.database.query<{ count: number }, [string]>( "SELECT count(*) AS count FROM oh_operations WHERE space_id = ?", ).get(this.spaceId)?.count ?? 0; - const operations: readonly OhOperationV1[] = storedCount <= 1000 - ? this.exportOperations(0, 1000) - : this.database.query( - "SELECT operation_json FROM oh_operations WHERE space_id = ? ORDER BY sequence", - ).all(this.spaceId).map((row) => { - let value: unknown; - try { value = JSON.parse(row.operation_json); } catch { throw new OhIntegrityError("A stored operation is not JSON."); } - if (canonicalJson(value) !== row.operation_json) throw new OhIntegrityError("A stored operation is not canonical JSON."); - const parsed = parseOhOperationV1(value); - if (parsed === null) throw new OhIntegrityError("A stored operation is invalid."); - return parsed; - }); + const operations: readonly OhOperationV1[] = this.database.query( + `SELECT ${OPERATION_COLUMNS} FROM oh_operations WHERE space_id = ? ORDER BY sequence`, + ).all(this.spaceId).map((row) => parseStoredOperationRow(row, { spaceId: this.spaceId })); + if (operations.length !== storedCount) throw new OhIntegrityError("Operation count changed during verification."); return this.#verifyOperations(operations); } #verifyOperations(operations: readonly OhOperationV1[]): OhReplayVerificationV1 { const records = new Map(); const materializedBy = new Map>(); + const operationIds = new Set(); let head: OhHeadV1 = { generation: 0, graphRevisionSha256: null, operationSha256: null, recordsSha256: EMPTY_RECORDS_SHA256, sequence: 0, v: 1 }; for (const operation of operations) { if (operation.spaceId !== this.spaceId || operation.sequence !== head.sequence + 1 - || operation.parentOperationSha256 !== head.operationSha256) throw new OhIntegrityError("Operation replay chain is broken."); + || operation.parentOperationSha256 !== head.operationSha256 + || operationIds.has(operation.operationId)) throw new OhIntegrityError("Operation replay chain is broken."); + operationIds.add(operation.operationId); for (const change of operation.changes) { if (change.kind === "put") { records.set(change.record.key, change.record); @@ -607,6 +914,72 @@ export class OhSqliteStore { return { manifest: OH_CONTRACT_MANIFEST_V1, sqliteSchemaVersion: OH_SQLITE_SCHEMA_VERSION }; } + /** Host control-plane primitive. Do not expose this method through agent tools. */ + purgeWorkingSpace(bindingValue: OhStoreBindingV1, purgedAt: string = canonicalNow()): OhSpacePurgeReceiptV1 { + this.#assertOpen(); + const binding = parseOhStoreBindingV1(bindingValue); + if (binding === null || binding.spaceId !== this.spaceId + || binding.profile.profileKind !== "working" + || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Whole-space purge requires a bound working profile."); + } + return withImmediateTransaction(this.database, () => { + const row = this.database.query( + `SELECT ${BINDING_COLUMNS} FROM oh_space_bindings WHERE space_id = ?`, + ).get(this.spaceId); + if (row === null) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const persistedBinding = parseStoredBindingRow(row, this.spaceId); + if (canonicalJson(persistedBinding) !== canonicalJson(binding)) { + throw new OhProfileError("Whole-space purge requires the exact persisted store binding."); + } + const receipt = createOhSpacePurgeReceiptV1({ binding, priorHead: this.head(), purgedAt }); + this.database.query(`INSERT INTO oh_space_purges(space_id, binding_sha256, + prior_operation_sha256, prior_sequence, purged_at, receipt_sha256, receipt_json) + VALUES (?, ?, ?, ?, ?, ?, ?)`).run(this.spaceId, binding.bindingSha256, + receipt.priorHead.operationSha256, receipt.priorHead.sequence, receipt.purgedAt, + receipt.receiptSha256, canonicalJson(receipt)); + this.database.query("DELETE FROM oh_search_fts WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_search_documents WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_dependencies WHERE space_id = ?").run(this.spaceId); + this.database.query(`DELETE FROM oh_operation_records WHERE operation_sha256 IN + (SELECT operation_sha256 FROM oh_operations WHERE space_id = ?)`).run(this.spaceId); + this.database.query("DELETE FROM oh_sync_outbox WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_sync_state WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_records WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_operations WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_space_bindings WHERE space_id = ?").run(this.spaceId); + this.database.query("DELETE FROM oh_spaces WHERE space_id = ?").run(this.spaceId); + const directTables = ["oh_spaces", "oh_space_bindings", "oh_operations", "oh_records", + "oh_dependencies", "oh_search_documents", "oh_sync_outbox", "oh_sync_state"] as const; + for (const table of directTables) { + const count = this.database.query<{ count: number }, [string]>( + `SELECT count(*) AS count FROM ${table} WHERE space_id = ?`, + ).get(this.spaceId)?.count; + if (count !== 0) throw new OhIntegrityError(`Space purge left rows in ${table}.`); + } + const operationRecords = this.database.query<{ count: number }, [string]>(`SELECT count(*) AS count + FROM oh_operation_records AS materialized JOIN oh_operations AS operation + ON operation.operation_sha256 = materialized.operation_sha256 + WHERE operation.space_id = ?`).get(this.spaceId)?.count; + const searchRows = this.database.query<{ count: number }, [string]>( + "SELECT count(*) AS count FROM oh_search_fts WHERE space_id = ?", + ).get(this.spaceId)?.count; + if (operationRecords !== 0 || searchRows !== 0) { + throw new OhIntegrityError("Space purge left derived private payload rows."); + } + const receiptRow = this.database.query( + `SELECT ${PURGE_COLUMNS} FROM oh_space_purges WHERE space_id = ?`, + ).get(this.spaceId); + if (receiptRow === null + || canonicalJson(parseStoredPurgeRow(receiptRow, this.spaceId)) !== canonicalJson(receipt)) { + throw new OhIntegrityError("The stored purge receipt differs from the requested purge."); + } + return receipt; + }); + } + close(): void { if (this.#closed) return; this.database.close(false); diff --git a/src/store.test.ts b/src/store.test.ts new file mode 100644 index 0000000..d45acb5 --- /dev/null +++ b/src/store.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, test } from "bun:test"; + +import { canonicalJson, canonicalSha256 } from "./canonical"; +import { OhRecordCodecRegistry } from "./contract"; +import { createKnowledgeGraphRecordV1 } from "./graph"; +import { createOhSqliteStoreAuthorityV1 } from "./sqlite/port"; +import { + createOhDependencyClosureV1, + createOhStoreBindingV1, + createOhStoreProfileV1, + emptyOhHeadV1, + OH_WORKING_STORE_PROFILE_V1, + OhProfileError, + OhSemanticBundleIngressV1, + parseOhDependencyClosureV1, + parseOhStoreBindingV1, + replayOhOperationsV1, + transitionOhSnapshotV1, + verifyOhDependencyClosureV1, + verifyOhDependencyClosureAgainstV1, +} from "./store"; + +describe("runtime-neutral Oh store contracts", () => { + test("binds a host-selected realm and application profile without changing V1 operations", () => { + const applicationProfileSha256 = canonicalSha256({ application: "fixture", v: 1 }); + const profile = createOhStoreProfileV1({ + applicationProfileSha256, + capabilities: OH_WORKING_STORE_PROFILE_V1.capabilities, + profileId: "fixture.working.v1", + profileKind: "working", + v: 1, + }); + const binding = createOhStoreBindingV1({ profile, realmId: "tenant:one/thread:two", + spaceId: "thread:two", v: 1 }); + expect(parseOhStoreBindingV1(binding)).toEqual(binding); + expect(binding.profile.applicationProfileSha256).toBe(applicationProfileSha256); + expect(binding.bindingSha256).toBe(canonicalSha256({ contractSha256: binding.contractSha256, + profile, realmId: binding.realmId, spaceId: binding.spaceId, v: 1 })); + expect(() => createOhStoreProfileV1({ applicationProfileSha256: null, + capabilities: { ...OH_WORKING_STORE_PROFILE_V1.capabilities, operationReplication: true }, + profileId: "unsafe.working.v1", profileKind: "working", v: 1 })).toThrow(OhProfileError); + }); + + test("exports only an exact dependency closure and detects tampering or smuggled records", () => { + const parent = createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:parent", + kind: "entity", v: 1, value: { name: "Parent" } }); + const child = createKnowledgeGraphRecordV1({ dependencies: [parent.key], key: "entity:child", + kind: "entity", v: 1, value: { name: "Child" } }); + const unrelated = createKnowledgeGraphRecordV1({ dependencies: [], key: "entity:unrelated", + kind: "entity", v: 1, value: { name: "Unrelated" } }); + const binding = createOhStoreBindingV1({ profile: OH_WORKING_STORE_PROFILE_V1, + realmId: "realm:test", spaceId: "space:test", v: 1 }); + const first = replayOhOperationsV1(binding.spaceId, []); + const snapshot = transitionOhSnapshotV1({ actorId: "agent.test", changes: [ + { kind: "put", record: child, v: 1 }, { kind: "put", record: parent, v: 1 }, + { kind: "put", record: unrelated, v: 1 }, + ], instant: "2026-08-29T12:00:00.000Z", operationId: "op_closure", + snapshot: first, spaceId: binding.spaceId }).snapshot; + const closure = createOhDependencyClosureV1({ binding, roots: [child.key], + snapshot }); + expect(closure.records.map(({ key }) => key)).toEqual([child.key, parent.key].sort()); + expect(parseOhDependencyClosureV1(closure)).toEqual(closure); + expect(verifyOhDependencyClosureV1(closure)).toEqual({ closure, ok: true }); + expect(verifyOhDependencyClosureAgainstV1(closure, { binding, head: snapshot.head })) + .toEqual({ closure, ok: true, verification: "expected-authority-and-head" }); + expect(verifyOhDependencyClosureAgainstV1(closure, { binding, + head: { ...snapshot.head, recordsSha256: "a".repeat(64) as typeof snapshot.head.recordsSha256 } })) + .toEqual({ ok: false, reason: "head-mismatch" }); + expect(parseOhDependencyClosureV1({ ...closure, records: [...closure.records, unrelated] })).toBeNull(); + expect(parseOhDependencyClosureV1({ ...closure, + closureSha256: "a".repeat(64) })).toBeNull(); + expect(first.records).toEqual([]); + }); + + test("requires a sealed explicit codec for every semantic put and commits one atomic bundle", async () => { + const authority = createOhSqliteStoreAuthorityV1({ path: ":memory:", + profile: OH_WORKING_STORE_PROFILE_V1, realmId: "realm:semantic", spaceId: "semantic" }); + const codecs = new OhRecordCodecRegistry().register({ kind: "entity", parse: (value) => { + if (typeof value !== "object" || value === null || Array.isArray(value) + || Object.keys(value).length !== 1 || typeof (value as { name?: unknown }).name !== "string") return null; + return { name: (value as { name: string }).name.normalize("NFC") }; + } }); + const ingress = new OhSemanticBundleIngressV1(authority.store, codecs); + expect(codecs.sealed).toBe(true); + expect(() => codecs.register({ kind: "statement", parse: () => ({}) })).toThrow("sealed"); + const head = await authority.store.head(); + const operation = await ingress.commit({ actorId: "agent.test", expectedHead: { + generation: head.generation, operationSha256: head.operationSha256 }, instant: "2026-08-29T12:00:00.000Z", + operationId: "op_semantic", puts: [{ dependencies: [], key: "entity:ada", kind: "entity", + v: 1, value: { name: "Ada" } }], tombstones: [], v: 1 }); + expect(operation.changes).toHaveLength(1); + await expect(ingress.commit({ actorId: "agent.test", expectedHead: { + generation: 1, operationSha256: operation.operationSha256 }, instant: null, + operationId: "op_unregistered", puts: [{ dependencies: [], key: "statement:no-codec", + kind: "statement", v: 1, value: {} }], tombstones: [], v: 1 })) + .rejects.toThrow("codec rejected"); + expect((await authority.store.head()).sequence).toBe(1); + await expect(ingress.commit({ actorId: "agent.test", expectedHead: { + generation: 1, operationSha256: operation.operationSha256 }, instant: null, + operationId: "op_missing_dependency", puts: [{ dependencies: ["entity:missing"], + key: "entity:child", kind: "entity", v: 1, value: { name: "Child" } }], + tombstones: [], v: 1 })).rejects.toThrow("Missing dependency"); + expect((await authority.store.head()).sequence).toBe(1); + expect(canonicalJson((await authority.store.snapshot()).records[0]?.value)).toBe('{"name":"Ada"}'); + await authority.store.close(); + }); + + test("rejects duplicate operation IDs during generic replay", () => { + const first = transitionOhSnapshotV1({ actorId: "agent.test", + changes: [{ kind: "put", record: createKnowledgeGraphRecordV1({ dependencies: [], + key: "entity:first", kind: "entity", v: 1, value: { name: "First" } }), v: 1 }], + instant: "2026-08-29T12:00:00.000Z", operationId: "op_duplicate", + snapshot: { head: emptyOhHeadV1(), records: [], v: 1 }, spaceId: "duplicate" }); + const second = transitionOhSnapshotV1({ actorId: "agent.test", + changes: [{ kind: "put", record: createKnowledgeGraphRecordV1({ dependencies: [], + key: "entity:second", kind: "entity", v: 1, value: { name: "Second" } }), v: 1 }], + instant: "2026-08-29T12:01:00.000Z", operationId: "op_duplicate", + snapshot: first.snapshot, spaceId: "duplicate" }); + expect(() => replayOhOperationsV1("duplicate", [first.operation, second.operation])).toThrow("replay chain"); + }); +}); diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..b58189d --- /dev/null +++ b/src/store.ts @@ -0,0 +1,701 @@ +import { + canonicalJson, + canonicalSha256, + hasExactKeys, + isPlainRecord, + parseCanonicalInstantV1, + parseSha256Hex, + safeCode, + type JsonValue, + type Sha256Hex, +} from "./canonical"; +import { + OH_CONTRACT_MANIFEST_V1, + OhRecordCodecRegistry, +} from "./contract"; +export { OhRecordCodecRegistry } from "./contract"; +import { + canonicalKnowledgeGraphChangesV1, + createKnowledgeGraphRecordV1, + graphRevisionSha256V1, + knowledgeGraphRecordRefV1, + OH_GRAPH_LIMITS_V1, + OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1, + parseKnowledgeGraphRecordV1, + type KnowledgeGraphChangeV1, + type KnowledgeGraphRecordKindV1, + type KnowledgeGraphRecordV1, +} from "./graph"; +import { + createOhOperationV1, + parseOhOperationV1, + type OhOperationV1, +} from "./operation"; + +export class OhConflictError extends Error { + constructor(message: string) { super(message); this.name = "OhConflictError"; } +} + +export class OhIntegrityError extends Error { + constructor(message: string) { super(message); this.name = "OhIntegrityError"; } +} + +export class OhDependencyError extends Error { + constructor(message: string) { super(message); this.name = "OhDependencyError"; } +} + +export class OhProfileError extends Error { + constructor(message: string) { super(message); this.name = "OhProfileError"; } +} + +export type OhHeadV1 = Readonly<{ + generation: number; + graphRevisionSha256: Sha256Hex | null; + operationSha256: Sha256Hex | null; + recordsSha256: Sha256Hex; + sequence: number; + v: 1; +}>; + +export type OhHeadRefV1 = Pick; + +export type OhCommitInputV1 = Readonly<{ + actorId: string; + changes: readonly KnowledgeGraphChangeV1[]; + expectedHead: Pick; + instant?: string; + operationId: string; +}>; + +export type OhSnapshotV1 = Readonly<{ + head: OhHeadV1; + records: readonly KnowledgeGraphRecordV1[]; + v: 1; +}>; + +export type OhChangesPageV1 = Readonly<{ + from: OhHeadRefV1; + hasMore: boolean; + operations: readonly OhOperationV1[]; + through: OhHeadV1; + to: OhHeadRefV1; + v: 1; +}>; + +export type OhStoreVerificationV1 = Readonly<{ + head: OhHeadV1; + integrity: "verified"; + operations: number; + records: number; + v: 1; +}>; + +export type OhStoreCapabilitiesV1 = Readonly<{ + changesSince: true; + dependencyClosureExport: true; + exactSnapshots: true; + operationReplication: boolean; + semanticBundleCommit: true; + v: 1; + wholeSpacePurge: boolean; +}>; + +export type OhStoreProfileV1 = Readonly<{ + applicationProfileSha256: Sha256Hex | null; + capabilities: OhStoreCapabilitiesV1; + profileId: string; + profileKind: "canonical" | "working"; + profileSha256: Sha256Hex; + v: 1; +}>; + +export type OhStoreBindingV1 = Readonly<{ + bindingSha256: Sha256Hex; + contractSha256: Sha256Hex; + profile: OhStoreProfileV1; + realmId: string; + spaceId: string; + v: 1; +}>; + +export const OH_CANONICAL_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: true, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: false, + }, + profileId: "oh.store.canonical.v1", + profileKind: "canonical", + v: 1, +}); + +export const OH_WORKING_STORE_PROFILE_V1 = createOhStoreProfileV1({ + applicationProfileSha256: null, + capabilities: { + changesSince: true, + dependencyClosureExport: true, + exactSnapshots: true, + operationReplication: false, + semanticBundleCommit: true, + v: 1, + wholeSpacePurge: true, + }, + profileId: "oh.store.working.v1", + profileKind: "working", + v: 1, +}); + +export type OhDependencyClosureV1 = Readonly<{ + binding: OhStoreBindingV1; + closureSha256: Sha256Hex; + head: OhHeadV1; + records: readonly KnowledgeGraphRecordV1[]; + roots: readonly string[]; + v: 1; +}>; + +export const OH_DEPENDENCY_CLOSURE_LIMITS_V1 = Object.freeze({ + bytes: 64 * 1024 * 1024, + records: 8_192, + roots: 1_024, +}); + +export type OhSpacePurgeReceiptV1 = Readonly<{ + bindingSha256: Sha256Hex; + priorHead: OhHeadV1; + purgedAt: string; + receiptSha256: Sha256Hex; + spaceId: string; + v: 1; +}>; + +export class OhPurgedSpaceError extends Error { + readonly receipt: OhSpacePurgeReceiptV1; + + constructor(receipt: OhSpacePurgeReceiptV1) { + super(`Oh space ${receipt.spaceId} was purged at ${receipt.purgedAt}.`); + this.name = "OhPurgedSpaceError"; + this.receipt = receipt; + } +} + +export interface OhStoreV1 { + readonly binding: OhStoreBindingV1; + changesSince( + from: OhHeadRefV1, + options?: Readonly<{ limit?: number; through?: OhHeadRefV1 }>, + ): Promise; + close(): Promise; + commit(input: OhCommitInputV1): Promise; + exportDependencyClosure(input: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + roots: readonly string[]; + }>): Promise; + head(): Promise; + snapshot(options?: Readonly<{ + head?: OhHeadRefV1; + maximumRecords?: number; + }>): Promise; + verify(): Promise; +} + +/** Kept separate so an agent-facing store object never carries deletion authority. */ +export interface OhStoreHostControlV1 { + readonly binding: OhStoreBindingV1; + purgeWorkingSpace(input: Readonly<{ purgedAt?: string }>): Promise; +} + +export type OhStoreAuthorityV1 = Readonly<{ + host: OhStoreHostControlV1; + store: OhStoreV1; +}>; + +const EMPTY_RECORDS_SHA256 = canonicalSha256([]); + +export function emptyOhHeadV1(): OhHeadV1 { + return { + generation: 0, + graphRevisionSha256: null, + operationSha256: null, + recordsSha256: EMPTY_RECORDS_SHA256, + sequence: 0, + v: 1, + }; +} + +export function parseOhHeadV1(value: unknown): OhHeadV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["generation", "graphRevisionSha256", + "operationSha256", "recordsSha256", "sequence", "v"]) || value.v !== 1) return null; + const graphRevisionSha256 = value.graphRevisionSha256 === null + ? null : parseSha256Hex(value.graphRevisionSha256); + const operationSha256 = value.operationSha256 === null + ? null : parseSha256Hex(value.operationSha256); + const recordsSha256 = parseSha256Hex(value.recordsSha256); + const generation = Number.isSafeInteger(value.generation) && (value.generation as number) >= 0 + ? value.generation as number : null; + const sequence = Number.isSafeInteger(value.sequence) && (value.sequence as number) >= 0 + ? value.sequence as number : null; + return generation !== null && sequence !== null && generation === sequence + && recordsSha256 !== null + && (value.graphRevisionSha256 === null || graphRevisionSha256 !== null) + && (value.operationSha256 === null || operationSha256 !== null) + && ((sequence === 0) === (operationSha256 === null)) + && ((sequence === 0) === (graphRevisionSha256 === null)) + ? { generation, graphRevisionSha256, operationSha256, recordsSha256, sequence, v: 1 } + : null; +} + +export function parseOhHeadRefV1(value: unknown): OhHeadRefV1 | null { + const complete = parseOhHeadV1(value); + if (complete !== null) { + return { operationSha256: complete.operationSha256, sequence: complete.sequence }; + } + if (!isPlainRecord(value) || !hasExactKeys(value, ["operationSha256", "sequence"])) return null; + const operationSha256 = value.operationSha256 === null ? null : parseSha256Hex(value.operationSha256); + const sequence = Number.isSafeInteger(value.sequence) && (value.sequence as number) >= 0 + ? value.sequence as number : null; + return sequence !== null && (value.operationSha256 === null || operationSha256 !== null) + && ((sequence === 0) === (operationSha256 === null)) + ? { operationSha256, sequence } : null; +} + +function parseCapabilities(value: unknown): OhStoreCapabilitiesV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["changesSince", "dependencyClosureExport", + "exactSnapshots", "operationReplication", "semanticBundleCommit", "v", "wholeSpacePurge"]) + || value.changesSince !== true || value.dependencyClosureExport !== true + || value.exactSnapshots !== true || typeof value.operationReplication !== "boolean" + || value.semanticBundleCommit !== true || value.v !== 1 + || typeof value.wholeSpacePurge !== "boolean") return null; + return { changesSince: true, dependencyClosureExport: true, exactSnapshots: true, + operationReplication: value.operationReplication, semanticBundleCommit: true, v: 1, + wholeSpacePurge: value.wholeSpacePurge }; +} + +type OhStoreProfileInputV1 = Omit; + +export function createOhStoreProfileV1(input: OhStoreProfileInputV1): OhStoreProfileV1 { + if (!isPlainRecord(input) || !hasExactKeys(input, ["applicationProfileSha256", "capabilities", + "profileId", "profileKind", "v"]) || input.v !== 1) throw new TypeError("Invalid Oh store profile input."); + const profileId = safeCode(input.profileId); + const applicationProfileSha256 = input.applicationProfileSha256 === null + ? null : parseSha256Hex(input.applicationProfileSha256); + const capabilities = parseCapabilities(input.capabilities); + if (profileId === null || capabilities === null + || (input.applicationProfileSha256 !== null && applicationProfileSha256 === null) + || (input.profileKind !== "canonical" && input.profileKind !== "working")) { + throw new TypeError("Invalid Oh store profile input."); + } + if (input.profileKind === "working" + && (capabilities.operationReplication || !capabilities.wholeSpacePurge)) { + throw new OhProfileError("A working profile must disable operation replication and permit whole-space purge."); + } + if (input.profileKind === "canonical" && capabilities.wholeSpacePurge) { + throw new OhProfileError("A canonical profile cannot permit whole-space purge."); + } + const payload = { applicationProfileSha256, capabilities: Object.freeze(capabilities), profileId, + profileKind: input.profileKind, v: 1 as const }; + return Object.freeze({ ...payload, profileSha256: canonicalSha256(payload) }); +} + +export function parseOhStoreProfileV1(value: unknown): OhStoreProfileV1 | null { + if (!isPlainRecord(value) || !Object.hasOwn(value, "profileSha256")) return null; + const digest = parseSha256Hex(value.profileSha256); + const { profileSha256: _profileSha256, ...input } = value; + try { + const created = createOhStoreProfileV1(input as unknown as OhStoreProfileInputV1); + return digest !== null && created.profileSha256 === digest ? created : null; + } catch { return null; } +} + +export function createOhStoreBindingV1(input: Readonly<{ + profile: OhStoreProfileV1; + realmId: string; + spaceId: string; + v: 1; +}>): OhStoreBindingV1 { + const profile = parseOhStoreProfileV1(input.profile); + const realmId = safeCode(input.realmId); + const spaceId = safeCode(input.spaceId); + if (input.v !== 1 || profile === null || realmId === null || spaceId === null) { + throw new TypeError("Invalid Oh store binding input."); + } + const payload = { contractSha256: OH_CONTRACT_MANIFEST_V1.contractSha256, + profile, realmId, spaceId, v: 1 as const }; + return Object.freeze({ ...payload, bindingSha256: canonicalSha256(payload) }); +} + +export function parseOhStoreBindingV1(value: unknown): OhStoreBindingV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["bindingSha256", "contractSha256", + "profile", "realmId", "spaceId", "v"]) || value.v !== 1 + || value.contractSha256 !== OH_CONTRACT_MANIFEST_V1.contractSha256) return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const profile = parseOhStoreProfileV1(value.profile); + try { + if (bindingSha256 === null || profile === null) return null; + const created = createOhStoreBindingV1({ profile, realmId: value.realmId as string, + spaceId: value.spaceId as string, v: 1 }); + return created.bindingSha256 === bindingSha256 ? created : null; + } catch { return null; } +} + +function sortedRecords(records: Iterable): readonly KnowledgeGraphRecordV1[] { + return [...records].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0); +} + +function verifyDependencies(records: ReadonlyMap): void { + for (const record of records.values()) { + for (const dependency of record.dependencies) { + if (!records.has(dependency)) throw new OhDependencyError(`Missing dependency ${dependency} for ${record.key}.`); + } + } +} + +export function replayOhOperationsV1( + spaceId: string, + values: readonly OhOperationV1[], + maximumRecords: number = OH_GRAPH_LIMITS_V1.recordsPerSnapshot, +): OhSnapshotV1 { + const parsedSpaceId = safeCode(spaceId); + if (parsedSpaceId === null || !Number.isSafeInteger(maximumRecords) || maximumRecords < 1 + || maximumRecords > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new TypeError("Invalid operation replay input."); + } + const records = new Map(); + const operationIds = new Set(); + let head = emptyOhHeadV1(); + for (const value of values) { + const operation = parseOhOperationV1(value); + if (operation === null || operation.spaceId !== parsedSpaceId + || operation.sequence !== head.sequence + 1 + || operation.parentOperationSha256 !== head.operationSha256 + || operationIds.has(operation.operationId)) { + throw new OhIntegrityError("Operation replay chain is broken."); + } + operationIds.add(operation.operationId); + for (const change of operation.changes) { + if (change.kind === "put") records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior?.recordSha256 !== change.priorSha256) { + throw new OhIntegrityError("Replay tombstone does not match its prior record."); + } + records.delete(change.key); + } + } + if (records.size > maximumRecords) throw new RangeError("Operation replay exceeds its record bound."); + verifyDependencies(records); + const refs = sortedRecords(records.values()).map(knowledgeGraphRecordRefV1); + const recordsSha256 = canonicalSha256(refs); + const graphRevisionSha256 = graphRevisionSha256V1({ changes: operation.changes, + operationId: operation.operationId, parentGraphRevisionSha256: head.graphRevisionSha256, + recordsSha256, revision: operation.sequence }); + if (recordsSha256 !== operation.recordsSha256 + || graphRevisionSha256 !== operation.graphRevisionSha256) { + throw new OhIntegrityError("Replay does not reproduce an operation head."); + } + head = { generation: operation.sequence, graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256, + sequence: operation.sequence, v: 1 }; + } + return { head, records: sortedRecords(records.values()), v: 1 }; +} + +export function transitionOhSnapshotV1(input: Readonly<{ + actorId: string; + changes: readonly KnowledgeGraphChangeV1[]; + instant: string; + operationId: string; + snapshot: OhSnapshotV1; + spaceId: string; +}>): Readonly<{ operation: OhOperationV1; snapshot: OhSnapshotV1 }> { + const actorId = safeCode(input.actorId); + const operationId = safeCode(input.operationId); + const spaceId = safeCode(input.spaceId); + const instant = parseCanonicalInstantV1(input.instant); + const changes = canonicalKnowledgeGraphChangesV1(input.changes); + if (actorId === null || operationId === null || spaceId === null || instant === null + || changes.length === 0 || changes.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid graph transition input."); + } + const head = parseOhHeadV1(input.snapshot.head); + if (input.snapshot.v !== 1 || head === null || !Array.isArray(input.snapshot.records)) { + throw new OhIntegrityError("The transition snapshot is invalid."); + } + const records = new Map(); + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || records.has(record.key)) { + throw new OhIntegrityError("The transition snapshot contains an invalid record."); + } + records.set(record.key, record); + } + verifyDependencies(records); + const priorRecordsSha256 = canonicalSha256(sortedRecords(records.values()).map(knowledgeGraphRecordRefV1)); + if (priorRecordsSha256 !== head.recordsSha256) { + throw new OhIntegrityError("The transition snapshot does not reproduce its head."); + } + for (const change of changes) { + if (change.kind === "put") records.set(change.record.key, change.record); + else { + const prior = records.get(change.key); + if (prior === undefined || prior.recordSha256 !== change.priorSha256) { + throw new OhConflictError(`The prior digest for ${change.key} does not match the snapshot.`); + } + records.delete(change.key); + } + } + if (records.size > OH_GRAPH_LIMITS_V1.recordsPerSnapshot) { + throw new RangeError("Graph transition exceeds its record snapshot limit."); + } + verifyDependencies(records); + const nextRecords = sortedRecords(records.values()); + const recordsSha256 = canonicalSha256(nextRecords.map(knowledgeGraphRecordRefV1)); + const graphRevisionSha256 = graphRevisionSha256V1({ changes, operationId, + parentGraphRevisionSha256: head.graphRevisionSha256, recordsSha256, + revision: head.sequence + 1 }); + const operation = createOhOperationV1({ actorId, changes, + contractId: OH_CONTRACT_MANIFEST_V1.contractId, graphRevisionSha256, instant, + operationId, parentOperationSha256: head.operationSha256, + recordsSha256, sequence: head.sequence + 1, spaceId, v: 1 }); + const nextHead: OhHeadV1 = { generation: operation.sequence, graphRevisionSha256, + operationSha256: operation.operationSha256, recordsSha256, + sequence: operation.sequence, v: 1 }; + return { operation, snapshot: { head: nextHead, records: nextRecords, v: 1 } }; +} + +function normalizeRoots(values: readonly string[]): readonly string[] { + if (!Array.isArray(values) || values.length < 1 || values.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots) { + throw new RangeError(`A dependency closure needs 1 through ${OH_DEPENDENCY_CLOSURE_LIMITS_V1.roots} roots.`); + } + const roots = values.map((value) => safeCode(value, 512)); + if (roots.some((value) => value === null)) throw new TypeError("Invalid dependency closure root."); + const sorted = [...roots as string[]].sort(); + if (sorted.some((value, index) => index > 0 && sorted[index - 1] === value)) { + throw new TypeError("Dependency closure roots must be unique."); + } + return sorted; +} + +function closureRecords( + available: ReadonlyMap, + roots: readonly string[], + maximumRecords: number, + maximumBytes: number = OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes - 64 * 1024, +): readonly KnowledgeGraphRecordV1[] { + const selected = new Map(); + const pending = [...roots]; + let selectedBytes = 0; + while (pending.length > 0) { + const key = pending.pop() as string; + if (selected.has(key)) continue; + const record = available.get(key); + if (record === undefined) throw new OhDependencyError(`Dependency closure record ${key} is missing.`); + selectedBytes += Buffer.byteLength(canonicalJson(record), "utf8") + 1; + if (selectedBytes > maximumBytes) throw new RangeError("Dependency closure exceeds its canonical byte bound."); + selected.set(key, record); + if (selected.size > maximumRecords) throw new RangeError("Dependency closure exceeds its record bound."); + pending.push(...record.dependencies); + } + return sortedRecords(selected.values()); +} + +export function createOhDependencyClosureV1(input: Readonly<{ + binding: OhStoreBindingV1; + maximumRecords?: number; + roots: readonly string[]; + snapshot: OhSnapshotV1; +}>): OhDependencyClosureV1 { + const binding = parseOhStoreBindingV1(input.binding); + const head = parseOhHeadV1(input.snapshot.head); + const maximumRecords = input.maximumRecords ?? OH_DEPENDENCY_CLOSURE_LIMITS_V1.records; + if (binding === null || head === null || !Number.isSafeInteger(maximumRecords) + || maximumRecords < 1 || maximumRecords > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) { + throw new TypeError("Invalid dependency closure input."); + } + const roots = normalizeRoots(input.roots); + const available = new Map(); + for (const value of input.snapshot.records) { + const record = parseKnowledgeGraphRecordV1(value); + if (record === null || available.has(record.key)) throw new OhIntegrityError("Snapshot contains an invalid record."); + available.set(record.key, record); + } + const recordsSha256 = canonicalSha256(sortedRecords(available.values()).map(knowledgeGraphRecordRefV1)); + if (recordsSha256 !== head.recordsSha256) throw new OhIntegrityError("Snapshot records do not reproduce its head."); + const records = closureRecords(available, roots, maximumRecords); + const payload = { binding, head, records, roots, v: 1 as const }; + const closure = { ...payload, closureSha256: canonicalSha256(payload) }; + if (Buffer.byteLength(canonicalJson(closure), "utf8") > OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes) { + throw new RangeError("Dependency closure exceeds its canonical byte bound."); + } + return Object.freeze(closure); +} + +export function parseOhDependencyClosureV1(value: unknown): OhDependencyClosureV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["binding", "closureSha256", "head", + "records", "roots", "v"]) || value.v !== 1 || !Array.isArray(value.records) + || !Array.isArray(value.roots) || value.records.length > OH_DEPENDENCY_CLOSURE_LIMITS_V1.records) return null; + const binding = parseOhStoreBindingV1(value.binding); + const head = parseOhHeadV1(value.head); + const closureSha256 = parseSha256Hex(value.closureSha256); + if (binding === null || head === null || closureSha256 === null) return null; + const records = new Map(); + for (const item of value.records) { + const record = parseKnowledgeGraphRecordV1(item); + if (record === null || records.has(record.key)) return null; + records.set(record.key, record); + } + try { + const roots = normalizeRoots(value.roots as string[]); + if (canonicalJson(roots) !== canonicalJson(value.roots)) return null; + const exact = closureRecords(records, roots, OH_DEPENDENCY_CLOSURE_LIMITS_V1.records); + if (canonicalJson(exact) !== canonicalJson(value.records)) return null; + const payload = { binding, head, records: exact, roots, v: 1 as const }; + const parsed = { ...payload, closureSha256 }; + return canonicalSha256(payload) === closureSha256 + && Buffer.byteLength(canonicalJson(parsed), "utf8") <= OH_DEPENDENCY_CLOSURE_LIMITS_V1.bytes + ? Object.freeze(parsed) : null; + } catch { return null; } +} + +export function verifyOhDependencyClosureV1(value: unknown): + | Readonly<{ closure: OhDependencyClosureV1; ok: true }> + | Readonly<{ ok: false; reason: "invalid-closure" }> { + const closure = parseOhDependencyClosureV1(value); + return closure === null ? { ok: false, reason: "invalid-closure" } : { closure, ok: true }; +} + +/** + * Strong adoption check. Unlike structural self-verification, this also binds + * the capsule to the exact store binding and head selected by trusted host code. + */ +export function verifyOhDependencyClosureAgainstV1(value: unknown, expected: Readonly<{ + binding: OhStoreBindingV1; + head: OhHeadV1; +}>): + | Readonly<{ closure: OhDependencyClosureV1; ok: true; verification: "expected-authority-and-head" }> + | Readonly<{ ok: false; reason: "binding-mismatch" | "head-mismatch" | "invalid-closure" | "invalid-expectation" }> { + const binding = parseOhStoreBindingV1(expected.binding); + const head = parseOhHeadV1(expected.head); + if (binding === null || head === null) return { ok: false, reason: "invalid-expectation" }; + const closure = parseOhDependencyClosureV1(value); + if (closure === null) return { ok: false, reason: "invalid-closure" }; + if (closure.binding.bindingSha256 !== binding.bindingSha256) return { ok: false, reason: "binding-mismatch" }; + if (canonicalJson(closure.head) !== canonicalJson(head)) return { ok: false, reason: "head-mismatch" }; + return { closure, ok: true, verification: "expected-authority-and-head" }; +} + +export function createOhSpacePurgeReceiptV1(input: Readonly<{ + binding: OhStoreBindingV1; + priorHead: OhHeadV1; + purgedAt: string; +}>): OhSpacePurgeReceiptV1 { + const binding = parseOhStoreBindingV1(input.binding); + const priorHead = parseOhHeadV1(input.priorHead); + const purgedAt = parseCanonicalInstantV1(input.purgedAt); + if (binding === null || priorHead === null || purgedAt === null + || binding.profile.profileKind !== "working" + || !binding.profile.capabilities.wholeSpacePurge) { + throw new OhProfileError("Only a bound working realm can produce a purge receipt."); + } + const payload = { bindingSha256: binding.bindingSha256, priorHead, purgedAt, + spaceId: binding.spaceId, v: 1 as const }; + return Object.freeze({ ...payload, receiptSha256: canonicalSha256(payload) }); +} + +export function parseOhSpacePurgeReceiptV1(value: unknown): OhSpacePurgeReceiptV1 | null { + if (!isPlainRecord(value) || !hasExactKeys(value, ["bindingSha256", "priorHead", "purgedAt", + "receiptSha256", "spaceId", "v"]) || value.v !== 1) return null; + const bindingSha256 = parseSha256Hex(value.bindingSha256); + const priorHead = parseOhHeadV1(value.priorHead); + const purgedAt = parseCanonicalInstantV1(value.purgedAt); + const receiptSha256 = parseSha256Hex(value.receiptSha256); + const spaceId = safeCode(value.spaceId); + if (bindingSha256 === null || priorHead === null || purgedAt === null + || receiptSha256 === null || spaceId === null) return null; + const payload = { bindingSha256, priorHead, purgedAt, spaceId, v: 1 as const }; + return canonicalSha256(payload) === receiptSha256 + ? Object.freeze({ ...payload, receiptSha256 }) : null; +} + +export type OhSemanticBundleV1 = Readonly<{ + actorId: string; + expectedHead: Pick; + instant: string | null; + operationId: string; + puts: readonly Readonly<{ + dependencies: readonly string[]; + key: string; + kind: KnowledgeGraphRecordKindV1; + v: 1; + value: unknown; + }>[]; + tombstones: readonly Readonly<{ + key: string; + priorSha256: Sha256Hex; + v: 1; + }>[]; + v: 1; +}>; + +/** A strict model-facing ingress: every put must have a registered codec. */ +export class OhSemanticBundleIngressV1 { + readonly #codecs: OhRecordCodecRegistry; + readonly #store: OhStoreV1; + + constructor(store: OhStoreV1, codecs: OhRecordCodecRegistry) { + this.#store = store; + this.#codecs = codecs.seal(); + } + + async commit(value: unknown): Promise { + if (!isPlainRecord(value) || !hasExactKeys(value, ["actorId", "expectedHead", "instant", + "operationId", "puts", "tombstones", "v"]) || value.v !== 1 + || !Array.isArray(value.puts) || !Array.isArray(value.tombstones) + || value.puts.length + value.tombstones.length < 1 + || value.puts.length + value.tombstones.length > OH_GRAPH_LIMITS_V1.changesPerOperation) { + throw new TypeError("Invalid semantic bundle."); + } + const actorId = safeCode(value.actorId); + const operationId = safeCode(value.operationId); + const expected = value.expectedHead; + const instant = value.instant === null ? undefined : parseCanonicalInstantV1(value.instant); + if (actorId === null || operationId === null || !isPlainRecord(expected) + || !hasExactKeys(expected, ["generation", "operationSha256"]) + || !Number.isSafeInteger(expected.generation) || (expected.generation as number) < 0 + || (expected.operationSha256 !== null && parseSha256Hex(expected.operationSha256) === null) + || (((expected.generation as number) === 0) !== (expected.operationSha256 === null)) + || (value.instant !== null && instant === null)) throw new TypeError("Invalid semantic bundle identity."); + const changes: KnowledgeGraphChangeV1[] = []; + for (const item of value.puts) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["dependencies", "key", "kind", "v", "value"]) + || item.v !== 1 || !Array.isArray(item.dependencies) + || !OH_KNOWLEDGE_GRAPH_RECORD_KINDS_V1.some((kind) => kind === item.kind)) { + throw new TypeError("Invalid semantic bundle put."); + } + const parsed = this.#codecs.parseRequired(item.kind as KnowledgeGraphRecordKindV1, item.value); + if (parsed === null) throw new TypeError(`The ${String(item.kind)} codec rejected a semantic value.`); + const record = createKnowledgeGraphRecordV1({ dependencies: item.dependencies as string[], + key: item.key as string, kind: item.kind as KnowledgeGraphRecordKindV1, v: 1, value: parsed }); + changes.push({ kind: "put", record, v: 1 }); + } + for (const item of value.tombstones) { + if (!isPlainRecord(item) || !hasExactKeys(item, ["key", "priorSha256", "v"]) || item.v !== 1) { + throw new TypeError("Invalid semantic bundle tombstone."); + } + const priorSha256 = parseSha256Hex(item.priorSha256); + if (typeof item.key !== "string" || priorSha256 === null) throw new TypeError("Invalid semantic bundle tombstone."); + changes.push({ key: item.key, kind: "tombstone", priorSha256, v: 1 }); + } + const canonical = canonicalKnowledgeGraphChangesV1(changes); + return await this.#store.commit({ actorId, changes: canonical, + expectedHead: { generation: expected.generation as number, + operationSha256: expected.operationSha256 as Sha256Hex | null }, + ...(typeof instant === "string" ? { instant } : {}), operationId }); + } +} diff --git a/tests/node-portable-types.ts b/tests/node-portable-types.ts new file mode 100644 index 0000000..5742306 --- /dev/null +++ b/tests/node-portable-types.ts @@ -0,0 +1,17 @@ +import { + OhRecordCodecRegistry, + type OhStoreV1, +} from "@hraness/oh/store"; +import { + createOhMemoryAgentV1, + type OhMemoryFacadeOptionsV1, + type OhMemoryRememberReceiptV1, +} from "@hraness/oh/experimental/memory"; + +// Compile-only consumer fixture for the portable public entrypoints. The Node +// runtime exercise lives in node-portable.mjs; this catches declaration drift. +export const portableCodecs = new OhRecordCodecRegistry(); +export const portableMemoryFactory: typeof createOhMemoryAgentV1 = createOhMemoryAgentV1; +export type PortableMemoryOptions = OhMemoryFacadeOptionsV1; +export type PortableMemoryReceipt = OhMemoryRememberReceiptV1; +export type PortableStore = OhStoreV1; diff --git a/tests/node-portable.mjs b/tests/node-portable.mjs new file mode 100644 index 0000000..37b975d --- /dev/null +++ b/tests/node-portable.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; + +// Exercise the published package subpaths, not internal build paths. This also +// catches an export that accidentally makes Bun-only modules reachable. +const store = await import("@hraness/oh/store"); +const libsql = await import("@hraness/oh/libsql"); +const memory = await import("@hraness/oh/experimental/memory"); +const projection = await import("@hraness/oh/projection"); + +assert.equal(typeof store.createOhStoreBindingV1, "function"); +assert.equal(typeof store.OhRecordCodecRegistry, "function"); +assert.equal(typeof store.OhSemanticBundleIngressV1, "function"); +assert.equal(typeof libsql.createOhLibSqlStoreAuthorityV1, "function"); +assert.equal(typeof memory.createOhMemoryAgentV1, "function"); +assert.equal(store.OH_WORKING_STORE_PROFILE_V1.profileKind, "working"); +assert.equal(store.OH_WORKING_STORE_PROFILE_V1.capabilities.operationReplication, false); +assert.equal(store.OH_WORKING_STORE_PROFILE_V1.capabilities.wholeSpacePurge, true); + +function emptyStore(profile, realmId, spaceId) { + const binding = store.createOhStoreBindingV1({ profile, realmId, spaceId, v: 1 }); + const head = store.emptyOhHeadV1(); + return { + binding, + async head() { return head; }, + async snapshot(options = {}) { + assert.equal(options.maximumRecords, memory.OH_MEMORY_LIMITS_V1.maximumRecordsPerLane); + return { head, records: [], v: 1 }; + }, + }; +} + +const canonicalStore = emptyStore(store.OH_CANONICAL_STORE_PROFILE_V1, + "realm:node-canonical", "node-canonical"); +const workingStore = emptyStore(store.OH_WORKING_STORE_PROFILE_V1, + "realm:node-working", "node-working"); +const lane = projection.ohProjectionVariableV1("lane"); +const key = projection.ohProjectionVariableV1("key"); +const kind = projection.ohProjectionVariableV1("kind"); +const digest = projection.ohProjectionVariableV1("digest"); +const record = projection.createOhProjectionLiteralV1({ + relation: "memory.record", terms: [lane, key, kind, digest], +}); +const visible = projection.createOhProjectionLiteralV1({ + relation: "memory.visible", terms: [lane, key, digest], +}); +const rulePack = projection.createOhProjectionRulePackV1({ + rulePackId: "memory.node-visible", rulePackRevision: 1, + rules: [projection.createOhProjectionRuleV1({ + body: [record], head: visible, ruleId: "memory.node-visible", + })], +}); +const query = projection.createOhProjectionQueryV1({ + find: ["lane", "key", "digest"], limit: 10, queryId: "memory.node-visible", + where: [visible], +}); +const agent = await memory.createOhMemoryAgentV1({ + actorId: "node.memory-agent", + canonical: { authorityId: "node.canonical", + expectedBindingSha256: canonicalStore.binding.bindingSha256, + expectedHead: await canonicalStore.head(), store: canonicalStore }, + monotonicNow: () => 0, + now: () => new Date("2026-08-29T12:00:00.000Z"), + programs: [{ programId: "memory.node-visible", purpose: "node.portability", query, rulePack }], + working: { authorityId: "node.working", codecs: new store.OhRecordCodecRegistry(), + expectedBindingSha256: workingStore.binding.bindingSha256, store: workingStore }, +}); +const result = await agent.query({ programId: "memory.node-visible", v: 1 }); +assert.equal(result.authority, "derived"); +assert.equal(result.identity.purpose, "node.portability"); +assert.deepEqual(result.rows, []); +assert.equal(Object.isFrozen(result.rows), true); diff --git a/tests/public-surface.test.ts b/tests/public-surface.test.ts index d8309ec..4fee5e3 100644 --- a/tests/public-surface.test.ts +++ b/tests/public-surface.test.ts @@ -27,8 +27,10 @@ const markdownFiles = [ "spec/v1/schema-evolution.md", "spec/v1/graph.md", "spec/v1/storage.md", + "spec/v1/store.md", "spec/v1/sync.md", "spec/v1/embedding.md", + "spec/v1/projection.md", "spec/v1/migration.md", "skills/oh/SKILL.md", ] as const; @@ -39,6 +41,10 @@ const schemaFiles = [ "spec/v1/schema-revision.schema.json", "spec/v1/operation.schema.json", "spec/v1/sync-bundle.schema.json", + "spec/v1/projection-rule-pack.schema.json", + "spec/v1/projection-query.schema.json", + "spec/v1/projection-identity.schema.json", + "spec/v1/projection-result.schema.json", ] as const; const publicSourceEntries = [ @@ -146,13 +152,13 @@ describe("public identity and documentation", () => { ]); expect(readme.startsWith(`# ${tagline}\n`)).toBe(true); expect(packageJson.name).toBe("@hraness/oh"); - expect(packageJson.version).toBe("0.1.1"); + expect(packageJson.version).toBe("0.2.0"); expect(packageJson.description).toBe(tagline); expect(packageJson.homepage).toBe("https://oh.computer"); expect(packageJson.license).toBe("MIT"); expect(packageJson.private).toBe(false); expect(packageJson.packageManager).toBe("bun@1.3.14"); - expect(packageJson.engines).toEqual({ bun: ">=1.3.14" }); + expect(packageJson.engines).toEqual({ bun: ">=1.3.14", node: ">=24" }); expect(packageJson.repository).toEqual({ type: "git", url: "git+https://github.com/hraness/oh.git" }); expect(packageJson.bugs).toEqual({ url: "https://github.com/hraness/oh/issues" }); expect(skill).toMatch(/^---\nname: oh\ndescription: .+\n---\n/u); @@ -239,7 +245,8 @@ describe("versioned public contract", () => { const manifest = await json("spec/manifest.json"); const version = (manifest.versions as readonly Record[])[0] as Record; const claims = [version.contract, version.embeddingProfile, version.ontology, version.specification, - ...(Array.isArray(version.schemas) ? version.schemas : [])]; + ...collectStringLeaves(version.memory), + ...collectStringLeaves(version.projection), ...(Array.isArray(version.schemas) ? version.schemas : [])]; expect(claims.length).toBeGreaterThan(4); for (const claim of claims) { expect(typeof claim).toBe("string"); @@ -257,7 +264,7 @@ describe("versioned public contract", () => { const claims = new Set(); for (const path of markdownFiles) { const markdown = await readFile(join(root, path), "utf8"); - for (const match of markdown.matchAll(/@hraness\/oh(?:\/[a-z0-9-]+)?/gu)) claims.add(match[0]); + for (const match of markdown.matchAll(/@hraness\/oh(?:\/[a-z0-9-]+)*/gu)) claims.add(match[0]); } expect(claims.size).toBeGreaterThan(1); for (const claim of claims) { @@ -276,10 +283,15 @@ describe("versioned public contract", () => { const exports = packageJson.exports as Record; expect(Object.keys(exports).sort()).toEqual([ ".", + "./experimental/memory", + "./experimental/projection-suss", + "./libsql", "./package.json", + "./projection", "./sdk", "./semantic", "./sqlite", + "./store", "./sync", ]); @@ -299,7 +311,8 @@ describe("versioned public contract", () => { const peers = packageJson.peerDependencies as Record; const peerMetadata = packageJson.peerDependenciesMeta as Record>; - expect(peers).toEqual({ "@libsql/client": ">=0.17.4 <1", "@tobilu/qmd": "2.5.3" }); + expect(peers).toEqual({ "@libsql/client": ">=0.17.4 <1", "@suss/datalog": "0.20.0", + "@tobilu/qmd": "2.5.3" }); expect(Object.keys(peers).every((name) => peerMetadata[name]?.optional === true)).toBe(true); }); @@ -312,6 +325,49 @@ describe("versioned public contract", () => { expect(await json("spec/v1/embedding-profile.json")).toEqual(OH_EMBEDDING_PROFILE_V1); }); + test("discovers the complete projection exchange surface", async () => { + const manifest = await json("spec/manifest.json"); + const version = (manifest.versions as readonly Record[])[0] as Record; + expect(version.projection).toEqual({ + identitySchema: "./v1/projection-identity.schema.json", + querySchema: "./v1/projection-query.schema.json", + resultSchema: "./v1/projection-result.schema.json", + rulePackSchema: "./v1/projection-rule-pack.schema.json", + specification: "./v1/projection.md", + }); + + const identity = await json("spec/v1/projection-identity.schema.json"); + const identityProperties = identity.properties as Record; + expect(Object.keys(identityProperties).sort()).toEqual([ + "contractSha256", "datasetSha256", "engineSha256", "evaluationSha256", "projectionSha256", + "querySha256", "rulePackSha256", "semantics", "snapshotSha256", "v", + ]); + + const result = await json("spec/v1/projection-result.schema.json"); + const definitions = result.$defs as Record>; + const evaluation = definitions.evaluation?.properties as Record; + const row = definitions.row?.properties as Record; + const stats = definitions.stats?.properties as Record; + expect(Object.keys(evaluation).sort()).toEqual([ + "maximumDerivedTuples", "maximumProofDepth", "maximumProofNodes", "maximumResultBytes", + "maximumRounds", "maximumTotalProofNodes", "maximumWorkUnits", "v", + ]); + expect(Object.keys(row).sort()).toEqual(["proofs", "proofsTruncated", "supportCount", "v", "values"]); + expect(Object.keys(stats).sort()).toEqual([ + "baseFacts", "derivedFacts", "proofNodes", "proofsTruncated", "queryMatches", "relations", + "rounds", "truncated", "truncationReasons", "v", "workUnits", + ]); + }); + + test("discovers the experimental composite memory boundary", async () => { + const manifest = await json("spec/manifest.json"); + const version = (manifest.versions as readonly Record[])[0] as Record; + expect(version.memory).toEqual({ specification: "./v1/memory.md" }); + const memory = await readFile(join(root, "spec/v1/memory.md"), "utf8"); + expect(memory).toContain("One kernel, two authorities"); + expect(memory).toContain("It does not sync the working operation chain"); + }); + test("keeps every JSON Schema parseable, versioned, and locally closed", async () => { for (const path of schemaFiles) { const schema = await json(path); @@ -342,6 +398,7 @@ describe("repository policy", () => { const scripts = packageJson.scripts as Record; const siteScripts = sitePackageJson.scripts as Record; expect(scripts.check).toContain("bun run test"); + expect(scripts.check).toContain("bun run test:node"); expect(scripts.test).toBe("bun test ./src ./tests ./site/tests/source.test.ts"); expect(scripts.test).not.toContain("runtime.test.ts"); expect(siteScripts.postbuild).toBe("bun test ./tests/runtime.test.ts"); diff --git a/tsconfig.node-portable.json b/tsconfig.node-portable.json new file mode 100644 index 0000000..4608d41 --- /dev/null +++ b/tsconfig.node-portable.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["tests/node-portable-types.ts"] +}