Address reviewer feedback on #83: - Replace dynamic `await import("./bootstrap-capable.js")` in bundle.ts with a static top-of-file import (no real cycle exists, bootstrap.ts is already statically imported). - Remove unused `bootstrapSym` Symbol.for() / `void bootstrapSym` dead code in importBundle. - Move the misplaced `import { decode } from "cborg"` to the top of bundle.ts with the other imports. - Document the new `export` / `import` commands and `--store`, `--scope`, `-o` flags in: - root README.md (commands + global flags) - packages/cli/README.md (command table + bundles section + global flags) - packages/cli/prompts/usage.md (`ocas prompt usage` output) - packages/core/README.md (Closure & Bundles API surface) - .cards/cli.md (bundle commands + --store architecture note)
5.8 KiB
@ocas/core
Core CAS engine — hashing, schema, store, verify, bootstrap.
Overview
@ocas/core is the foundation of the ocas monorepo. It defines content-addressed nodes (CasNode), the Store interface, XXH64-based hashing with deterministic CBOR, JSON Schema registration and validation (including cas_ref links between nodes), bootstrap seeding, and integrity verification.
Other packages build on this layer: @ocas/fs provides persistence, and @ocas/cli exposes store operations on the command line.
Dependencies: ajv, cborg, liquidjs, xxhash-wasm
Installation
pnpm add @ocas/core
API
All symbols below are exported from src/index.ts.
Types
/** 13-character uppercase Crockford Base32 (XXH64) */
type Hash = string;
type CasNode<T = unknown> = {
type: Hash;
payload: T;
timestamp: number; // Unix epoch ms
};
type Store = { cas: CasStore; var: VarStore; tag: TagStore; };
type JSONSchema = Record<string, unknown>;
type BootstrapCapableStore = Store & {
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash>;
};
Hashing
function computeHash(typeHash: Hash, payload: unknown): Promise<Hash>;
function computeSelfHash(payload: unknown): Promise<Hash>;
function cborEncode(value: unknown): Uint8Array;
computeHash — XXH64(utf8(typeHash) ++ CBOR(payload)) for normal nodes.
computeSelfHash — XXH64(CBOR(payload)) for bootstrap nodes where type === hash.
Bootstrap
const BOOTSTRAP_STORE: unique symbol;
async function bootstrap(store: Store): Promise<Hash>;
Writes the meta-schema seed node (idempotent). Requires a BootstrapCapableStore (e.g. from createMemoryStore()).
Schema
class SchemaValidationError extends Error;
async function putSchema(store: Store, jsonSchema: JSONSchema): Promise<Hash>;
function getSchema(store: Store, typeHash: Hash): JSONSchema | null;
function validate(store: Store, node: CasNode): boolean;
function refs(store: Store, node: CasNode): Hash[];
function walk(
store: Store,
rootHash: Hash,
visitor: (hash: Hash, node: CasNode) => void,
): void;
putSchema— stores a schema typed by the meta-schema; returned hash is thetypeHashfor conforming payloads.refs— collects allformat: "cas_ref"values in the payload per schema shape.walk— BFS fromrootHash, followingcas_refedges; cycles are visited once.
Store
function createMemoryStore(): BootstrapCapableStore;
In-memory Store with type indexing, suitable for tests and ephemeral use.
Verify
async function verify(hash: Hash, node: CasNode): Promise<boolean>;
Recomputes hash from node and compares to hash (self-referencing vs normal rules).
Closure & Bundles
type ClosureResult = {
nodes: Set<Hash>;
vars: Variable[];
tags: Map<Hash, Tag[]>;
};
function computeClosure(store: Store, roots: Hash[]): ClosureResult;
type ExportStats = { nodes: number; vars: number; tags: number };
type ImportOptions = { scope?: string };
type ImportStats = {
nodes: { imported: number; skipped: number };
vars: { created: number; updated: number };
tags: number;
};
async function exportBundle(
store: Store,
roots: Hash[],
outputPath: string,
): Promise<ExportStats>;
async function importBundle(
bundlePath: string,
target: Store,
options?: ImportOptions,
): Promise<ImportStats>;
async function loadBundleStore(bundlePath: string): Promise<Store>;
computeClosure— walkscas_refedges and schema chains from each root, also gathering everyVariablewhosevaluelands in the closure and everyTagattached to an in-closure target.exportBundle— writes a self-contained POSIX-tar archive containingcas/<hash>.bin(CBOR-encoded payloads),vars.jsonl, andtags.jsonl.importBundle— content-addressed merge intotarget. Idempotent: re-importing the same bundle yields zeroimportedand zerocreated.options.scoperewrites the leading@scope/of every imported variable name except@ocas/*builtins.loadBundleStore— convenience that returns an in-memoryStorepopulated from a bundle (for read-only inspection without touching the persistent store).
Example
import {
bootstrap,
createMemoryStore,
putSchema,
refs,
validate,
walk,
} from "@ocas/core";
const store = createMemoryStore();
const metaHash = await bootstrap(store);
const personType = await putSchema(store, {
type: "object",
properties: {
name: { type: "string" },
friend: { type: "string", format: "cas_ref" },
},
required: ["name"],
additionalProperties: false,
});
const aliceHash = await store.put(personType, { name: "Alice" });
const bobHash = await store.put(personType, {
name: "Bob",
friend: aliceHash,
});
const bob = store.get(bobHash)!;
console.log(validate(store, bob)); // true
console.log(refs(store, bob)); // [aliceHash]
walk(store, bobHash, (h) => console.log(h)); // bobHash, aliceHash
Internal Structure
| File | Purpose |
|---|---|
types.ts |
Hash, CasNode, Store |
hash.ts |
computeHash, computeSelfHash |
cbor.ts |
Deterministic CBOR encoding |
bootstrap-capable.ts |
BOOTSTRAP_STORE symbol and capability check |
bootstrap.ts |
Meta-schema seed and bootstrap() |
store.ts |
createMemoryStore() |
mem-store.ts |
Alternate in-memory store (tests only; not exported) |
schema.ts |
Schema put/get/validate, refs, walk |
verify.ts |
Node integrity verification |
closure.ts |
computeClosure — refs + schema chain traversal |
bundle.ts |
exportBundle, importBundle, loadBundleStore (POSIX tar) |
index.ts |
Public exports |
Tests live in src/*.test.ts and tests/.