# 4-24 [done] Resource Tree / File Tree / Page Tree 真源合同与执行清单 v1

> 更新时间：2026-05-13
>
> 2026-05-15 口径更新：
> - 本文件完成时的 `index.md` 可见 UI 模型已被 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-35-convex-filetree-title-md-source-alignment-v1.md` 覆盖。
> - 后续 Convex File Tree 默认页面正文行显示为 `{title}.md`，rowId 为 `doc:<documentId>`；本文中的 `index.md` 仅表示历史正文 object identity / 兼容语义，不再作为默认可见子行继续扩展。
>
> 上游依据：
> - `/mnt/Data1T/mnote/design/10-review/05-tree.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/10-review/README.md`

## 1. 目标

本清单用于把 `design/10-review/05-tree.md` 中的判断落到树域主线：

- `Resource Tree` 是长期对象组织真源，由 Rust kernel 持有语义。
- `File Tree` 是 Resource Tree 的主组织投影，展示页面、`index.md`、附件、mindmap、OnlyOffice、代码附件等资源。
- `Page Tree` 是页面导航投影 / 快捷视图，不拥有排序、父子、附件归属或资源归属的最终真相。
- Sidebar、文件树 UI、页面树 UI 都只能消费 projection 和发 command，不能重新拼第二套结构真相。

## 2. 术语冻结

- `Resource Tree`：workspace 内对象与资源的 canonical hierarchy。它不是 UI 文件树组件，而是 kernel 层的资源组织语义。
- `File Tree`：Resource Tree 面向 VS Code Explorer 体验的 projection。
- `Page Tree`：Resource Tree 中页面对象的导航 projection，可做快捷显示，但不拥有资源归属。
- `ObjectIdentity`：打开、保存、草稿、tab、command 使用的对象身份。
- `BlockAssetRelation`：页面正文 block 与资源对象之间的稳定关联，例如 `documentId + blockId + assetId + assetKind`。

## 3. 非目标

- 不在本阶段重写 Convex 底层存储。
- 不在本阶段一次性删除所有 compat/fallback。
- 不把文件树 UI 组件提升为真源。
- 不让 Page Tree 接管附件、mindmap 或 OnlyOffice 的归属。

## 4. 前置顺序与执行门

本清单是 `5-12` 的前置合同。执行顺序固定为：

1. 先完成本文件的 `Resource Tree / File Tree / Page Tree` 真源合同。
2. 再执行 `design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md` 中的主编辑区 object tab、草稿隔离和打开路径调整。

不允许绕过本文件直接在编辑区里新增第二套 asset / page 归属判断。编辑区可以先做 mindmap 止血，但止血代码必须只消费本文件定义的 object identity / file tree intent，不得把临时前端判断提升为长期合同。

当前代码基线：

- `rust/crates/core-protocol/src/kernel.rs` 已有 `KernelProjectionResourceKind`、`KernelProjectionAssetKind`、`KernelProjectionResourceMeta`、`KernelProjectionItem`。
- `rust/crates/bridge-runtime/src/lib.rs` 已有 `build_file_tree_projection_result`、`make_projection_resource_meta`、`normalize_file_tree_asset` 等 file tree projection 构造逻辑。
- `rust/crates/mnote-web/src/routes/tree.rs` 与 `rust/crates/mnote-web/src/ssr/pages/layout.rs` 已消费 `resourceMeta` 并发出 `tree.asset.open`。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs` 当前仍把 mindmap asset 主路径导航到 `/mindmap/{documentId}/{assetId}`，这是 `5-10` bug 的直接落点，但长期合同归属仍应先在本文件冻结。

阶段门：

- G1：协议能表达 `index.md`、mindmap、OnlyOffice、普通附件、代码附件的不同 object identity。
- G2：`file_tree` projection row 能稳定输出 object identity 或等价 `resourceMeta.extra.objectIdentity`。
- G3：`page_tree` projection 被约束为页面导航投影，不承载 asset 归属。
- G4：`tree.asset.open` 只表达打开资源对象的 intent，不决定“把资源当正文打开”。
- G5：完成 G1-G4 后，`5-12` 才能把主编辑区打开路径、草稿 key、保存命令接入 object tab / object editor 规则。

