# 4-10 [recycle] 树域 Rust 家族化执行清单 v1

> 更新时间：2026-04-26
>
> 回收说明（2026-04-28）：本文是早期总执行清单，未完成项已被 `4-11`、`4-16`、`4-18` 拆分并覆盖。本文保留为历史执行口径，不再作为当前 `process` 入口；后续以 `design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md` 及 `done/4-11`、`done/4-18` 为准。
>
> 关联文档：
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.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-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`

## 1. 文档目的

这份清单只服务一件事：

> **把树域从“Rust 持有语义、React 持有主执行面”的过渡态，推进到“Rust 家族同时持有语义与主执行面”的下一阶段。**

本文不是上位方案稿，不重复讨论：

- 为什么要继续 Rust 家族化
- 为什么 `Convex` 要保留
- 为什么 `page tree / file tree` 不能退化

这些已经在 `4-9` 与关联文档中固定。

本文只给：

- 分阶段执行顺序
- 每阶段 checklist
- 每阶段阻塞项
- 每阶段验收口径

---

## 2. 当前基线

当前默认真实状态（2026-04-25）：

- 树域 truth 已收口到 `tree-first graph kernel`
- `Convex` 继续提供存储、同步、内容与实时底座
- `mnote-web` 已具备 projection / command / transport 基础出口
- `page tree / file tree / picker` 已开始消费 projection family
- 主站默认 `treeRendererFamily` 已切到 `rust_family`
- `page tree / file tree / picker` 默认主路径已进入 same-origin iframe compat host
- 但当前切主的是 `compat host`，不是 final Rust renderer；宿主与 fallback 仍保留

因此本清单的目标不是“再做一轮 projection 收口”，而是：

- 补齐 Rust 侧剩余契约
- 迁掉主路径 React 树 renderer
- 保住 `page tree / file tree` 现有能力

---

## 3. 总体执行顺序

建议固定为下面五段：

1. `Phase A`：补齐 `file_tree` Rust 直接输出
2. `Phase B`：完成 `tree.*` 命令主路径切换
3. `Phase C`：补齐树域正式 realtime contract
4. `Phase D`：迁移 `page tree` Rust 家族 renderer
5. `Phase E`：迁移 `file tree / picker` Rust 家族 renderer，并收薄宿主

原因：

- 先补 `file_tree` 输出，避免 renderer 迁移时继续背前端 adapter。
- 先收命令与 realtime，避免 UI 壳迁完后还卡在旧协议。
- `page tree` 先迁，能先验证主行 renderer 与交互骨架。
- `file tree` 后迁，避免最复杂对象面成为第一块落地风险点。

---

## 4. Phase A：补齐 `file_tree` Rust 直接输出

### 当前进度（2026-04-25）

- [x] 前端 `file tree` 主路径已优先消费 Rust `kernelFileTreeProjection.items`
- [x] `/api/sidebar`、SSR loader、`/api/mnote-web/stream` 已补 `kernelFileTreeProjection`
- [x] 非过滤主路径不再默认依赖 `pageRows + assetsByDoc` 拼装
- [x] `file tree` 同源 host 的 inline override 已收口到 `kernel file_tree items`
  - `inlineFileTreeRows` 旧注入边界已移除
  - `TreeShellIframeHost` 不再接受 `FileTreeRow[]` 作为正式 inline renderer 输入
- [x] 搜索过滤态不再回退 `pageRows + assets` 二次重建
  - `buildVisibleRows` 缺少 `kernel file_tree items` 时稳定返回空列表
  - 旧 `filterKernelFileTreeProjectionItems` 宿主裁剪 helper 已移除
  - 正式 Rust 搜索 projection 已在 `4-11 Phase H` 接入 3000 同源主链

### 目标

让 `file_tree` 不再依赖当前前端在 `pageRows + assets` 上继续拼主路径可见行语义。

### checklist

- [x] 盘点当前 `file_tree` 仍由前端 adapter 补齐的对象语义：
  - `index.md`
  - 附件
  - `mindmap` 文件夹
  - 导图子附件
  - 表格 / 书籍 / PDF 等扩展对象
- [x] 在 Rust 侧明确 `file_tree` projection 的正式输出字段：
  - `resource_kind`
  - `asset_kind`
  - `icon_hint`
  - `expandable`
  - `expanded_by_default`
- [x] 为 `index / asset / asset_folder / mindmap / table / book / pdf` 补 fixture
  - `tree-delta.test.ts` 已覆盖 delta 重建后的 `mindmap / pdf / book / table`
  - `route.test.ts` 已覆盖 snapshot route 中的 `index / asset / asset-folder / mindmap / table / book / pdf`
- [x] 为 `file_tree` route 增加契约测试
  - `/api/mnote-web/stream` route snapshot 已断言 `file_tree` item contract 与 `iconHint`
- [x] 把前端 `file tree` 的主路径输入改成“消费 Rust 直接输出的 item 列表”，而不是继续拼对象真相
  - `sidebar` 主链默认输入已收口到 `kernelFileTreeProjection.items`
  - `rust_family` host 已不再接受 `inlineFileTreeRows` 旧 rows 注入

### 阻塞项

- `file_tree` canonical 基础对象语义已由 projection contract 覆盖
- 搜索态已接入 Rust 搜索 projection 主链；更完整的搜索输入扩展与 final renderer 收口已转入 `4-11 Phase H/F`

### 验收口径

- `file_tree` 主路径不再依赖临时 adapter 维持对象层级
- Rust route 输出足以直接驱动 renderer
- `index.md / asset-folder / asset` 行语义在 fixture 与 route test 中稳定存在

---

## 5. Phase B：完成 `tree.*` 命令主路径切换

### 当前进度（2026-04-25）

- [x] 已确认正式命令名：
  - `create -> tree.node.create`
  - `rename -> tree.node.rename`
  - `move -> tree.subtree.move`
- [x] 已新增前端 `/api/tree/commands` route，主路径可直接发正式 tree action
- [x] `createDocumentCommand` 已切到 `/api/tree/commands`
- [x] `renameDocumentCommand` 已切到 `/api/tree/commands`
- [x] `moveDocumentCommand` 已切到 `/api/tree/commands`
- [x] `delete / restore / purge / embed / copy-tree` 已切到 `/api/tree/commands`
- [x] `create` 保留本地 `ensureDocumentScaffold` 副作用，不因切流丢失
- [x] `/api/documents/create` 已收薄为 compat alias，转发 `/api/tree/commands`
- [x] `/api/documents/title` 已按命令语义分流：
  - 树重命名兼容请求 -> `/api/tree/commands`
  - `page.head.updateTitle` -> `page write`
- [x] `/api/documents/move` 已收薄为 compat alias，转发 `/api/tree/commands`
- [x] `/api/documents/delete / restore / purge / embed / copy-tree` 已收薄为 compat alias，转发 `/api/tree/commands`
- [x] `bridge-runtime` 已补 `tree.node.archive / restore / purge / embed` 与 `tree.subtree.copy` 别名执行计划
- [x] `tree.*` 主路径已补路由 / shared client / compat alias / runtime 回归测试
- [x] `move` 的 target-parent legality、self / descendant 拦截、canonical `parent_id / sort_order / workspace_id / updated_at` 已下沉到 Convex `documents.move`
- [x] `move legality` 已继续离开 Next route 结果拼装，开始转到 Rust bridge-runtime 计算
  - `/api/tree/commands` 不再自己构造 `movePreflight`
  - Next route 现在只把 `documents` 快照作为 `preflightData` 交给 Rust
  - Rust bridge-runtime 已可从快照推导 target parent / ancestor chain 并拒绝 descendant / missing-parent
- [ ] `move legality` 与更完整排序规则仍未完全进入最终 Rust kernel 主链；当前实际写入仍停留在 Convex compat mutation
  - Rust `bridge-runtime` 已新增 canonical move order plan，并在 command plan 中输出 `normalizedMove`
  - Convex `documents.move` 已增加 `normalizedMove` 可选校验，执行前确认当前 patch plan 与 Rust plan 一致
  - 最终 Rust 写路径执行下沉仍转入 `4-11 Phase I`

### 目标

让树域主路径默认走正式 `tree.*` 命令面，`documents.*` 退到 compat。

### checklist

- [x] 盘点当前主路径仍在调用的 `documents.*` 入口：
  - create
  - rename
  - move
  - archive
  - restore
  - purge
  - embed
  - copy-tree
- [x] 明确每条命令对应的 `tree.*` 正式名
- [x] 前端 shared command client 默认发 `tree.*`
- [x] `documents.*` 仅保留 alias / compat
- [ ] 把 move legality、target parent、position normalize 继续从 Convex compat mutation 回收给 Rust kernel
  - legality 与 normalized order plan 已在 Rust 可测试；TS transport 已透传 `normalizedMove`；Convex 已可选校验 Rust plan；最终写入仍待迁移
- [x] 为 `tree.*` 路径补命令回归测试

### 阻塞项

- `embed / copy-tree` 仍有较强前端语义残留
- move legality 与排序规则已离开 Next route 拼装；排序计划已进入 Rust 可观测 plan 并由 Convex 可选校验，但实际执行仍待下沉

### 验收口径

- 新增主路径不再继续扩写 `documents.*`
- 主调用路径默认发 `tree.*`
- 组件层不再承担长期树命令语义解释

---

## 6. Phase C：补齐树域正式 realtime contract

### 当前进度（2026-04-24）

- [x] `tree stream` 前端主链已从“freshness 仲裁”收口到“stream live 优先，query/fallback 兜底”
- [x] `usePreferredSidebarSnapshot` 已按 `treeStream.status` 做主链选择：
  - `live / connecting / idle` 有 stream 数据时优先 stream
  - `fallback` 时回退 query
- [x] `useSidebarData` 手动 `refetch` 已收窄为临时 snapshot：
  - 仅在当前 live 基线未变化时暂时覆盖
  - live 基线变化后回到新的 live snapshot
- [x] `/api/mnote-web/stream` 已升级为同源长连接 SSE：
  - 首帧固定发 `snapshot`
  - 后续按最新 `command_log` cursor 轮询检测变化
  - 当前已支持最小 `delta/noop/resync` 分流，不伪造不稳定的复杂树增量
- [x] stream cursor 已从“复用 overview 分页 cursor”改成“stream 自己的 latest command-log cursor”
- [x] stream cursor 已补 `domain_events` 感知
  - 当 `command_logs` 未推进、但 `domain_events` 新增时，不再静默停在旧 cursor
  - 当前最小口径先触发 `resync`，避免 workspace realtime 漏感知事件侧推进
- [x] `workspace / subtree` 双 scope 已在 route + test 形成正式 envelope 边界：
  - `workspace -> sidebar_tree`
  - `subtree(rootNodeId) -> page_tree`
- [x] `/api/tree/commands` 主路径已开始写入 `bridgeLogs`：
  - create / rename / archive / purge / embed / copy / move / restore 都会推进 stream 可见 cursor
  - failed mutation 也会写 failure artifact，但 stream 主查询已收窄为只看 `succeeded`
- [x] stream 当前已能消费显式 `streamDelta`：
  - `create -> upsert_document`
  - `rename -> upsert_document(局部 patch)`
  - `page.head.updateTitle -> upsert_document(局部 patch)`
  - `archive / purge -> remove_document`
  - `embed -> noop`
  - `move -> move_document(细粒度 delta)`
  - `restore -> upsert_document(细粒度 delta)`
  - `copy -> upsert_documents(细粒度 delta)`
  - `documents` 列表不可用时，仍保留 `replace_sidebar / replace_documents` 作为兼容兜底
- [x] `move / restore / copy` 已从“只能纯 `resync`”推进到显式 delta
  - `move` 已推进到 `move_document`
  - `restore` 已推进到 `upsert_document`
  - `copy` 已推进到 `upsert_documents`
  - Rust `bridge-runtime` 已开始输出 `streamDeltaHint`，Next route 仅按 Rust plan hint 与 mutation result 物化 delta
- [x] 已新增树域 renderer 统一 delta 应用边界：
  - `tree-delta.ts` 已补 `applyTreeStreamDeltaToProjectionState`
  - `sidebar_tree / page_tree / file_tree` 已可从同一 delta 边界派生各自输入
- [x] `replace_documents` delta 重建时已保留 `mindmapAssetChildren`
  - `file_tree` 不再把 `asset-folder` 在 delta 重建时退化成普通 `asset`
- [x] Rust / 3000 同源 stream 已能从单条新 `domain_event.payload.streamDelta` 发出 `delta`
- [x] Rust / 3000 同源 stream 已能对同一 `command_id` 的 command log + domain event 双写做 delta 去重
  - `streamDelta` 一致时发一次 `delta`
  - 不一致或多条混杂时继续保守 `resync`
- [x] Rust command 侧已开始生成正式 tree delta / event hint：
  - `streamDeltaHint` 覆盖 create / rename / archive / restore / purge / embed / move / copy
  - `domainEventHint` 覆盖 `tree.node.created / renamed / archived / restored / purged / embedded / duplicated` 与 `tree.subtree.moved / copied`
  - Next route 不再按 action 手写主 delta，而是按 Rust plan hint + mutation result 物化 `streamDelta`
- [x] resource command 侧已开始进入正式 delta / event hint：
  - `tree.resource.copy / move / upload` 已输出 `asset_result -> upsert_assets`
  - `domainEventHint` 覆盖 `tree.resource.copied / moved / uploaded`
  - 资源 copy/move/upload 的 artifact 已由 3000 Rust artifact writer 持久化
- [ ] Rust 写路径仍未覆盖所有主写入口的完整 domain event 持久化
  - `mnote-web /api/tree/commands` 已可由 Rust transport 直接生成并持久化 `command_logs / domain_events`
  - 3000 tree/media 主写入口与文档类 Rust transport adapter 已改为 Rust artifact writer：
    - `/api/tree/commands`
    - `/api/media/batch`
    - `/api/media/upload`
    - `page-command-adapter`
    - `page-lifecycle-command-adapter` 的 `create / move / delete / restore / duplicate / copy_tree`
    - `save-command-adapter`
    - `metadata-command-adapter` 的 `title / stats / options`
    - `lib/documents/block-command-adapter`
    - `page-write-command-adapter` 的 `page.head.updateTitle / page.layout.updateOptions / page.body.save`
  - 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
- [ ] `3000` 仍是浏览器入口，但 polling transport 主体已在 Rust SSE；Next route 仍保留薄代理与过渡职责

### 目标

让树域从“可用 stream + fallback”进入正式 `snapshot + delta + resync` 合同。

### checklist

- [x] 明确 workspace 与 subtree 两类 stream scope
- [x] 统一 `snapshot / delta / resync / cursor` 协议
  - `snapshot / cursor` 已在同源 stream route 固定
  - `delta` 已支持最小显式 contract，但复杂树变更仍待 Rust 正式事件面补齐
  - `resync` 仍是复杂树变更与漂移场景的保守回退
  - 当前勾选口径以 `3000` 同源 Next route contract 为准，`server.ts` / `server.test.ts` / `route.test.ts` 已覆盖；Rust 原生 transport 仍待继续收口
- [x] 补 cursor 漂移与 resync 策略
- [x] 明确大漂移时的回退口径
  - 当前保守口径：无法稳定解释的新 cursor 统一退 `resync snapshot`
- [x] 为 `page tree / file tree / sidebar_tree` 补事件 fixture
  - `server.test.ts` 已覆盖 `sidebar_tree` snapshot / delta / resync
  - `route.test.ts` 已覆盖 `page_tree` subtree snapshot fixture
  - `route.test.ts` 与 `tree-delta.test.ts` 已覆盖 `file_tree` 的 `document / index / asset-folder / asset` 行语义
- [x] 为树域 renderer 定义统一 delta 应用边界
  - 当前口径是 `documents/sidebar -> projection state` 的统一边界，不等于 final Rust domain-event 细粒度 reducer
- [x] 让主路径不再主要依赖 freshness 选择与补偿式 refetch
  - sidebar stream 主链已变成“长连接 snapshot/delta/resync”
  - query/fallback 继续保留为护栏

### 阻塞项

- Next route 级过渡 transport 仍在承担一部分正式职责
- `delta` payload 的对象覆盖面还不够完整
- Rust command 已给 `move / restore / copy` 输出 delta/event hint；3000 tree/media 主写入口 artifact 持久化与显式 adapter 层已切到 Rust artifact writer；剩余未完成项转入更正式的 block/save-snapshot domain-event contract 收口
- final domain event delta contract 已转入 `4-11 Phase J`

### 验收口径

- 树域实时主链存在正式协议说明与测试
- 主路径能稳定处理 snapshot、delta、resync
- fallback 成为护栏，而不是主同步策略本身

---

## 7. Phase D：迁移 `page tree` Rust 家族 renderer

### 当前进度（2026-04-24）

- [x] 前端 `page tree / file tree` surface 已显式暴露 `rendererFamily` 边界
- [x] `SidebarTreeSurface` 已成为更正式的 renderer host 边界，而不只是隐式 React 直连
- [x] 已新增 `TreeShellHost`，`page tree / file tree / picker` 在 `rust_family` 下会进入显式宿主分支，而不再只是透传 `data-renderer-family`
- [x] 主站已存在极薄挂载位；当前 `rust_family` 分支先受控包住 React fallback，后续再替换成真正的 Rust family renderer
- [x] 已新增 `3000` 同源 `/api/tree/shell` proxy route；浏览器不再直接持有 `mnote-web` base url
- [x] 未过滤 `page tree` 在 `rust_family` 下已可切到 same-origin iframe compat host，并 bridge：
  - `tree.navigate`
  - `tree.page.context-menu`
  - `tree.node.created / renamed / moved`
- [x] `page tree` 已新增 `tree.page.focus.changed` bridge
  - 宿主已显式持有 `focusedDocumentId` 本地状态
  - page focus 不再只停留在 iframe 内部私有状态
  - iframe host 与 Rust shell contract 已补定向测试
- [x] 过滤态 `page tree` 在 `rust_family` 下已可通过 inline override 进入 same-origin iframe compat host
  - 浏览器端直接注入过滤后的 projection rows
  - 不再因为 filter 非空而强制回退 React fallback
- [x] 过滤态 `page tree` 即使筛到 0 条，也会继续通过 inline override 留在 same-origin iframe compat host
- [x] 默认主路径已切到 `rust_family` same-origin compat host
  - `task112` 主站 smoke 已验证 page tree hostKind 为 `iframe`
  - 当前切主的是 compat host，不等于 final Rust renderer 已完成
- [x] `page tree` 主路径 React renderer 已退出
  - `TreeShellSurface` 缺少 iframe 宿主时只显示 `page-tree-renderer-removed`
  - 主路径运行时已不再挂载旧 React `PrivateTree`
- [x] `task112` 已补 page tree 的 create / rename / 大树滚动真实 smoke
  - compat shell hover 动作区已用于实跑新建子页面与重命名
  - 大树场景下，键盘导航可把目标行稳定带入可视区
- [ ] 当前 `page tree` 仍是 `mnote-web tree shell compat host`，不是最终 Rust renderer
- [ ] 过滤态 `page tree` 当前仍是 compat host + inline override，不是最终 Rust family renderer

### 目标

让 `page tree` 的主渲染与主交互骨架退出当前 React 主壳。

### checklist

- [ ] 明确 `page tree` renderer 最小输入：
  - projection items
  - local UI state
  - command dispatcher
- [ ] 抽离正式模块边界：
  - row model
  - focus model
  - keyboard model
  - DnD state machine
  - context action registry
- [ ] 实现 Rust 家族 page tree renderer
- [x] 迁移展开/折叠
- [x] 迁移当前页高亮与祖先自动展开
- [x] 迁移 hover 动作区
- [x] 迁移右键菜单入口
- [x] 迁移键盘导航
- [x] 迁移基础拖拽排序
- [x] 在主站预留极薄挂载位
- [x] 用 feature flag 控制新旧 page tree 切换

### page tree 能力保留硬门槛

- [x] 稳定层级展开/折叠
- [x] 当前页高亮
- [x] 祖先自动展开
- [x] 新建子页面
- [x] 重命名
- [x] 页面移动
- [x] 上下文菜单
- [x] 键盘导航
- [x] 基础拖拽排序
- [x] 大树场景稳定滚动

说明：

- 上面已勾选的 `右键菜单入口 / 上下文菜单 / feature flag`，当前口径是“same-origin compat host + 宿主 bridge 已闭环”，不等于 final Rust renderer 已切主。
- 本轮新增勾选的 `展开/折叠 / 当前页高亮 / 祖先自动展开 / 键盘导航 / 拖拽排序 / 页面移动 / 新建子页面 / 重命名 / 大树场景稳定滚动 / hover 动作区`，口径同样是“默认 `rust_family` compat host + `task112` 真实 smoke 已通过”，不等于 final Rust renderer 已完成。

### 阻塞项

- 宿主边界仍过厚
- 旧 React 组件仍承担主交互状态机

### 验收口径

- 至少一条默认真实流量使用 Rust 家族 page tree renderer
- 不再依赖当前 React `PrivateTree` 作为主路径 renderer
- `page tree` 功能不低于当前产品合同

---

## 8. Phase E：迁移 `file tree / picker` Rust 家族 renderer，并收薄宿主

### 目标

在 `page tree` 主路径稳定后，迁移 `file tree` 与 `picker`，并继续收薄主站宿主职责。

### 当前进度（2026-04-24）

- [x] `picker` 空查询态与搜索态已统一复用 `TreePickerSurface`
- [x] `picker` 已开始复用与 tree surface 同一 renderer family 边界
- [x] `file tree / picker / page tree` surface 已显式输出 `data-renderer-family`
- [x] `file tree / picker / page tree` 已统一经由 `TreeShellHost` 进入显式 host 选择边界，并输出 `data-tree-host-kind`
- [x] `picker` 已接入 `rust_family` host 边界；当前 host 仍先包住 React fallback，不代表已切到 Rust renderer
- [x] 未过滤 `picker` 空查询态在 `rust_family` 下已切到 same-origin iframe compat host
- [x] `picker` 搜索结果态在 `rust_family` 下已切到 same-origin iframe compat host：
  - 浏览器端通过 inline override 注入搜索结果
  - 不再因为搜索态而强制回退 React fallback
- [x] `picker` 在 `rust_family` 下，空查询且无可选页面时也会继续留在 same-origin iframe compat host
  - 不再因为空结果而直接退回宿主内纯文本 div
- [x] `picker` 根节点与排除项逻辑已补组件回归：
  - 空查询时保留根节点
  - `excludeIds` 不再把当前页误放回候选列表
  - 组件测试与真实浏览器核对已覆盖该口径
- [x] `picker` 空查询态也已支持键盘高亮与 Enter 选中
  - 不再只在搜索结果态依赖输入框 keydown hack
  - React fallback 与 same-origin compat host 都会同步当前高亮项
- [x] `picker` same-origin compat host 已透传 `activePickerItemKey`
  - 根节点高亮不再只能停留在 React fallback
  - Rust tree shell picker 页面测试已覆盖该 query/state
- [x] `picker` 在 `rust_family` 下已把输入框键盘命令继续下沉到 same-origin compat shell
  - 宿主改为发送 `tree.picker.command`
  - iframe 会回传 `tree.picker.focus.changed`
  - `MoveEmbedPickerDialog` 不再直接决定 rust_family 主路径的高亮切换与 Enter 选中
- [x] `picker` runtime 状态更新已对齐 Rust renderer input reducer contract
  - `mnote-web /tree` 与 3000 inline host 均暴露 `rust_picker_state_reducer_v1`
  - `tree.picker.command` 的 next/previous/home/end/pick/focus 已先统一经 `applyPickerStateAction`
  - 当前仍是 compat JS 镜像执行合同，不代表 final Rust runtime 已接管
- [x] 未过滤 `file tree` 在 `rust_family` 下已可切到 same-origin iframe compat host，并 bridge：
  - `tree.navigate`
  - `tree.filetree.context-menu`
  - `tree.asset.open`
  - `tree.node.created / renamed / moved`
- [x] `file tree` 已新增 `tree.filetree.selection.changed` bridge：
  - 宿主可同步 `selectedRowIds / anchorRowId / focusedRowId`
  - 现有复制 / 删除 / 粘贴等依赖宿主选择态的链路不再只能停留在 React `FileTree`
- [x] `file tree` selection truth 已从宿主 legacy reducer 收薄为 renderer event snapshot：
  - `rust_family` 下 `Sidebar` 只订阅 `tree.filetree.selection.changed`
  - 删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费 renderer snapshot
  - `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop
  - 该项不代表 compat runtime 内部选择算法已经 Rust runtime 化；后续仍转入 `4-11 Phase F/G`
