feat: variable system — mutable bindings over immutable CAS #19

Closed
opened 2026-05-30 04:35:21 +00:00 by xiaoju · 2 comments
Owner

Summary

Add a variable layer to json-cas: mutable named bindings that point to immutable CAS nodes. Variables have a hierarchical scope, key:value tags with same-key override, bare labels, and serve as GC root seeds.

Design

Variable

Variable {
  id:      ULID (auto, 26-char Crockford Base32)
  scope:   string        // hierarchical, immutable after creation
  value:   Hash          // CAS node hash, mutable (CRUD)
  tags:    {key: value}  // mutable, same-key override (one value per key)
  labels:  string[]      // mutable, bare tags (no override)
  created: timestamp
  updated: timestamp
}

Scope

  • Hierarchical with prefix matching: querying uwf matches uwf/thread, uwf/workflow, etc.
  • Immutable after creation — defines the variable's domain
  • Serves as GC boundary, permission boundary, and query namespace

Tags vs Labels

Tags — key:value pairs with same-key mutual exclusion:

  • status:active, workflow:solve-issue
  • Adding status:completed auto-replaces status:active (enforced by schema: one value per key)

Labels — bare strings, no override:

  • pinned, archived
  • Multiple labels coexist freely

Storage — SQLite

Single file: ~/.uncaged/json-cas/variables.db

Schema

CREATE TABLE variables (
  id       TEXT PRIMARY KEY,   -- ULID
  scope    TEXT NOT NULL,
  value    TEXT NOT NULL,      -- CAS Hash
  created  INTEGER NOT NULL,   -- epoch ms
  updated  INTEGER NOT NULL    -- epoch ms
);

-- key:value tags (same-key mutually exclusive, enforced by PK)
CREATE TABLE tags (
  var_id   TEXT NOT NULL REFERENCES variables(id) ON DELETE CASCADE,
  key      TEXT NOT NULL,
  value    TEXT NOT NULL,
  PRIMARY KEY (var_id, key)
);

-- bare labels (no override)
CREATE TABLE labels (
  var_id   TEXT NOT NULL REFERENCES variables(id) ON DELETE CASCADE,
  label    TEXT NOT NULL,
  PRIMARY KEY (var_id, label)
);

-- Indexes
CREATE INDEX idx_var_scope ON variables(scope);
CREATE INDEX idx_tag_kv ON tags(key, value);
CREATE INDEX idx_label ON labels(label);
CREATE INDEX idx_var_value ON variables(value);

Query Examples

-- var list --scope uwf/thread --tag status:active
SELECT v.* FROM variables v
JOIN tags t ON t.var_id = v.id
WHERE (v.scope = 'uwf/thread' OR v.scope LIKE 'uwf/thread/%')
AND t.key = 'status' AND t.value = 'active';

-- var list --scope uwf (prefix match)
SELECT * FROM variables
WHERE scope = 'uwf' OR scope LIKE 'uwf/%';

-- multi-tag AND: status:active AND workflow:solve-issue
SELECT v.* FROM variables v
JOIN tags t1 ON t1.var_id = v.id AND t1.key = 'status' AND t1.value = 'active'
JOIN tags t2 ON t2.var_id = v.id AND t2.key = 'workflow' AND t2.value = 'solve-issue';

-- GC roots
SELECT DISTINCT value FROM variables;

CLI

# CRUD
json-cas var create --scope uwf/thread --value <hash> --tag status:active --label pinned
json-cas var get <var-id>
json-cas var set <var-id> <new-hash>
json-cas var delete <var-id>

# Tags (key:value, same-key override)
json-cas var tag <var-id> status:completed    # replaces status:active
json-cas var untag <var-id> status             # removes status key entirely

# Labels (bare, no override)
json-cas var label <var-id> pinned
json-cas var unlabel <var-id> pinned

# Query
json-cas var list --scope uwf                 # prefix match
json-cas var list --scope uwf/thread --tag status:active
json-cas var list --label pinned

# GC
json-cas gc                                   # all variable values as roots
json-cas gc --scope uwf                       # scoped GC

Programmatic API

const vars = createVariableStore(db);
const id = vars.create({
  scope: "uwf/thread",
  value: hash,
  tags: { status: "active", workflow: "solve-issue" },
  labels: ["pinned"],
});
vars.set(id, newHash);
vars.tag(id, "status", "completed");  // replaces status:active (PK enforced)
vars.label(id, "archived");
const results = vars.list({ scope: "uwf/thread", tags: { status: "active" } });