## 5. 执行清单

### 5.1 协议模型

- [x] 在 `rust/crates/core-protocol/` 增加或扩展资源对象协议。
  - 优先落点：`rust/crates/core-protocol/src/kernel.rs`，必要时拆出 `rust/crates/core-protocol/src/resource.rs` 并在 `lib.rs` re-export。
  - 当前已有 `KernelProjectionResourceKind`，本阶段应补齐或映射为长期 `ResourceKind` 语义。
  - 最小资源种类：`page`、`index`、`mindmap`、`attachment`、`onlyoffice`、`code`。
  - `ObjectIdentity` 最小字段：`objectKind`、`documentId`、`blockId`、`assetId`。
  - `BlockAssetRelation` 最小字段：`documentId`、`blockId`、`assetId`、`assetKind`。
  - 命名规则：Rust 类型使用 `KernelObjectIdentity` / `KernelBlockAssetRelation` 或等价项目内前缀；序列化字段使用 camelCase。

- [x] 补协议单测。
  - 文件建议：`rust/crates/core-protocol/tests/resource_tree_contract.rs`。
  - 断言：mindmap、OnlyOffice、普通附件都能表达为 resource object。
  - 断言：`index.md` 的 object identity 与 mindmap asset 的 object identity 不相等。
  - 断言：`BlockAssetRelation` 能表达 `documentId + blockId + assetId + assetKind`，且不会被反序列化为页面正文 identity。
  - 运行命令：
    - `cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol --test resource_tree_contract -- --nocapture`
    - 预期：新增协议测试通过，失败时不得继续进入 `5-12` 的 P1/P2。

### 5.2 Projection 合同

- [x] 扩展 `file_tree` projection 的 row contract。
  - 每个 row 必须带稳定 `row_id`、`node_id`、`projection_kind`、`resource_meta`。
  - `resource_meta` 至少包含 `resourceKind`、`documentId`、`assetId`、`assetKind`、`objectIdentity`。
  - 优先修改点：`rust/crates/core-protocol/src/kernel.rs` 的 `KernelProjectionResourceMeta`，以及 `rust/crates/bridge-runtime/src/lib.rs` 的 `make_projection_resource_meta`。
  - 兼容策略：已有 `resourceKind/documentId/assetId/assetKind` 保持不破坏；新增字段可先放入 `resourceMeta.extra.objectIdentity`，待前后端消费稳定后再提升为强类型字段。

- [x] 固定 `page_tree` projection 的降级边界。
  - `page_tree` 只输出页面导航关系。
  - `page_tree` 不输出附件归属、mindmap 归属、OnlyOffice 归属的最终判断。
  - 如需显示资源状态，只能引用 `resource_meta` 或 resource projection，不得在 page tree renderer 内拼装。
  - 优先检查点：`rust/crates/bridge-runtime/src/lib.rs` 中 `page_tree` projection 构造逻辑、`wolai-frontend/src/lib/tree-projection.ts`、`wolai-frontend/src/lib/documents/page-subtree.ts`。

- [x] 补 Rust projection 测试。
  - 文件优先看：`rust/crates/bridge-runtime/src/lib.rs`、`rust/crates/mnote-web/src/tree_shell/`。
  - 测试要求：同一页面下的 `index.md` 与 mindmap asset 生成不同 row，但共享同一 `documentId`，并通过 `BlockAssetRelation` 建立关系。
  - 建议新增或扩展测试：
    - `kernel_project_view_query_executes_into_file_tree_projection_contract`
    - `kernel_file_tree_projection_query_supports_extended_resources_and_max_results`
    - 新增 `kernel_file_tree_projection_separates_index_and_mindmap_object_identity`
  - 运行命令：
    - `cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime file_tree_projection -- --nocapture`
    - `cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web file_tree_projection -- --nocapture`

### 5.3 File Tree 派生规则

