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)
7.3 KiB
OCAS — Object Content Addressable Store
Overview
OCAS is a self-describing content-addressable store for typed JSON data. Every node has a type field (hash of a JSON Schema) and a payload. Hashes are 13-character Crockford Base32 strings (XXH64 over deterministic CBOR).
All commands output { type, value } JSON envelopes, making them composable via pipes.
Install: pnpm add -g @ocas/cli
Packages: @ocas/core (engine) · @ocas/fs (filesystem store) · @ocas/cli (CLI)
When to Use
- Storing structured, schema-validated JSON data with content addressing
- Building knowledge graphs or DAGs with typed nodes and
cas_refedges - Agent memory, config versioning, or any use case needing immutable data + mutable pointers
- Don't use for: binary blobs, large files, or high-throughput streaming
Quick Start
# Register a schema (from stdin)
echo '{
"type": "object",
"properties": { "title": { "type": "string" }, "done": { "type": "boolean" } },
"required": ["title", "done"],
"additionalProperties": false
}' | ocas put @ocas/schema -p
# → { "type": "...", "value": "<schema-hash>" }
# Name it
ocas var set @todo/schema <schema-hash>
# Store data
echo '{ "title": "Buy milk", "done": false }' | ocas put @todo/schema -p
# Retrieve + verify
ocas get <hash>
ocas verify <hash>
Core Concepts
Hashes
13-char uppercase Crockford Base32 (e.g. 9S7JEYS3FKSDH). Deterministic: same content → same hash.
Envelope Format
Every command outputs { type, value }. type is the hash of the result schema. Pipe any envelope into render -p to render it human-readable.
Variables
Mutable pointers to immutable data (like git branches → commits). All names must follow @scope/name format:
@myapp/config✅@ocas/schema✅ (builtin, read-only)config❌ (no scope)
Templates
LiquidJS templates bound to a schema. render uses the template for the node's type, falling back to YAML.
CLI Reference
Store & Retrieve
ocas put <type> <file> # store node → hash
ocas put <type> -p # read payload from stdin
ocas get <hash> # retrieve node
ocas has <hash> # check existence
ocas hash <type> <file> # compute hash without storing
ocas verify <hash> # integrity + schema validation
Graph Traversal
ocas refs <hash> # direct cas_ref edges
ocas walk <hash> # recursive DAG traversal
ocas walk <hash> --format tree # tree view
Listing & Querying
ocas list --type <hash|name> # list nodes by type
ocas list-schema # all schemas
ocas list-meta # meta-schema hashes
Sorting and pagination:
ocas list --type @todo/schema --sort updated --desc --limit 20
ocas list --type @todo/schema --offset 20 --limit 20 # page 2
Variables
ocas var set @myapp/config <hash> # bind name → hash
ocas var set @myapp/config <hash> --tag env:prod --tag pinned
ocas var get @myapp/config # look up
ocas var delete @myapp/config # remove
ocas var list [prefix] # list (prefix filter)
ocas var list @myapp/ --tag env:prod # filter by scope + tag
ocas var history @myapp/config # last 10 values (LRU)
ocas tag @myapp/config status:active # add tag/label to a target
ocas untag @myapp/config status # remove tag/label by key
Naming rules:
- Format:
@scope/name—@[a-zA-Z][a-zA-Z0-9]*/segments @ocas/*reserved for builtins- Any command accepting a hash also accepts a variable name
Templates & Rendering
ocas template set <schema-hash> --inline "{{ payload.title }}"
ocas template get <schema-hash>
ocas template list
ocas template delete <schema-hash>
ocas render <hash> # render with template (or YAML fallback)
ocas render --pipe/-p # render from piped envelope
ocas get <hash> -r # inline render shorthand
Render options: --resolution N (max depth), --decay N (depth decay), --epsilon N (cutoff).
Garbage Collection
ocas gc # collect unreachable nodes
ocas gc | ocas render -p # human-readable stats
Bundles (Export / Import)
ocas export <root>... -o <bundle.tar> # write closure of roots to tar
ocas export @myapp/config -o myapp.tar
ocas export @myapp/config @myapp/users -o m.tar # multiple roots
ocas import <bundle.tar> # import bundle (idempotent)
ocas import <bundle.tar> --scope @prod # remap @<scope>/* → @prod/*
ocas get <hash> --store <bundle.tar> # read-only access into bundle
export walks refs and schema chains; the resulting tar contains every reachable
CAS node (cas/<hash>.bin, CBOR), every variable whose value is in-closure, and every
tag attached to an in-closure target. import is content-addressed (deduplicates
existing nodes). --scope @new rewrites the leading @scope of imported variable
names except @ocas/* builtins. --store <bundle.tar> opens a bundle as a read-only
store for any inspection command; write commands (put, tag, gc, import,
var set, …) refuse with --store is read-only.
Global Flags
| Flag | Description |
|---|---|
--home <path> |
Store directory (default: $OCAS_HOME or ~/.ocas) |
--store <bundle.tar> |
Open a bundle as a read-only store (write commands rejected) |
--json |
Compact JSON output |
-p, --pipe |
Read from stdin |
-r, --render |
Render output inline |
--sort created|updated |
Sort key (default: created) |
--limit <n> |
Max results (default: 100) |
--offset <n> |
Skip first N (default: 0) |
--desc |
Sort descending |
-o <path> |
Output path (used by export) |
--scope @new |
Variable scope remap (used by import) |
Pipe Composition Patterns
# Store + render in one go
echo '{"title":"test","done":false}' | ocas put @todo/schema -p | ocas render -p
# Or use -r shorthand
ocas get <hash> -r
# List schemas, extract hashes with jq
ocas list --type @ocas/schema | jq -r '.value[].hash'
# Render GC stats
ocas gc | ocas render -p
Library Usage
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" });
For filesystem persistence:
import { openStore } from "@ocas/fs";
const store = await openStore("/path/to/store");
// store.cas / store.var / store.tag
Common Pitfalls
- Variable names without
@scope/— all names must be@scope/nameformat.configalone will be rejected. - Writing to
@ocas/*namespace — reserved for builtins, CLI will reject. - Forgetting
-pfor stdin —ocas put <type>expects a file path; use-pto read from stdin. - Expecting
listto return hashes —listcommands returnListEntry[]with{ hash, created, updated }, not bare hashes. workspace:*in published packages — only onmainbranch; release branches must have fixed versions.