- [x] `file tree` runtime 选择入口已对齐 Rust renderer input reducer contract
  - `mnote-web /tree` 与 3000 inline host 均暴露 `rust_filetree_selection_reducer_v1`
  - 单选 / 多选 / Shift 范围选 / 右键选中 / 可见行归一化 / drag rows 解析已先统一经 `applyFileTreeSelectionAction`
  - 当前仍是 compat JS 镜像执行合同，不代表 final Rust runtime 已接管
- [x] `file tree` 删除目标归一化已进入 Rust preflight 主链：
  - `tree.filetree.delete.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreeDeletePlan`
  - 3000 同源 `/api/tree/filetree/delete-preflight` route 与 client 已覆盖
  - `Sidebar` 删除链已不再自己推导 rust_family 主路径的 doc/asset 删除列表
- [x] `file tree` 粘贴目标与复制分类已进入 Rust preflight 主链：
  - `tree.filetree.paste.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreePastePlan`
  - 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
  - `Sidebar` 粘贴链在 `rust_family` 下已不再自己推导目标页面、`doc/index` 递归语义和可复制 asset 列表
- [x] `file tree` 外部上传目标推导已进入 Rust preflight 主链：
  - `tree.filetree.upload-target.preflight` 已由 Rust `bridge-runtime` 输出 `fileTreeUploadTargetPlan`
  - 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
  - `Sidebar` 外部文件 drop 链已不再自己推导 target row、focused row、workspace fallback 与 mindmap target