- [x] 页面节点下固定派生 `index.md` row。
  - `index.md` row 只能打开 Page Aggregate body。
  - `index.md` row 不得根据页面内第一个 block 类型改变打开目标。
  - 优先检查点：`rust/crates/bridge-runtime/src/lib.rs` 的 `build_file_tree_projection_result`，当前已固定创建 `index:{documentId}` node。
  - 前端消费检查点：`rust/crates/mnote-web/src/ssr/pages/layout.rs` 的 file tree row click 分支。

- [x] 页面内资源固定派生为 child resource row。
  - mindmap row 打开 mindmap object editor。
  - OnlyOffice row 打开 OnlyOffice object editor。
  - 代码附件 row 打开代码/预览编辑器。
  - 普通附件 row 打开签名资源或预览。
  - 优先检查点：`rust/crates/bridge-runtime/src/lib.rs` 的 `normalize_file_tree_asset`、`infer_file_tree_asset_shape`、`classify_asset_resource_kind`。
  - 必须保留 `documentId` 与 `assetId`，供 `5-12` 的 object tab identity 使用。

- [x] 文件树 row 不得从前端多份数据临时拼真相。
  - 允许前端显示、筛选、选中、展开。
  - 不允许前端决定资源归属、排序真相或 block-asset relation 真相。
  - 前端允许做的本地状态：`expanded`、`selected`、`hover`、`focus`、`dragging`、`drop target`。
  - 前端禁止新增长期字段：`assetParentDocumentId`、`mindmapOwnerPageId`、`pageTreeAssetChildren` 这类与 kernel projection 重复的归属真相。

### 5.4 Page Tree 派生规则

- [x] Page Tree 只消费页面导航投影。
  - 可显示页面标题、层级、当前页、快捷入口。
  - 不负责附件、mindmap、OnlyOffice、代码附件的生命周期。

- [x] Page Tree command 只发页面导航相关 command。
  - 页面新建、重命名、移动、归档继续走 `tree.node.*` 或正式 page/tree command。
  - 资源 attach/detach/rename/move 不进入 page tree 私有命令。
  - 负向检查：Page Tree renderer / host 中不得新增 mindmap、OnlyOffice、附件归属推导分支。

### 5.5 Command 边界

- [x] 新增或确认资源关系 command family。
  - `tree.asset.attach`
  - `tree.asset.detach`
  - `tree.resource.rename`
  - `tree.resource.move`

- [x] 明确不同对象写入 owner。
  - 页面正文：`page.body.save`
  - 页面标题：`page.head.updateTitle`
  - 页面设置：`page.layout.updateOptions`
  - mindmap 内容：`mindmap.command.apply`
  - OnlyOffice 内容：OnlyOffice callback / forcesave 写回链
  - 资源归属：`tree.asset.*` / `tree.resource.*`
  - 优先检查点：
    - `rust/crates/bridge-runtime/src/lib.rs` command plan
    - `rust/crates/mnote-web/src/transport/convex.rs` command adapter
    - `wolai-frontend/src/lib/documents/rust-runtime.test.ts`

### 5.6 Smoke 与验收

- [x] 增加 Resource Tree / File Tree projection smoke。
  - 断言文件树同页下可见 `index.md`、mindmap、附件。
  - 断言点击 `index.md` 只打开 Page Aggregate body。
  - 断言点击 mindmap 不进入 Page Aggregate body 保存链。
  - 可复用脚本：`scripts/task112-tree-rust-family-regression-smoke.js`。
  - 建议新增断言脚本：`scripts/task170-resource-tree-filetree-source-smoke.js`，或在 task112 中增加独立 case。
  - 运行命令：
    - `node scripts/task112-tree-rust-family-regression-smoke.js`
    - 若新增脚本：`node scripts/task170-resource-tree-filetree-source-smoke.js`

- [x] 增加 Page Tree 负向 smoke。
  - 断言 Page Tree 不显示资源归属为自己的结构真相。
  - 断言 Page Tree 操作不会改变 mindmap/附件归属。
  - 可复用脚本：`scripts/task112-tree-rust-family-regression-smoke.js`。
  - 负向检查输出需记录：操作 Page Tree 后，同一 `assetId` 的 `documentId` / `objectIdentity` 未变化。

