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:
+2
-2
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user