- [x] same-origin compat shell 已补最小 `file tree` 交互骨架：
  - 单选 / 多选 / Shift 范围选
  - 右键聚焦选中
  - 空白区清空选择
  - `doc / index / asset` 双击打开
- [x] same-origin compat shell 已补 `file tree` 内部拖放 bridge：
  - iframe 可发 `tree.filetree.internal-drop`
  - 宿主已回接现有 `onInternalDrop`
  - `Alt` copy / move 区分继续沿用宿主已有语义
- [x] same-origin compat shell 已补 `file tree` 外部文件拖入 bridge：
  - 行级 drop 与空白区 drop 都会向宿主发 `tree.filetree.external-drop`
  - 宿主已回接现有 `onDropFiles`
- [x] same-origin compat shell 已补最小 `asset-folder` 行语义：
  - `mindmap` 不再只表现为普通 `asset`
  - 已可渲染 `asset-folder -> asset` 层级
  - `asset-folder` 已接入双击打开、右键菜单与 drop target
- [x] `file tree` 资源类型图标语义已补最小闭环
  - same-origin compat shell 已区分 `pdf / image / video / audio / book / table / mindmap`
  - React fallback `FileTree` 也已补同口径图标分支，避免切流前后视觉合同分裂
- [x] 过滤态 `file tree` 在 `rust_family` 下已可通过 inline override 进入 same-origin iframe compat host
  - 浏览器端已改为直接注入过滤后的 `kernel file_tree items`
  - 不再因为 filter 非空而强制回退 React fallback
