diff --git a/.cards/bootstrap.md b/.cards/bootstrap.md index 2481651..afbe6ab 100644 --- a/.cards/bootstrap.md +++ b/.cards/bootstrap.md @@ -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 diff --git a/.cards/cli.md b/.cards/cli.md index a3549f3..88d8f46 100644 --- a/.cards/cli.md +++ b/.cards/cli.md @@ -31,7 +31,7 @@ ocas hash # compute hash without storing ocas verify # check integrity + schema validity ocas refs # list direct ocas_ref edges ocas walk # recursive DAG traversal -ocas list --type # list nodes by type +ocas list --type # 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 ` | 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 ` is registered, `ocas get myapp/config` resolves to that hash. diff --git a/.cards/render-system.md b/.cards/render-system.md index e66dcbd..59ece2d 100644 --- a/.cards/render-system.md +++ b/.cards/render-system.md @@ -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 diff --git a/.cards/schema.md b/.cards/schema.md index 56cc8bd..7f144ad 100644 --- a/.cards/schema.md +++ b/.cards/schema.md @@ -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. diff --git a/.cards/variable.md b/.cards/variable.md index 548b204..8e2ffe0 100644 --- a/.cards/variable.md +++ b/.cards/variable.md @@ -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/` 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/` 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 diff --git a/CLAUDE.md b/CLAUDE.md index 6651001..6b0ee94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index 304441b..42c60c0 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ raw (non-envelope) text. ``` ``` -Usage: ocas [--store ] [--json] [args] +Usage: ocas [--home ] [--json] [args] Commands (all emit a { type, value } envelope unless noted): put Store node (value = hash) (@output/put) @@ -120,7 +120,7 @@ Commands (all emit a { type, value } envelope unless noted): hash Compute hash without storing (@output/hash) render [options] Render node as text (raw output) render --pipe/-p [options] Render a piped envelope (raw output) - list --type Hashes for a type (value = list) (@output/list) + list --type 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 Store directory (default: ~/.ocas) - --json Compact JSON output - --pipe, -p Read a { type, value } envelope from stdin for render + --home 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 ` resolve the same way. There is no separate alias +concept — every name lookup queries the variable store. + ### Pipe examples ```bash diff --git a/packages/cli/README.md b/packages/cli/README.md index a248b2a..8578935 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -31,15 +31,15 @@ bun packages/cli-ocas/src/index.ts [args] ## CLI Usage ``` -Usage: ocas [--store ] [--json] [args] +Usage: ocas [--home ] [--json] [args] ``` ### Global flags | Flag | Description | |------|-------------| -| `--store ` | Store directory (default: `~/.uncaged/ocas`) | -| `--var-db ` | Variable database path (default: `/variables.db`) | +| `--home ` | Store directory (default: `$OCAS_HOME` or `~/.ocas`) | +| `--var-db ` | Variable database path (default: `/variables.db`) | | `--json` | Compact (single-line) JSON output | ### Envelope format @@ -71,7 +71,7 @@ raw, non-envelope text. | `hash ` | computed hash (string) | `@output/hash` | | `render [options]` | raw text (no envelope) | — | | `render --pipe/-p [options]` | raw text from piped envelope | — | -| `list --type ` | hashes (string[]) | `@output/list` | +| `list --type ` | hashes (string[]) | `@output/list` | | `var set [--tag ...]` | variable object | `@output/var-set` | | `var get --schema ` | variable object | `@output/var-get` | | `var delete [--schema ]` | 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 ` 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 +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 ` | +| Store directory | `~/.ocas` | `--home ` or `OCAS_HOME` env var | No config file is read; all behavior is controlled via flags and command arguments. diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a87ed80..4d5651f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -156,7 +156,7 @@ async function readStdinJson(): Promise { /** * 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 { const fullPath = resolve(storePath); @@ -234,9 +234,9 @@ function parseTagsLabels(args: string[]): { async function cmdPut(args: string[]): Promise { 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 \n ocas put --pipe/-p", ); @@ -244,7 +244,7 @@ async function cmdPut(args: string[]): Promise { 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 { async function cmdHash(args: string[]): Promise { 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 \n ocas hash --pipe/-p", ); @@ -416,7 +416,7 @@ async function cmdHash(args: string[]): Promise { 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 { die("Usage: ocas var get --schema "); } - 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 { 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 { die("Usage: ocas var tag --schema "); } - 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 { 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 { async function cmdList(_args: string[]): Promise { const typeFlag = flags.type; if (typeof typeFlag !== "string") - die("Usage: ocas list --type "); + die("Usage: ocas list --type "); const { store, varStore } = await openStoreAndVarStore(); try { const typeHash = resolveHash(typeFlag, varStore); @@ -994,7 +990,7 @@ Commands: hash Compute hash without storing (@ocas/output/hash) render [options] Render node as text with resolution decay (raw output) render --pipe/-p [options] Render { type, value } from stdin (raw output) - list --type List hashes for a type (value=string[]) (@ocas/output/list) + list --type 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 [--tag ...] Create/update a variable (@ocas/output/var-set) diff --git a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap index 74bc3c7..e3fb0d4 100644 --- a/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap +++ b/packages/cli/tests/__snapshots__/edge-cases.test.ts.snap @@ -23,7 +23,7 @@ Commands: hash Compute hash without storing (@ocas/output/hash) render [options] Render node as text with resolution decay (raw output) render --pipe/-p [options] Render { type, value } from stdin (raw output) - list --type List hashes for a type (value=string[]) (@ocas/output/list) + list --type 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 [--tag ...] Create/update a variable (@ocas/output/var-set) diff --git a/packages/cli/tests/variable.test.ts b/packages/cli/tests/variable.test.ts index 02f2914..54fd6c6 100644 --- a/packages/cli/tests/variable.test.ts +++ b/packages/cli/tests/variable.test.ts @@ -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);