## 6. 建议提交切片

本文件只定义设计顺序，不要求一次提交完成全部实现。后续执行时建议按以下切片推进：

1. 协议切片：`core-protocol` 增补 object identity / relation 类型与测试。
2. Projection 切片：`bridge-runtime` 与 `mnote-web` file tree projection 输出 object identity。
3. UI 消费切片：file tree host 只转发 object open intent，不拼归属真相。
4. Page Tree 负向切片：补 page tree 不持有资源归属的单测或 smoke。
5. 验收切片：跑 Rust 测试与真实浏览器 smoke，把证据补回对应 bug / design 文档。

## 7. 验收命令

最低验收命令：

```bash
cd /mnt/Data1T/mnote/rust && cargo test -p core-protocol --test resource_tree_contract -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p bridge-runtime file_tree_projection -- --nocapture
cd /mnt/Data1T/mnote/rust && cargo test -p mnote-web file_tree_projection -- --nocapture
node scripts/task112-tree-rust-family-regression-smoke.js
```

如执行环境没有启动 `3000`，先运行仓库常用入口：

```bash
npm run desktop:hot
```

真实浏览器 smoke 必须使用默认测试账号 `mnote.e2e@example.com` / `MnoteE2E123!`，不能用 `devFallback` 代替登录态。

## 8. Done Gate

本文移动到 `done/` 前必须满足：

- [x] `Resource Tree`、`File Tree`、`Page Tree` 三个术语在协议和文档中不再混用。
- [x] `file_tree` row 能表达 `index.md`、mindmap、OnlyOffice、附件、代码附件的不同 object identity。
- [x] `page_tree` 被验证为页面导航 projection，不拥有资源归属真相。
- [x] mindmap 与 `index.md` 的打开、保存、草稿身份互相隔离。
- [x] 至少一条真实浏览器 smoke 覆盖 `index.md + mindmap asset + 切页 + 再打开` 的完整链路。

## 9. 完成证据

- 协议：`rust/crates/core-protocol/src/kernel.rs` 增加 `KernelObjectIdentity`、`KernelObjectKind`、`KernelBlockAssetRelation`，并由 `rust/crates/core-protocol/tests/resource_tree_contract.rs` 覆盖。
- Projection：`rust/crates/bridge-runtime/src/lib.rs` 的 file tree projection 输出 `resourceMeta.objectIdentity` / `blockAssetRelation`，并覆盖 index、mindmap、OnlyOffice、代码附件、普通附件。
- UI intent：`rust/crates/mnote-web/src/ssr/pages/layout.rs`、`rust/crates/mnote-web/src/routes/tree.rs`、`rust/crates/mnote-web/src/tree_shell/filetree_renderer.rs` 将 `objectIdentity` 传入 file tree DOM 与 `tree.asset.open`。
- Smoke：`node scripts/task112-tree-rust-family-regression-smoke.js` 通过，page tree host / file tree host 主链为 `dom`；debug tree shell 关闭项按当前主线记录为 skipped。
- Mindmap 链路：`node scripts/task169-mindmap-realtime-smoke.js` 通过，覆盖 mindmap asset 打开、长中文编辑、保存、切页、回 `index.md`、再打开 mindmap。

## 10. 交接给 5-12 的输入

完成本文件后，`5-12` 可以依赖以下输入推进主编辑区：

- 文件树 `index.md` row 的 `objectIdentity`：`page:index:{documentId}`。
- 文件树 mindmap row 的 `objectIdentity`：`resource:mindmap:{documentId}:{assetId}`。
- 文件树 OnlyOffice row 的 `objectIdentity`：`resource:onlyoffice:{documentId}:{assetId}`。
- 文件树普通附件 row 的 `objectIdentity`：`resource:attachment:{documentId}:{assetId}`。
- 文件树代码附件 row 的 `objectIdentity`：`resource:code:{documentId}:{assetId}`。
- Page Tree 不再作为资源归属输入；它只提供页面导航选择。