- [x] 过滤态 `file tree` 已先从 `kernelFileTreeProjection.items` 收敛正式 item contract，再统一生成 rows 与 same-origin host inline 输入
  - 不再回退 `pageRows + assets` 二次重建主可见行
  - `sidebar` 主路径过滤态已优先消费 kernel file tree items
- [x] `file tree` 搜索态已切到 Rust projection query 主链
  - `core-protocol` 的 `KernelProjectionFilter` 已包含 `query`
  - `bridge-runtime` 的 `kernel.project_view(file_tree)` 可按 `query` 输出命中项与必要祖先，并裁剪 edges
  - `mnote-web` `/api/tree/projections/file?...&query=` 已透传 query 到 Rust projection
  - `wolai-frontend` 新增 3000 同源 `/api/tree/projections/file` route，Sidebar 搜索态只消费该 route 返回的 Rust `file_tree` projection items
  - 旧 `filterKernelFileTreeProjectionItems` 宿主裁剪主链已移除
- [x] `file tree` same-origin compat host 已能直接消费 kernel file tree items
  - 过滤态宿主不再必须先把 `FileTreeRow[]` 重新翻译回 inline item
- [x] `file tree` inline override 已移除 `FileTreeRow[]` 正式输入
  - `TreeShellIframeHost` 现在只接受 `kernel file_tree items` 作为 inline projection contract
