Files
xiaoju dbfaf01031
CI / check (pull_request) Successful in 2m35s
docs: add export/import to README/cards; tidy bundle.ts imports
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)
2026-06-07 03:19:59 +00:00

7.8 KiB

OCAS

Object Content Addressable Store — self-describing CAS with JSON Schema typed nodes.

Every node has a typed payload: its type field is the hash of a JSON Schema that describes the payload shape. Hashes are 13-character Crockford Base32 strings derived from XXH64 over deterministic CBOR encoding. Payloads can reference other nodes via format: "cas_ref" fields, forming a traversable DAG.

Install

pnpm add -g @ocas/cli

The store is auto-created and bootstrapped on first use — no init command needed.

Requires Node.js >= 22.5.0 (uses built-in node:sqlite)

Quick Start

# Register a schema (schemas are just nodes typed by the meta-schema)
echo '{
  "type": "object",
  "properties": { "title": { "type": "string" }, "done": { "type": "boolean" } },
  "required": ["title", "done"],
  "additionalProperties": false
}' | ocas put @ocas/schema -p
# → { "type": "...", "value": "1ABC2DEF34567" }

# Give it a friendly name
ocas var set @todo/schema 1ABC2DEF34567

# Store a todo item
echo '{ "title": "Buy milk", "done": false }' | ocas put @todo/schema -p
# → { "type": "...", "value": "9XYZ8WVU76543" }

# Retrieve it
ocas get 9XYZ8WVU76543

# Verify integrity + schema validation
ocas verify 9XYZ8WVU76543

Envelope Format

Every command outputs a { type, value } JSON envelope. type is the hash of the result schema, value is the payload. This makes output self-describing and composable:

# Pipe any command's output into render
ocas put @ocas/schema schema.json | ocas render -p

# Or use the shorthand -r flag
ocas get 9XYZ8WVU76543 -r

# Extract values with jq
ocas list --type @ocas/schema | jq -r '.value[].hash'

render is the only command that emits raw text instead of an envelope.

Commands

Store & Retrieve

ocas put <type> <file>          # store a node, returns its hash
ocas put <type> -p              # read payload from stdin
ocas get <hash>                 # retrieve a node
ocas has <hash>                 # check if a node exists
ocas hash <type> <file>         # compute hash without storing
ocas verify <hash>              # integrity check + schema validation

Graph Traversal

ocas refs <hash>                # list direct cas_ref edges
ocas walk <hash>                # recursive DAG traversal
ocas walk <hash> --format tree  # tree-view output

Listing & Querying

ocas list --type <hash|name>    # list nodes by type
ocas list-schema                # list all schemas
ocas list-meta                  # list meta-schema hashes

All list commands support sorting and pagination:

ocas list --type todo/schema --sort updated --desc --limit 20
ocas list --type todo/schema --offset 20 --limit 20   # page 2
ocas list-schema --sort created --limit 50

Variables

Variables are mutable pointers to immutable data — like git branches pointing to commits.

Names must follow @scope/name format (e.g. @myapp/config). The @ocas/* scope is reserved.

ocas var set @myapp/config <hash>         # bind a name to a hash
ocas var get @myapp/config                # look up current binding
ocas var delete @myapp/config             # remove binding
ocas var list [prefix]                    # list variables (prefix filter)
ocas var list @myapp/ --tag env:prod      # filter by scope prefix and tag
ocas var history @myapp/config            # show last 10 values (LRU)

Tags & labels — attach metadata to variables:

ocas var set @myapp/config <hash> --tag env:prod --tag pinned
ocas var tag @myapp/config --schema <hash> status:active   # add tag
ocas var tag @myapp/config --schema <hash> :status          # remove tag

Any command that takes a hash also accepts a variable name:

ocas get @myapp/config         # resolves to the bound hash
ocas put @ocas/schema s.json   # @ocas/schema is a builtin variable

Templates & Rendering

Bind a LiquidJS template to a schema, then render nodes of that type:

# Set a template
ocas template set <schema-hash> --inline "Todo: {{ payload.title }} [{{ payload.done }}]"

# Render a node (uses the template for its type, falls back to YAML)
ocas render <hash>

# Render from a pipe (any envelope)
ocas gc | ocas render -p

# Inline render shorthand
ocas get <hash> -r

Render options for recursive reference expansion:

ocas render <hash> --resolution 3    # max recursion depth
ocas render <hash> --decay 0.5       # depth decay factor
ocas render <hash> --epsilon 0.01    # cutoff threshold

Garbage Collection

ocas gc                # collect unreachable nodes
ocas gc | ocas render -p   # human-readable stats

Nodes reachable from any variable binding are kept; everything else is swept.

Bundles (Export / Import)

Pack the transitive CAS closure of one or more roots into a self-contained tar archive that can be moved between stores:

# Export a closure (nodes + schemas + variables + tags reachable from roots)
ocas export @myapp/config -o myapp.tar
ocas export @myapp/config @myapp/users -o myapp.tar     # multiple roots
ocas export 1ABC2DEF34567 -o snapshot.tar               # roots can be hashes

# Import a bundle into the current store (idempotent — content-addressed dedup)
ocas import myapp.tar
ocas import myapp.tar --scope @prod      # remap @myapp/* → @prod/* on import

# Open a bundle as a read-only store without unpacking it
ocas get @myapp/config --store myapp.tar
ocas walk @myapp/config --store myapp.tar
ocas var list --store myapp.tar

--store <bundle.tar> works with all read-only commands (get, has, walk, refs, list, var list, var get, …). Write commands (put, tag, gc, import, var set, …) are rejected with a clear error.

Global Flags

Flag Description
--home <path> Store directory (default: $OCAS_HOME or ~/.ocas)
--var-db <path> Variable database path
--store <bundle.tar> Open a bundle file as a read-only store (write commands rejected)
--json Compact single-line JSON output
--pipe, -p Read from stdin
--render, -r Render output inline
--sort created|updated Sort key for list commands (default: created)
--limit <n> Max results (default: 100)
--offset <n> Skip first N results (default: 0)
--desc Sort descending

Architecture

  ┌───────────┐
  │ @ocas/cli │    CLI interface
  └─────┬─────┘
        │
  ┌───────────┐
  │ @ocas/fs  │    Filesystem store (CBOR + SQLite)
  └─────┬─────┘
        │
  ┌────────────┐
  │ @ocas/core │   Hashing, schemas, validation, bootstrap
  └────────────┘

Using as a Library

pnpm add @ocas/core              # in-memory store
pnpm add @ocas/core @ocas/fs     # + filesystem persistence
import { bootstrap, createMemoryStore, putSchema } from "@ocas/core";

const store = createMemoryStore();
await bootstrap(store);

const typeHash = await putSchema(store, {
  type: "object",
  properties: { message: { type: "string" } },
  required: ["message"],
  additionalProperties: false,
});

const hash = await store.put(typeHash, { message: "hello" });
const node = store.get(hash);

For filesystem persistence, use @ocas/fs:

import { openStore } from "@ocas/fs";

const store = await openStore("/path/to/store");

See individual package READMEs for full API docs: @ocas/core · @ocas/fs · @ocas/cli

Development

git clone <repo-url> && cd ocas
pnpm install
pnpm run build     # tsc --build
pnpm test          # run all tests
pnpm run check     # biome lint
pnpm run format    # biome format

License

MIT