Files
mnote/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md

659 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 SSENext 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 hint3000 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` routeSidebar 搜索态只消费该 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 JSONcompat 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__` 注入第二份 itemsReact 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 家族。**