- [x] `file tree` 默认主路径已切到 `rust_family` same-origin compat host
  - `task112` 主站 smoke 已验证 file tree hostKind 为 `iframe`
  - 当前切主的是 compat host；复杂交互执行面仍依赖宿主 bridge
- [x] `file tree` 主路径 React renderer 已退出
  - `TreeShellSurface` 缺少 iframe 宿主时只显示 `file-tree-renderer-removed`
  - 主路径运行时已不再挂载旧 React `FileTree`
- [x] `file tree` 首屏 Rust initial DOM 的 hydrate 后状态 patch 已继续收薄
  - `TreeShellIframeHost` 在 `data-rust-filetree-renderer=initial_v1` hydrate 成功后，对宿主 active / selection patch 只更新 `data-active / data-selected`
  - 该项只是减少 compat JS 整树重绘，不等于 final Rust renderer 已完成
- [x] 3000 inline host 已减少第二份 projection 注入真相
  - `TreeShellIframeHost` 主路径现在把 inline projection items 放入 `tree-shell-state.items`
  - `rendererInput` 与 `items` 同时进入 appState JSON，compat runtime 优先从该合同读取 expanded / selection / picker active / exclude
  - `__MNOTE_TREE_SHELL_OVERRIDE__` 不再作为 3000 主路径默认注入第二份 items，只保留旧兼容入口