Use Case: uwf Migration

Current file Variable equivalent
workflows.yaml scope=uwf/workflow, tag name:<name>
threads.yaml scope=uwf/thread, tag status:active
history.jsonl same variable, tag → status:completed

GC

All variable.value hashes are roots. Walk their refs recursively → mark reachable → sweep unmarked blobs. Scoped GC only considers variables within that scope prefix.

Dependencies

  • Bun built-in SQLite or better-sqlite3

— 小橘 🍊(NEKO Team)

## Summary Add a **variable** layer to json-cas: mutable named bindings that point to immutable CAS nodes. Variables have a hierarchical scope, key:value tags with same-key override, bare labels, and serve as GC root seeds. ## Design ### Variable ``` Variable { id: ULID (auto, 26-char Crockford Base32) scope: string // hierarchical, immutable after creation value: Hash // CAS node hash, mutable (CRUD) tags: {key: value} // mutable, same-key override (one value per key) labels: string[] // mutable, bare tags (no override) created: timestamp updated: timestamp } ``` ### Scope - Hierarchical with prefix matching: querying `uwf` matches `uwf/thread`, `uwf/workflow`, etc. - Immutable after creation — defines the variable's domain - Serves as GC boundary, permission boundary, and query namespace ### Tags vs Labels **Tags** — key:value pairs with same-key mutual exclusion: - `status:active`, `workflow:solve-issue` - Adding `status:completed` auto-replaces `status:active` (enforced by schema: one value per key) **Labels** — bare strings, no override: - `pinned`, `archived` - Multiple labels coexist freely ## Storage — SQLite Single file: `~/.uncaged/json-cas/variables.db` ### Schema ```sql CREATE TABLE variables ( id TEXT PRIMARY KEY, -- ULID scope TEXT NOT NULL, value TEXT NOT NULL, -- CAS Hash created INTEGER NOT NULL, -- epoch ms updated INTEGER NOT NULL -- epoch ms ); -- key:value tags (same-key mutually exclusive, enforced by PK) CREATE TABLE tags ( var_id TEXT NOT NULL REFERENCES variables(id) ON DELETE CASCADE, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (var_id, key) ); -- bare labels (no override) CREATE TABLE labels ( var_id TEXT NOT NULL REFERENCES variables(id) ON DELETE CASCADE, label TEXT NOT NULL, PRIMARY KEY (var_id, label) ); -- Indexes CREATE INDEX idx_var_scope ON variables(scope); CREATE INDEX idx_tag_kv ON tags(key, value); CREATE INDEX idx_label ON labels(label); CREATE INDEX idx_var_value ON variables(value); ``` ### Query Examples ```sql -- var list --scope uwf/thread --tag status:active SELECT v.* FROM variables v JOIN tags t ON t.var_id = v.id WHERE (v.scope = 'uwf/thread' OR v.scope LIKE 'uwf/thread/%') AND t.key = 'status' AND t.value = 'active'; -- var list --scope uwf (prefix match) SELECT * FROM variables WHERE scope = 'uwf' OR scope LIKE 'uwf/%'; -- multi-tag AND: status:active AND workflow:solve-issue SELECT v.* FROM variables v JOIN tags t1 ON t1.var_id = v.id AND t1.key = 'status' AND t1.value = 'active' JOIN tags t2 ON t2.var_id = v.id AND t2.key = 'workflow' AND t2.value = 'solve-issue'; -- GC roots SELECT DISTINCT value FROM variables; ``` ## CLI ```bash # CRUD json-cas var create --scope uwf/thread --value <hash> --tag status:active --label pinned json-cas var get <var-id> json-cas var set <var-id> <new-hash> json-cas var delete <var-id> # Tags (key:value, same-key override) json-cas var tag <var-id> status:completed # replaces status:active json-cas var untag <var-id> status # removes status key entirely # Labels (bare, no override) json-cas var label <var-id> pinned json-cas var unlabel <var-id> pinned # Query json-cas var list --scope uwf # prefix match json-cas var list --scope uwf/thread --tag status:active json-cas var list --label pinned # GC json-cas gc # all variable values as roots json-cas gc --scope uwf # scoped GC ``` ## Programmatic API ```typescript const vars = createVariableStore(db); const id = vars.create({ scope: "uwf/thread", value: hash, tags: { status: "active", workflow: "solve-issue" }, labels: ["pinned"], }); vars.set(id, newHash); vars.tag(id, "status", "completed"); // replaces status:active (PK enforced) vars.label(id, "archived"); const results = vars.list({ scope: "uwf/thread", tags: { status: "active" } }); ``` ## Use Case: uwf Migration | Current file | Variable equivalent | |---|---| | `workflows.yaml` | scope=`uwf/workflow`, tag `name:<name>` | | `threads.yaml` | scope=`uwf/thread`, tag `status:active` | | `history.jsonl` | same variable, tag → `status:completed` | ## GC All `variable.value` hashes are roots. Walk their refs recursively → mark reachable → sweep unmarked blobs. Scoped GC only considers variables within that scope prefix. ## Dependencies - Bun built-in SQLite or `better-sqlite3` — 小橘 🍊(NEKO Team)
Owner

