docs: unify alias→variable, refactor var commands

- All docs/cards: alias → variable name
- CLAUDE.md: add Architecture Notes section
- README: fix --store→--home, add variable names section
- CLI: var commands use openStoreAndVarStore(), no double bootstrap
- Tests updated for bootstrap-seeded variables

Fixes #20
This commit is contained in:
2026-06-01 12:38:25 +00:00
parent d47f88a59d
commit a62e18a189
11 changed files with 86 additions and 45 deletions
+2 -2
View File
@@ -24,13 +24,13 @@ This is the only node in the entire store where `type === hash`. It is the root
## What Bootstrap Registers
`bootstrap(store)` is called once when a [[Store]] is opened (and is idempotent). It registers:
`bootstrap(store, varStore?)` is called once when a [[Store]] is opened (and is idempotent). It registers:
1. **The meta-schema** — defines the structure of all schemas (allowed JSON Schema keywords)
2. **Primitive type schemas**`@ocas/string`, `@ocas/number`, `@ocas/object`, `@ocas/array`, `@ocas/bool`
3. **Output schemas** — 18 `@ocas/output/*` schemas for [[Render System|CLI envelope]] types
All of these are stored as regular CAS nodes, addressable by hash. The `@ocas/*` aliases are convenience names resolved at runtime.
All of these are stored as regular CAS nodes, addressable by hash. When a [[Variable|varStore]] is provided, bootstrap also writes each `@ocas/*` builtin name → hash binding into the variable store, making them resolvable like any other variable.
## Idempotency
+5 -3
View File
@@ -31,7 +31,7 @@ ocas hash <type> <file|--pipe> # compute hash without storing
ocas verify <hash> # check integrity + schema validity
ocas refs <hash> # list direct ocas_ref edges
ocas walk <hash> # recursive DAG traversal
ocas list --type <hash|alias> # list nodes by type
ocas list --type <hash|name> # list nodes by type
ocas list-schema # list all schema hashes
ocas list-meta # list meta-schema hashes
ocas gc # garbage collection
@@ -72,12 +72,14 @@ ocas render --pipe/-p [options]
| `--inline <text>` | Inline text content for `template set` |
| `--format tree` | Tree display for `walk` |
## Type Aliases
## Variable Names
The CLI resolves `@ocas/*` aliases to hashes automatically:
The CLI resolves `@ocas/*` variable names to hashes automatically. All commands that accept a hash argument also accept a variable name — `resolveHash()` queries the [[Variable]] store and returns the bound hash:
```bash
ocas put @ocas/object data.json # resolves @ocas/object → hash
ocas put @ocas/schema schema.json # auto-routes to putSchema()
ocas list --type @ocas/schema # list all schemas
```
User-defined variable names work the same way — once `ocas var set myapp/config <hash>` is registered, `ocas get myapp/config` resolves to that hash.
+1 -1
View File
@@ -28,7 +28,7 @@ ocas gc | ocas render -p
ocas list --type @ocas/schema | ocas render -p
```
`wrapEnvelope(store, alias, value)` creates an envelope by resolving the alias to its schema hash.
`wrapEnvelope(store, name, value)` creates an envelope by resolving the variable name to its schema hash.
## Templates
+2 -2
View File
@@ -51,10 +51,10 @@ When the store sees `ocas_ref`, it knows this field is a graph edge — not just
## Namespace
Alias names starting with `@ocas/` are reserved for the system. Built-in aliases:
Variable names starting with `@ocas/` are reserved for the system. Built-in names:
- `@ocas/schema` — the meta-schema (self-referencing)
- `@ocas/string`, `@ocas/number`, `@ocas/object`, `@ocas/array`, `@ocas/bool` — primitive types
- `@ocas/output/*` — [[Render System|envelope]] schemas for CLI output
Users define their own schemas freely — the only restriction is the `@ocas/` prefix.
These names are written to the [[Variable]] store during [[Bootstrap]] and resolve to their hashes via the same lookup that handles user-defined variables. Users define their own schemas freely — the only restriction is the `@ocas/` prefix.
+5 -1
View File
@@ -50,7 +50,11 @@ Tags and labels share a unified command (`var tag`):
## Namespace Protection
Variable names starting with `@ocas/` are reserved for internal use (e.g. `@ocas/template/text/<hash>` for [[Render System|template]] storage). The CLI rejects user attempts to write to this namespace.
Variable names starting with `@ocas/` are reserved for internal use. [[Bootstrap]] writes the builtin schema bindings (`@ocas/schema`, `@ocas/string`, `@ocas/output/*`, …) into the variable store, and `@ocas/template/text/<hash>` is used for [[Render System|template]] storage. The CLI rejects user attempts to write to this namespace.
## Names as Hash Inputs
Every CLI command that takes a hash argument also accepts a variable name. The `resolveHash()` helper checks whether the input matches the 13-char hash format; if not, it queries the variable store by exact name and returns the first match's value. This unifies builtin schema names and user-defined variables under a single resolution path — there is no separate "alias" concept.
## Role in Garbage Collection
+8
View File
@@ -59,6 +59,14 @@ bun run format # Biome format (auto-fix)
- `Hash` — 13-character uppercase Crockford Base32 string (XXH64)
- `CasNode` — content-addressed node with schema
- `Store` — abstract storage interface (get/put)
- `VariableStore` — SQLite-backed mutable bindings (name → hash)
### Architecture Notes
- **No "alias" concept** — every name resolution flows through the `VariableStore`. Builtin schemas (`@ocas/schema`, `@ocas/string`, `@ocas/output/*`, …) are registered as variables during `bootstrap(store, varStore)`, alongside user-defined variables created via `ocas var set`.
- **`bootstrap(store, varStore?)`** writes builtin name → hash bindings into the varStore when one is provided; called automatically by `openStore()` and `openStoreAndVarStore()`.
- **`resolveHash(input, varStore)`** is the unified hash/name resolver in the CLI. If `input` matches the 13-char hash format it is returned as-is; otherwise the varStore is queried by exact name. This means every CLI command that accepts a hash argument also accepts a variable name (schema names, user vars, etc.).
- **`openStoreAndVarStore()`** in the CLI opens both stores and bootstraps once; prefer it over separate `openStore()` + `createVariableStore()` to avoid double bootstrap.
### Internal Dependencies
+13 -5
View File
@@ -108,7 +108,7 @@ raw (non-envelope) text.
```
```
Usage: ocas [--store <path>] [--json] <command> [args]
Usage: ocas [--home <path>] [--json] <command> [args]
Commands (all emit a { type, value } envelope unless noted):
put <type-hash> <file.json> Store node (value = hash) (@output/put)
@@ -120,7 +120,7 @@ Commands (all emit a { type, value } envelope unless noted):
hash <type-hash> <file.json> Compute hash without storing (@output/hash)
render <hash> [options] Render node as text (raw output)
render --pipe/-p [options] Render a piped envelope (raw output)
list --type <hash-or-alias> Hashes for a type (value = list) (@output/list)
list --type <hash-or-name> Hashes for a type (value = list) (@output/list)
list-meta Meta-schema hashes (@output/list-meta)
list-schema All schema hashes (@output/list-schema)
var set|get|delete|tag|list ... Variable CRUD (@output/var-*)
@@ -128,11 +128,19 @@ Commands (all emit a { type, value } envelope unless noted):
gc Garbage collection (@output/gc)
Flags:
--store <path> Store directory (default: ~/.ocas)
--json Compact JSON output
--pipe, -p Read a { type, value } envelope from stdin for render
--home <path> Store directory (default: $OCAS_HOME or ~/.ocas)
--json Compact JSON output
--pipe, -p Read a { type, value } envelope from stdin for render
```
### Variable names
Any command that takes a hash also accepts a variable name. Builtin schemas
(`@ocas/schema`, `@ocas/string`, `@ocas/object`, `@ocas/output/*`, …) are
registered in the variable store during bootstrap; user variables created via
`ocas var set <name> <hash>` resolve the same way. There is no separate alias
concept — every name lookup queries the variable store.
### Pipe examples
```bash
+18 -5
View File
@@ -31,15 +31,15 @@ bun packages/cli-ocas/src/index.ts <command> [args]
## CLI Usage
```
Usage: ocas [--store <path>] [--json] <command> [args]
Usage: ocas [--home <path>] [--json] <command> [args]
```
### Global flags
| Flag | Description |
|------|-------------|
| `--store <path>` | Store directory (default: `~/.uncaged/ocas`) |
| `--var-db <path>` | Variable database path (default: `<store>/variables.db`) |
| `--home <path>` | Store directory (default: `$OCAS_HOME` or `~/.ocas`) |
| `--var-db <path>` | Variable database path (default: `<home>/variables.db`) |
| `--json` | Compact (single-line) JSON output |
### Envelope format
@@ -71,7 +71,7 @@ raw, non-envelope text.
| `hash <type-hash> <file.json>` | computed hash (string) | `@output/hash` |
| `render <hash> [options]` | raw text (no envelope) | — |
| `render --pipe/-p [options]` | raw text from piped envelope | — |
| `list --type <hash-or-alias>` | hashes (string[]) | `@output/list` |
| `list --type <hash-or-name>` | hashes (string[]) | `@output/list` |
| `var set <name> <hash> [--tag ...]` | variable object | `@output/var-set` |
| `var get <name> --schema <hash>` | variable object | `@output/var-get` |
| `var delete <name> [--schema <hash>]` | variable or variable[] | `@output/var-delete` |
@@ -117,6 +117,19 @@ ocas gc | ocas render -p
ocas list --type @schema | ocas render -p
```
### Variable names as hash arguments
Every command that takes a hash also accepts a variable name. Builtin schema names like `@ocas/schema`, `@ocas/string`, `@ocas/output/*` are registered in the variable store during bootstrap; user variables created via `ocas var set <name> <hash>` resolve the same way:
```bash
ocas put @ocas/object data.json # @ocas/object → builtin schema hash
ocas list --type @ocas/schema # filter by builtin schema
ocas var set myapp/config <hash>
ocas get myapp/config # resolves to the user-bound hash
```
There is no separate alias system — names are just variables.
### Templates
`template` commands manage the LiquidJS template bound to a schema (stored as a
@@ -142,6 +155,6 @@ There is no separate `src/` module tree; the CLI is a single entry file. Tests (
| Setting | Default | Override |
|---------|---------|----------|
| Store directory | `~/.uncaged/ocas` | `--store <path>` |
| Store directory | `~/.ocas` | `--home <path>` or `OCAS_HOME` env var |
No config file is read; all behavior is controlled via flags and command arguments.
+13 -17
View File
@@ -156,7 +156,7 @@ async function readStdinJson(): Promise<unknown> {
/**
* Open the filesystem-backed CAS store.
* Automatically creates directory and bootstraps if needed.
* If a varStore is provided, builtin schema aliases are written to it during bootstrap.
* If a varStore is provided, builtin schema variables are written to it during bootstrap.
*/
async function openStore(varStore?: VariableStore): Promise<Store> {
const fullPath = resolve(storePath);
@@ -234,9 +234,9 @@ function parseTagsLabels(args: string[]): {
async function cmdPut(args: string[]): Promise<void> {
const isPipe = flags.pipe === true || flags.p === true;
const typeHashOrAlias = args[0];
const typeHashOrName = args[0];
const file = isPipe ? undefined : args[1];
if (!typeHashOrAlias || (!isPipe && !file))
if (!typeHashOrName || (!isPipe && !file))
die(
"Usage: ocas put <type-hash> <file.json>\n ocas put <type-hash> --pipe/-p",
);
@@ -244,7 +244,7 @@ async function cmdPut(args: string[]): Promise<void> {
die("Cannot use --pipe/-p with a file argument. Use one or the other.");
const { store, varStore } = await openStoreAndVarStore();
try {
const typeHash = resolveHash(typeHashOrAlias, varStore);
const typeHash = resolveHash(typeHashOrName, varStore);
const payload = isPipe
? await readStdinJson()
: readJsonFile(file as string);
@@ -406,9 +406,9 @@ async function cmdWalk(args: string[]): Promise<void> {
async function cmdHash(args: string[]): Promise<void> {
const isPipe = flags.pipe === true || flags.p === true;
const typeHashOrAlias = args[0];
const typeHashOrName = args[0];
const file = isPipe ? undefined : args[1];
if (!typeHashOrAlias || (!isPipe && !file))
if (!typeHashOrName || (!isPipe && !file))
die(
"Usage: ocas hash <type-hash> <file.json>\n ocas hash <type-hash> --pipe/-p",
);
@@ -416,7 +416,7 @@ async function cmdHash(args: string[]): Promise<void> {
die("Cannot use --pipe/-p with a file argument. Use one or the other.");
const { store, varStore } = await openStoreAndVarStore();
try {
const typeHash = resolveHash(typeHashOrAlias, varStore);
const typeHash = resolveHash(typeHashOrName, varStore);
const payload = isPipe
? await readStdinJson()
: readJsonFile(file as string);
@@ -619,8 +619,7 @@ async function cmdVarGet(args: string[]): Promise<void> {
die("Usage: ocas var get <name> --schema <hash-or-name>");
}
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
const { store, varStore } = await openStoreAndVarStore();
try {
const schema = resolveHash(schemaInput, varStore);
@@ -649,8 +648,7 @@ async function cmdVarDelete(args: string[]): Promise<void> {
die("The @ocas/ namespace is reserved and cannot be modified directly.");
}
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
const { store, varStore } = await openStoreAndVarStore();
try {
if (schemaInput !== undefined) {
@@ -692,8 +690,7 @@ async function cmdVarTag(args: string[]): Promise<void> {
die("Usage: ocas var tag <name> --schema <hash-or-name> <operations...>");
}
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
const { store, varStore } = await openStoreAndVarStore();
try {
const schema = resolveHash(schemaInput, varStore);
@@ -728,8 +725,7 @@ async function cmdVarList(args: string[]): Promise<void> {
const schemaInput = flags.schema as string | undefined;
const tagFlags = flags.tag;
const store = await openStore();
const varStore = createVariableStore(resolve(varDbPath), store);
const { store, varStore } = await openStoreAndVarStore();
try {
const schema =
@@ -950,7 +946,7 @@ async function cmdGc(_args: string[]): Promise<void> {
async function cmdList(_args: string[]): Promise<void> {
const typeFlag = flags.type;
if (typeof typeFlag !== "string")
die("Usage: ocas list --type <hash-or-alias>");
die("Usage: ocas list --type <hash-or-name>");
const { store, varStore } = await openStoreAndVarStore();
try {
const typeHash = resolveHash(typeFlag, varStore);
@@ -994,7 +990,7 @@ Commands:
hash <type-hash> <file.json|--pipe> Compute hash without storing (@ocas/output/hash)
render <hash> [options] Render node as text with resolution decay (raw output)
render --pipe/-p [options] Render { type, value } from stdin (raw output)
list --type <hash-or-alias> List hashes for a type (value=string[]) (@ocas/output/list)
list --type <hash-or-name> List hashes for a type (value=string[]) (@ocas/output/list)
list-meta List meta-schema hashes (value=string[]) (@ocas/output/list-meta)
list-schema List all schema hashes (value=string[]) (@ocas/output/list-schema)
var set <name> <hash> [--tag <tag>...] Create/update a variable (@ocas/output/var-set)
@@ -23,7 +23,7 @@ Commands:
hash <type-hash> <file.json|--pipe> Compute hash without storing (@ocas/output/hash)
render <hash> [options] Render node as text with resolution decay (raw output)
render --pipe/-p [options] Render { type, value } from stdin (raw output)
list --type <hash-or-alias> List hashes for a type (value=string[]) (@ocas/output/list)
list --type <hash-or-name> List hashes for a type (value=string[]) (@ocas/output/list)
list-meta List meta-schema hashes (value=string[]) (@ocas/output/list-meta)
list-schema List all schema hashes (value=string[]) (@ocas/output/list-schema)
var set <name> <hash> [--tag <tag>...] Create/update a variable (@ocas/output/var-set)
+18 -8
View File
@@ -498,13 +498,14 @@ describe("var list", () => {
const hash2 = await createTestNode(store, typeHash, { test: "data2" });
const hash3 = await createTestNode(store, typeHash, { test: "data3" });
// Create three variables
await runCli("var", "set", "a", hash1);
await runCli("var", "set", "b", hash2);
await runCli("var", "set", "c", hash3);
// Create three variables (use a prefix to filter out builtin @ocas/* vars
// that bootstrap writes into the varStore)
await runCli("var", "set", "test/a", hash1);
await runCli("var", "set", "test/b", hash2);
await runCli("var", "set", "test/c", hash3);
// List all
const { stdout, exitCode } = await runCli("var", "list");
// List all under our prefix
const { stdout, exitCode } = await runCli("var", "list", "test/");
expect(exitCode).toBe(0);
@@ -538,10 +539,19 @@ describe("var list", () => {
test("filter by schema", async () => {
const store = createFsStore(storePath);
const typeHash1 = await getBootstrapHash(store);
const typeHash2 = await putSchema(store, { title: "Test", type: "object" });
const bootstrapHash = await getBootstrapHash(store);
const typeHash1 = await putSchema(store, {
title: "TypeA",
type: "object",
});
const typeHash2 = await putSchema(store, {
title: "TypeB",
type: "object",
});
const hash1 = await createTestNode(store, typeHash1, { test: "data1" });
const hash2 = await createTestNode(store, typeHash2, { test: "data2" });
// bootstrapHash is the meta-schema, used implicitly when bootstrap runs
void bootstrapHash;
// Create variables with different schemas
await runCli("var", "set", "a", hash1);