- [x] 3000 inline host 已对齐 Rust state reducer 合同
  - page focus/keyboard 暴露 `rust_page_focus_keyboard_reducer_v1`
  - filetree selection 暴露 `rust_filetree_selection_reducer_v1`
  - picker state 暴露 `rust_picker_state_reducer_v1`
  - 当前只是把默认主路径 compat runtime 的状态更新入口统一到 Rust contract，不等于 final renderer 完成
- [ ] `file tree / picker` 的真正 Rust family renderer 仍未接入；当前只是接入 same-origin compat host，而不是最终 renderer
- [ ] 宿主仍承担实际命令调度、上传执行与部分可见行 / DOM runtime 过渡职责；还不是最终 Rust family 正式模块边界
  - 内部 drop / 删除 / 粘贴 / 外部上传目标的目标与对象分类已下沉 Rust preflight，但文件字节读取、菜单状态、实际 transport 与通知仍在宿主

### checklist

- [x] 明确 `file tree` renderer 的最小输入与可见行模型
  - 当前主链最小输入已先统一为 `kernel file_tree items + expanded document ids + expanded asset-folder ids`
- [x] 迁移 `doc -> index -> asset-folder -> asset` 可见行语义（compat shell 最小版）
- [x] 迁移资源图标与资源菜单分支
  - 资源图标 contract 已在 `route.test.ts` / `tree-delta.test.ts` 覆盖
  - `tree.filetree.context-menu` bridge 已在 `tree-shell-iframe-host.test.tsx` 覆盖
