RFC: ocas export / import — CAS closure bundling #83

Closed
opened 2026-06-06 22:17:45 +00:00 by xiaoju · 4 comments
Owner

动机

将一个或多个 CAS 节点及其完整闭包打包成可移植的 tar,用于跨设备传输、离线分享、或对着 bundle 直接跑读命令。

核心概念

Bundle = 只读 OCAS Store(tar 格式)

给定一组根节点,计算闭包(walk refs + schema 链 + template 变量),打包成 tar。接收端可以:

  • --store bundle.tar 对着 bundle 跑任何读命令
  • ocas import 将闭包导入本地 store

Bundle 格式

bundle.tar
├── cas/          # 直接拷贝 FsStore 的原始节点文件(零序列化)
├── vars.jsonl    # 每行一个 {name, value}
└── tags.jsonl    # 每行一个 {target, key, value}
  • CAS 节点直接 copy bin,不做序列化/反序列化
  • 变量和 tags 用 JSONL:人可读、可 grep、跨平台
  • 不含 SQLite 二进制,零兼容问题

闭包计算

给定根集合 R,闭包 C 包含:

  1. 节点链 — 从每个根 BFS walk 所有 refs(复用现有 walk()
  2. Schema 链 — 每个节点的 node.type → schema → meta-schema(复用 gc.ts mark 逻辑)
  3. Template 节点 — 每个 schema 对应的 @ocas/template/text/<type-hash> 变量指向的 CAS 节点
  4. 变量 — 所有 value 指向闭包内节点的变量(含根变量、template 变量、@ocas/* 内建变量)
  5. Tags — 闭包内节点上的所有 tags

命令设计

ocas export

ocas export @myapp/config @myapp/data -o bundle.tar
  • 接受变量名或 hash 作为根
  • 计算闭包,打包 tar
  • 输出汇报:N nodes, N vars, N tags

--store flag(所有读命令)

ocas var list --store bundle.tar       # 查看 bundle 里的变量
ocas get <hash> --store bundle.tar     # 读节点
ocas walk @myapp/config --store bundle.tar
ocas render @myapp/config --store bundle.tar
ocas refs <hash> --store bundle.tar

实现:解压 → JSONL parse + cas/ 文件 → MemoryStore → 注入到命令。
变量名解析(resolveHash)自然可用。

ocas import

ocas import bundle.tar --scope @imported

行为:

  1. 解压 → 读 cas/ 文件 + JSONL
  2. CAS 节点:逐个写入,hash 相同自动跳过(content-addressed)
  3. 变量:scope 重映射(@myapp/name@imported/name),var set 写入
  4. Tags:对导入节点 tag(target, ops)
  5. 输出汇报:
Imported 42 nodes (3 skipped, already exist)
Variables:
  created  @imported/config → 4KNMR2PX8B2YZ
  created  @imported/data   → 7BQST3VW0A1XE
  updated  @imported/schema → 9CDEF1234GHIJ  (was: 1ABCD5678EFGH)
Tags: 5 applied

Scope 重映射规则:

  • @original/name@scope/name(替换 scope 部分)
  • @ocas/* 内建变量不重映射,原样导入
  • --scope 可选 — 不传则保持原变量名
  • 变量名冲突直接覆盖(var set 语义),输出标记 updated vs created

实现计划

改动
@ocas/core computeClosure(store, roots): Set<Hash> — 从 gc.ts mark 阶段抽取
@ocas/core@ocas/fs exportBundle(store, varNames, output) — 闭包 → tar
@ocas/core@ocas/fs importBundle(bundlePath, targetStore, scope?) — tar → store
@ocas/cli ocas export 子命令
@ocas/cli ocas import 子命令
@ocas/cli --store <path> global flag — 解压 → MemoryStore → 注入

开放问题

  1. 压缩 — tar 本身不压缩,是否默认 .tar.gz?或者让用户自己处理?
  2. MemoryStore@ocas/core 是否已有完整的 MemoryStore 实现?若无需新增。
  3. 大 bundle — 节点数量很大时,全部加载到 MemoryStore 是否有内存问题?初期可以不管,后续按需优化。
## 动机 将一个或多个 CAS 节点及其完整闭包打包成可移植的 tar,用于跨设备传输、离线分享、或对着 bundle 直接跑读命令。 ## 核心概念 **Bundle = 只读 OCAS Store(tar 格式)** 给定一组根节点,计算闭包(walk refs + schema 链 + template 变量),打包成 tar。接收端可以: - 用 `--store bundle.tar` 对着 bundle 跑任何读命令 - 用 `ocas import` 将闭包导入本地 store ## Bundle 格式 ``` bundle.tar ├── cas/ # 直接拷贝 FsStore 的原始节点文件(零序列化) ├── vars.jsonl # 每行一个 {name, value} └── tags.jsonl # 每行一个 {target, key, value} ``` - CAS 节点直接 copy bin,不做序列化/反序列化 - 变量和 tags 用 JSONL:人可读、可 grep、跨平台 - 不含 SQLite 二进制,零兼容问题 ## 闭包计算 给定根集合 R,闭包 C 包含: 1. **节点链** — 从每个根 BFS walk 所有 refs(复用现有 `walk()`) 2. **Schema 链** — 每个节点的 `node.type` → schema → meta-schema(复用 gc.ts mark 逻辑) 3. **Template 节点** — 每个 schema 对应的 `@ocas/template/text/<type-hash>` 变量指向的 CAS 节点 4. **变量** — 所有 value 指向闭包内节点的变量(含根变量、template 变量、`@ocas/*` 内建变量) 5. **Tags** — 闭包内节点上的所有 tags ## 命令设计 ### `ocas export` ```bash ocas export @myapp/config @myapp/data -o bundle.tar ``` - 接受变量名或 hash 作为根 - 计算闭包,打包 tar - 输出汇报:N nodes, N vars, N tags ### `--store` flag(所有读命令) ```bash ocas var list --store bundle.tar # 查看 bundle 里的变量 ocas get <hash> --store bundle.tar # 读节点 ocas walk @myapp/config --store bundle.tar ocas render @myapp/config --store bundle.tar ocas refs <hash> --store bundle.tar ``` 实现:解压 → JSONL parse + cas/ 文件 → MemoryStore → 注入到命令。 变量名解析(`resolveHash`)自然可用。 ### `ocas import` ```bash ocas import bundle.tar --scope @imported ``` 行为: 1. 解压 → 读 cas/ 文件 + JSONL 2. CAS 节点:逐个写入,hash 相同自动跳过(content-addressed) 3. 变量:scope 重映射(`@myapp/name` → `@imported/name`),`var set` 写入 4. Tags:对导入节点 `tag(target, ops)` 5. 输出汇报: ``` Imported 42 nodes (3 skipped, already exist) Variables: created @imported/config → 4KNMR2PX8B2YZ created @imported/data → 7BQST3VW0A1XE updated @imported/schema → 9CDEF1234GHIJ (was: 1ABCD5678EFGH) Tags: 5 applied ``` **Scope 重映射规则:** - `@original/name` → `@scope/name`(替换 scope 部分) - `@ocas/*` 内建变量不重映射,原样导入 - `--scope` 可选 — 不传则保持原变量名 - 变量名冲突直接覆盖(`var set` 语义),输出标记 `updated` vs `created` ## 实现计划 | 层 | 改动 | |---|---| | `@ocas/core` | `computeClosure(store, roots): Set<Hash>` — 从 gc.ts mark 阶段抽取 | | `@ocas/core` 或 `@ocas/fs` | `exportBundle(store, varNames, output)` — 闭包 → tar | | `@ocas/core` 或 `@ocas/fs` | `importBundle(bundlePath, targetStore, scope?)` — tar → store | | `@ocas/cli` | `ocas export` 子命令 | | `@ocas/cli` | `ocas import` 子命令 | | `@ocas/cli` | `--store <path>` global flag — 解压 → MemoryStore → 注入 | ## 开放问题 1. **压缩** — tar 本身不压缩,是否默认 `.tar.gz`?或者让用户自己处理? 2. **MemoryStore** — `@ocas/core` 是否已有完整的 MemoryStore 实现?若无需新增。 3. **大 bundle** — 节点数量很大时,全部加载到 MemoryStore 是否有内存问题?初期可以不管,后续按需优化。
Author
Owner

设计简化:去掉 manifest

原来 manifest 记录 roots,但 vars.jsonl 已经包含所有变量(根就是普通变量),ocas var list --store bundle.tar 即可查看。无需额外元数据。

最终 bundle 格式:

bundle.tar
├── cas/          # 节点文件(copy bin)
├── vars.jsonl    # {name, value}
└── tags.jsonl    # {target, key, value}

三个文件,零冗余。

—— 小橘 🍊

设计简化:**去掉 manifest**。 原来 manifest 记录 roots,但 `vars.jsonl` 已经包含所有变量(根就是普通变量),`ocas var list --store bundle.tar` 即可查看。无需额外元数据。 最终 bundle 格式: ``` bundle.tar ├── cas/ # 节点文件(copy bin) ├── vars.jsonl # {name, value} └── tags.jsonl # {target, key, value} ``` 三个文件,零冗余。 —— 小橘 🍊
Author
Owner

修正:去掉 cas/ 子目录,与 ~/.ocas 布局保持一致。

FsStore 的节点文件是平铺在目录根的 <HASH>.bin,没有 cas/ 子目录。bundle 保持同样布局:

bundle.tar
├── <HASH>.bin     # 节点文件(与 ~/.ocas/ 布局一致)
├── ...
├── vars.jsonl     # {name, value}
└── tags.jsonl     # {target, key, value}

这样 import 就是直接把 .bin 拷到 ~/.ocas/,零转换。--store 解压后 openStore(tempDir) 直接能用(JSONL 加载到 MemoryStore 做 var/tag)。

—— 小橘 🍊

修正:**去掉 `cas/` 子目录**,与 `~/.ocas` 布局保持一致。 FsStore 的节点文件是平铺在目录根的 `<HASH>.bin`,没有 `cas/` 子目录。bundle 保持同样布局: ``` bundle.tar ├── <HASH>.bin # 节点文件(与 ~/.ocas/ 布局一致) ├── ... ├── vars.jsonl # {name, value} └── tags.jsonl # {target, key, value} ``` 这样 import 就是直接把 `.bin` 拷到 `~/.ocas/`,零转换。`--store` 解压后 `openStore(tempDir)` 直接能用(JSONL 加载到 MemoryStore 做 var/tag)。 —— 小橘 🍊
Author
Owner

最终 bundle 格式确认:

bundle.tar
├── nodes/         # CAS 节点文件
│   ├── <HASH>.bin
│   └── ...
├── vars.jsonl     # {name, value}
└── tags.jsonl     # {target, key, value}
  • nodes/ 子目录存节点,不平铺(与 #84~/.ocas 布局改造对齐)
  • 不做前缀分桶 — 闭包通常几十到几百节点,分桶过度
  • 不含 type index — 只读场景下 MemoryStore 加载时在内存重建即可
  • 不含 manifest — 根信息在 vars.jsonl

—— 小橘 🍊

最终 bundle 格式确认: ``` bundle.tar ├── nodes/ # CAS 节点文件 │ ├── <HASH>.bin │ └── ... ├── vars.jsonl # {name, value} └── tags.jsonl # {target, key, value} ``` - **`nodes/`** 子目录存节点,不平铺(与 #84 的 `~/.ocas` 布局改造对齐) - **不做前缀分桶** — 闭包通常几十到几百节点,分桶过度 - **不含 type index** — 只读场景下 MemoryStore 加载时在内存重建即可 - **不含 manifest** — 根信息在 `vars.jsonl` 里 —— 小橘 🍊
Author
Owner

开放问题结论

Q1 压缩 — 默认 .tar.gz,node 内置 zlib 零依赖

Q2 MemoryStore@ocas/core 已有完整的 createMemoryStore()(CAS + VarStore + TagStore 三合一)。--store 实现方案:解压 tar → 读 .bin 到 MemoryStore CAS → 读 vars.jsonl / tags.jsonl 写入 VarStore / TagStore → bootstrap → 注入命令。不需要新建任何东西。

Q3 大 bundle 内存 — FsStore 自身也是启动时全量加载到内存 Map(loadDir() line 206-207),所以 MemoryStore 方案并不比正常使用差。初期不用管,上万节点时统一改按需加载(见 #85)。

三个问题全部关闭,RFC 设计完备。

—— 小橘 🍊

## 开放问题结论 **Q1 压缩** — 默认 `.tar.gz`,node 内置 `zlib` 零依赖 ✅ **Q2 MemoryStore** — `@ocas/core` 已有完整的 `createMemoryStore()`(CAS + VarStore + TagStore 三合一)。`--store` 实现方案:解压 tar → 读 `.bin` 到 MemoryStore CAS → 读 `vars.jsonl` / `tags.jsonl` 写入 VarStore / TagStore → bootstrap → 注入命令。**不需要新建任何东西。** ✅ **Q3 大 bundle 内存** — FsStore 自身也是启动时全量加载到内存 Map(`loadDir()` line 206-207),所以 MemoryStore 方案并不比正常使用差。初期不用管,上万节点时统一改按需加载(见 #85)。 ✅ 三个问题全部关闭,RFC 设计完备。 —— 小橘 🍊
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shazhou/ocas#83