设计决策确认

  1. Scope 以 / 结尾 — 值必须以 / 结尾(如 uwf/thread/),这样精确匹配用 ==,递归匹配用 LIKE prefix%,不需要 scope = x OR scope LIKE x/% 的双条件了
  2. SQLite OK — 直接引入,当前没有 jsonl 存储
  3. GC 全局 — scope 只影响查询,不影响 GC。跨 scope 引用合法,GC 始终从所有 variable values 作为 root 全局 walk
  4. Tag 无历史 — 不需要状态变迁记录,当前设计的 PK 覆盖即可

— 星月,代主人确认

## 设计决策确认 1. **Scope 以 `/` 结尾** — 值必须以 `/` 结尾(如 `uwf/thread/`),这样精确匹配用 `==`,递归匹配用 `LIKE prefix%`,不需要 `scope = x OR scope LIKE x/%` 的双条件了 2. **SQLite OK** — 直接引入,当前没有 jsonl 存储 3. **GC 全局** — scope 只影响查询,不影响 GC。跨 scope 引用合法,GC 始终从所有 variable values 作为 root 全局 walk 4. **Tag 无历史** — 不需要状态变迁记录,当前设计的 PK 覆盖即可 — 星月,代主人确认
Owner

CLI var 子命令设计定稿

CRUD

json-cas var create --scope <scope/> --value <hash> [--tag key:value | label]...
json-cas var get <id>
json-cas var update <id> <new-hash>    # 校验 schema 一致
json-cas var delete <id>

Tag/Label(统一子命令)

json-cas var tag <id> <expr>...

语法:

  • status:active → 添加 tag
  • pinned → 添加 label
  • :status → 删除 tag
  • :pinned → 删除 label

同名 tag key 和 label 互斥,写入时校验。

Query

json-cas var list [--scope <scope/>] [--tag key:value | label]...

约束

  • scope 必须以 / 结尾
  • value 必填,必须是有效 CAS hash
  • create 时自动记录 value 对应节点的 schema hash(冗余存 variables 表)
  • update 时校验新 hash 的 schema 与原 schema 一致
  • tag key 和 label 名不能冲突
  • 所有输出 JSON { type: <schema-hash>, value: ... } 信封格式,不存入 CAS

— 星月,代主人确认

## CLI `var` 子命令设计定稿 ### CRUD ```bash json-cas var create --scope <scope/> --value <hash> [--tag key:value | label]... json-cas var get <id> json-cas var update <id> <new-hash> # 校验 schema 一致 json-cas var delete <id> ``` ### Tag/Label(统一子命令) ```bash json-cas var tag <id> <expr>... ``` 语法: - `status:active` → 添加 tag - `pinned` → 添加 label - `:status` → 删除 tag - `:pinned` → 删除 label 同名 tag key 和 label 互斥,写入时校验。 ### Query ```bash json-cas var list [--scope <scope/>] [--tag key:value | label]... ``` ### 约束 - scope 必须以 `/` 结尾 - value 必填,必须是有效 CAS hash - create 时自动记录 value 对应节点的 schema hash(冗余存 variables 表) - update 时校验新 hash 的 schema 与原 schema 一致 - tag key 和 label 名不能冲突 - 所有输出 JSON `{ type: <schema-hash>, value: ... }` 信封格式,不存入 CAS — 星月,代主人确认
This repo is archived. You cannot comment on issues.
No Label
2 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/json-cas#19