- [x] 迁移单选 / 多选 / Shift 范围选 / 右键选中
- [x] 迁移双击打开资源
- [x] 迁移内部拖放（bridge 到宿主既有 drop 处理）
- [x] 迁移外部文件拖入上传（bridge 到宿主既有上传处理）
- [x] 迁移 copy / move 区分与非法投放校验（Rust preflight 主链 + 宿主 transport 过渡）
- [x] 迁移粘贴目标推导与复制分类（Rust preflight 主链 + 宿主 transport 过渡）
- [x] 让 `picker` 复用同一 renderer / state family 的轻量模式（先完成统一 `TreePickerSurface`）
- [x] 让 `picker` 搜索结果态进入 `rust_family` same-origin compat host
- [x] 收掉主路径 React `FileTree` renderer
- [x] 把 `picker` 键盘高亮 / Enter 主链继续从宿主 input keydown 收到 iframe shell
- [x] 收掉宿主内剩余树结构重建与主交互状态骨架（本批可验证范围）
  - 3000 主路径已固定为 `state.items + rendererInput`，不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份 items；React FileTree/PageTree fallback 不再作为 rust_family 主路径。完整 Rust runtime 替换仍归入后续 final renderer。

### file tree 能力保留硬门槛

- [x] 页面
- [x] `index.md`
- [x] 附件
- [x] `mindmap` 文件夹
- [x] 导图子附件
- [x] 多选
- [x] 范围选
- [x] 资源级右键菜单
- [x] 双击打开
- [x] 内部拖放
- [x] 外部文件拖入
- [x] 资源类型图标语义

### picker 能力保留硬门槛

- [x] 空态稳定显示
- [x] 搜索结果稳定显示
- [x] 高亮与键盘选中稳定
  - 组件回归与 Rust picker shell 定向测试已补
  - `task113` 已在 `3000` 主站默认 `rust_family` 配置下实跑通过
- [x] 根节点与排除项逻辑不回退

### 阻塞项

- `file_tree` 搜索过滤已不再由宿主裁剪 items，但 compat shell 内仍有一层把 items 规范化为 DOM 行的过渡逻辑
- 选择 truth 对宿主已变成 renderer event snapshot，但 compat runtime 内部仍有 TS 选择/拖放/菜单 DOM 状态机，仍未形成完全独立的 Rust family 正式模块
- `file tree` 复杂拖放 / 粘贴 / 上传态的执行仍依赖宿主 bridge；内部 drop、删除、粘贴目标、外部上传目标 preflight 已下沉 Rust，过滤态已不再强制回退 React fallback
- `picker` 搜索态虽已进入 same-origin compat host，但当前仍是 compat shell + inline override，不是最终 Rust renderer / 正式搜索合同
- `picker` 键盘高亮链已补组件回归、shell query/state 测试与 `3000` 主站真实 smoke
- 当前剩余差距不在“键盘能否工作”，而在“仍是 compat shell + 宿主状态机，而非 final Rust renderer”
- final renderer 与宿主收薄已转入 `4-11 Phase F/G`

### 验收口径

- `file tree` 不再依赖当前 React `FileTree` 作为主路径 renderer
- `picker` 复用统一 renderer 家族
- 宿主不再承担主要树域交互状态机
- 当前实现距离该验收口径仍有明确差距：
  - same-origin compat host 已接入
  - `picker` 搜索态已进入 compat host，但仍不是最终 Rust renderer
  - `file tree` 的内部拖放 / 外部上传 / `asset-folder` 最小语义与搜索 projection 已接入，但还停留在 compat shell + 宿主 bridge
  - 还不能宣称 `Phase E` 已完成

---

## 9. 跨阶段通用回归要求

每一阶段切流前都必须至少补齐以下回归：

- [x] 页面树切页 smoke
- [x] 页面树展开/折叠 smoke
- [x] 页面树拖拽 smoke
- [x] 页面树右键菜单 smoke
- [x] 文件树多选与范围选 smoke
- [x] 文件树双击打开 smoke
- [x] 文件树内部拖放 smoke
- [x] 文件树外部文件拖入 smoke
- [x] picker 搜索 / 空态 / 选中 smoke
- [x] stream 中断 fallback smoke

要求：

- 不只验证“组件能渲染”
- 要验证“关键交互能力未退化”

补充口径（2026-04-24）：

- 当前以上 10 项由 `scripts/task112-tree-rust-family-regression-smoke.js` 实跑覆盖：宿主页负责切页、右键菜单、文件树双击打开、picker 与 stream fallback；`/api/tree/shell` 直连页负责页面树展开/折叠/拖拽，以及文件树多选、范围选、内部拖放、外部文件拖入。
- 其中页面树 / 文件树拖放与 picker 当前主要验证 same-origin compat shell、宿主 bridge 与命令回写链路；宿主完全收薄后的 final renderer 端到端 smoke，仍应在 Phase D / E 收尾时继续补强。

---

## 10. 当前最近一步

按当前优先级，最近一步建议固定为：

1. 先补 `Phase A`，把 `file_tree` Rust 直接输出缺口补齐。
2. 同时推进 `Phase B`，把主路径命令面收口到 `tree.*`。
3. 在这两件事没有完成前，不建议宣称“树域主执行面已经可以正式切到 Rust 家族 renderer”。

原因：

- 如果 `file_tree` 语义还要靠前端补
- 如果命令面还主要停在 compat

那么换 renderer 只会把现有双轨问题搬进新壳，而不是收掉它。

---

## 11. 完成判定

只有同时满足下面几条，才可以宣称“树域 Rust 家族化这一轮完成”：

- `page tree / file tree / picker` 主路径 renderer 已进入 Rust 家族
- `file_tree` 已由 Rust 直接输出正式对象投影
- 主调用路径默认走 `tree.*`
- realtime 已进入正式 `snapshot + delta + resync` 合同
- 宿主只保留极薄挂载与桥接职责
- `page tree / file tree` 的现有能力没有因迁移退化

---

## 12. 一句话收口

这轮工作的正确推进方式不是：

- “先把现有 React 组件翻译成 Rust”

而是：

> **先补齐 `file_tree / command / realtime` 三条主链，再以能力不退化为硬门槛，逐步把 `page tree / file tree / picker` 的主 renderer 和主交互骨架迁入 Rust 家族。**
