# 4-11 [done] 树域 Rust 家族 final renderer 与宿主收薄清单 v1

> 更新时间：2026-04-26
>
> 前置文档：
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md`
> - `/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/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
>
> 口径覆盖（2026-04-28）：此前 `rust_runtime_artifact_host`、Rust/WASM reducer runtime、Rust initial DOM 与 `task112/task113` smoke 通过，只能证明 reducer/runtime 阶段完成，不能证明 final Rust DOM renderer 完成。真正 final renderer 的硬门禁以 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准：默认真实流量必须不再依赖 `TreeShellIframeHost` 的 `iframe_srcdoc` DOM shell。
>
> 收口更新（2026-04-28）：`task-010` 至 `task-013` 已完成上述 final DOM shell 硬门禁。`rust_family` 默认真实流量已切到 `rust_wasm_dom_shell_host` + `dom_wasm` bridge。
>
> 最终清理更新（2026-05-16）：`TreeShellIframeHost` 与对应测试已移动到 `recycle/`，`NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST` 入口已删除；active source 不再保留 iframe/srcDoc legacy/debug host。

## 1. 当前已收口基线

- `3000` 仍是唯一浏览器公开入口。
- `page tree / file tree / picker` 默认主路径已进入 `rust_family` 3000 `rust_wasm_dom_shell_host`。
- `3104` tree shell proxy 已退到显式 debug/internal 边界；默认主站 iframe 不再请求 `/api/tree/shell -> mnote-web:3104`，避免 `desktop:hot` 下页面树/文件树显示 `{"error":"fetch failed"}`。
- `file_tree` 主路径输入已收口到 `kernel file_tree items`。
- `buildVisibleRows` 不再从 `pageRows + assets` fallback 重建文件树对象语义。
- `streamDelta` 已可写入 `command_logs` 与 `domain_events`。
- Rust SSE 与 3000 同源 SSE 都能从单条新 `domain_event.payload.streamDelta` 直接产出 `delta`。
- Rust `bridge-runtime` 已具备 `tree.subtree.move` 的可测试 canonical move order plan。
- TS transport 已把 Rust `normalizedMove` 透传到 Convex `documents.move` 的可选校验边界。
- Convex `documents.move` 在收到 `normalizedMove` 时会先确认当前排序 patch plan 与 Rust plan 一致，再执行既有兼容写入。
- Rust SSE 与 3000 同源 SSE 对同一 `command_id` 的 command log + domain event 双写做 delta 去重：delta 一致时发一次 `delta`，不一致继续 `resync`。
- `tree.subtree.move` 已从 `replace_documents` 过渡 delta 推进到 `move_document` 细粒度 delta：
  - Rust `bridge-runtime` 已输出 `streamDeltaHint(kind=move_document)` 与 `domainEventHint(tree.subtree.moved)`
  - 3000 `/api/tree/commands` 按 Rust plan hint + mutation result 物化并写入 `move_document`
  - 3000 SSE 与 Rust SSE 均保留 `documentId / parentId / sortOrder / updatedAt`
  - 前端 `tree-delta` reducer 可用该 delta 重建 `sidebar_tree / page_tree / file_tree`
- `tree.node.restore / tree.subtree.copy` 已从 `replace_documents` 过渡 delta 推进到细粒度 upsert：
  - `restore -> upsert_document`
  - `copy -> upsert_documents`
  - 前端 `tree-delta` reducer 可用 upsert delta 重建 `sidebar_tree / page_tree / file_tree`
- Rust `bridge-runtime` 已输出正式 tree domain event plan：
  - `tree.node.created / renamed / archived / restored / purged / embedded`
  - `tree.subtree.moved / copied`
  - 每条正式 tree command plan 都带 `domainEventPlan(schema=mnote.tree.domain_event, schemaVersion=1, eventType, streamDeltaHint)`
  - `recordBridgeCommandArtifacts` 会优先使用 Rust plan 透传的 event type / materialized streamDelta，而不是 `${command}.requested`
- `file_tree` 搜索过滤已接入 Rust projection query 主链：
  - `KernelProjectionFilter.query` 已进入协议
  - `KernelProjectionFilter.maxResults` 已进入协议
  - Rust `kernel.project_view(file_tree)` 已能输出命中项与必要祖先
  - Rust 搜索 projection 已按“直接命中项限流，必要祖先不计入上限”执行 `maxResults`
  - `mnote-web` `/api/tree/projections/file?...&query=` 与 3000 同源 `/api/tree/projections/file` route 已覆盖
  - `mnote-web` 与 3000 同源 route/client 已透传 `maxResults`
  - Sidebar 搜索态只消费 Rust 搜索 projection items，旧宿主裁剪 helper 已移除
- tree realtime 主链已补强：
  - command log 与 domain event 双写时，即使上一帧 cursor 来自另一条流，也会按时间排除旧 artifact 后再做同 `command_id` delta 去重
  - 未识别 domain event payload 继续保守 `resync`
  - TS artifact 写入边界已固定 `mnote.tree.domain_event` payload schema v1，并保留历史 snake_case 字段兼容 SSE
- resource command 主链已补强：
  - `tree.resource.copy / tree.resource.move` 已由 Rust `bridge-runtime` 生成 `resourceTransferPlan`
  - `tree.resource.upload` 已由 Rust `bridge-runtime` 生成 `resourceUploadPlan`
  - Convex `mediaAssets.batchCopy / batchMove / createWithStorage` 已按 Rust plan 做可选一致性校验
  - `tree.resource.copied / moved / uploaded` 已可生成 `asset_result -> upsert_assets` delta / domain event plan
  - 3000 media batch/upload route 已改为调用 Rust artifact writer，按 Rust artifact plan 持久化 `command_logs / domain_events`
  - Sidebar 对 copy / move / rename / delete / restore / upload 的资源请求已抽成 `file-tree/resource-command-client`
- file tree 内部 drop 已新增 Rust command preflight：
  - `tree.filetree.drop.preflight` 由 Rust `bridge-runtime` 归一化 target、row 去重、doc/asset 分类、mindmap subPath、copy/move action
  - Rust preflight 会拒绝页面移动到自身/后代
  - preflight plan 已输出 `documentTransferPlan` 与 `resourceTransferPlan`，Sidebar 内部 drop 主路径改为先消费 Rust plan 再触发既有 transport
  - 宿主仍负责实际命令触发、乐观 UI、副作用通知和错误提示
- file tree 删除目标已新增 Rust command preflight：
  - `tree.filetree.delete.preflight` 由 Rust `bridge-runtime` 归一化 row 去重、父页面覆盖过滤、doc/asset 删除目标
  - Rust delete plan 会在“选中父页面”时过滤其子页面附件，避免宿主重复构造 asset 删除列表
  - Sidebar 删除确认与执行主路径改为先消费 Rust delete plan，再触发既有 document/media transport
- file tree 粘贴目标与复制分类已新增 Rust command preflight：
  - `tree.filetree.paste.preflight` 由 Rust `bridge-runtime` 归一化剪贴板 row、目标页面、focused row、`doc/index` 复制递归语义、真实 asset 过滤和 mindmap `targetSubPath`
  - 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
  - `Sidebar` 在 `rust_family` 粘贴主路径先消费 Rust paste plan，再触发既有 `copyTreeCommand` / `copyFileTreeResourceAssets`
- file tree 外部上传目标已新增 Rust command preflight：
  - `tree.filetree.upload-target.preflight` 由 Rust `bridge-runtime` 归一化 drop target、focused row、active doc、目标 workspace 与 mindmap `targetSubPath`
  - 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
  - `Sidebar` 外部文件 drop 主路径先消费 Rust upload target plan，再执行既有文件字节读取与 upload transport
- Rust tree shell 已新增可测试 renderer/state family 基础合同：
  - `renderer_input` 覆盖 `page / filetree / picker` 的 projection items、expanded ids、focused id、selected row ids、active picker item、excluded ids、command dispatcher
  - `mnote-web` tree shell HTML 已嵌入 Rust 构建的 `rendererInput` JSON contract，供 compat host 与后续 final renderer 共用；compat shell 初始 expanded / selection / picker active / exclude 已优先读取该合同
  - 3000 默认 `TreeShellRustDomShellHost` 已对齐同一 projection model：`page / filetree / picker` 的 projection items 进入 DOM host model；`TreeShellIframeHost` 的 appState JSON 只保留在 legacy/debug host。
  - `rendererInput` 已继续暴露三类 reducer contract：`rust_page_focus_keyboard_reducer_v1`、`rust_filetree_selection_reducer_v1`、`rust_picker_state_reducer_v1`
  - `mnote-web /tree` debug shell 与 3000 legacy inline compat host 均已消费同名 reducer contract；当前默认 DOM host 已切到 `TreeShellRustDomShellHost`，并通过 WASM artifact 或同源 Rust reduce seam 执行 runtime。
  - 前端 `TreeShellHost` 已暴露 `data-tree-renderer-contract=rust_renderer_input_v1`，主路径 `data-tree-host-implementation=rust_wasm_dom_shell_host`；`mnote_web_iframe_proxy`、`rust_inline_compat_host` 与 `rust_runtime_artifact_host` 不再代表默认主执行面
  - `filetree_selection` 覆盖单选、多选、Shift 范围选、右键选中、清空、visible rows 归一化、drag row ids
  - `picker_state` 覆盖高亮归一化、上下/Home/End、排除项、Enter pick
  - `focus_state` 覆盖 focused id 归一化与 next/previous/home/end 行走
  - `expansion_state` 覆盖默认展开、toggle、活动节点祖先展开
  - `keyboard_state` 覆盖导航、打开、展开/折叠、上下文菜单 intent
  - `drag_drop_state` 覆盖拖拽 payload 去重归一化与 copy/move effect
  - `action_registry` 覆盖 page/filetree/picker 的可用动作集合
  - `page_renderer` 已输出 `data-rust-page-renderer=initial_v1` 的 page tree 首屏嵌套 HTML，并由 compat runtime 优先 hydrate 该 Rust DOM；后续状态变化仍保留 JS 重绘路径
  - `filetree_renderer` 已输出 `data-rust-filetree-renderer=initial_v1` 的 file tree 首屏嵌套 HTML；历史 3000 inline `srcDoc` 曾优先 hydrate 该 DOM，当前默认主路径已由 `TreeShellRustDomShellHost` 承载，legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
  - `picker_renderer` 已输出 `data-rust-picker-renderer=initial_v1` 的 picker 首屏 HTML；历史 3000 inline `srcDoc` 曾优先 hydrate root/item DOM，当前默认主路径已由 `TreeShellRustDomShellHost` 承载，legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
- Rust artifact 写路径已开始落地：
  - `bridge-runtime` 已新增 `RuntimeCommandArtifactPlan`，可从 Rust command plan + mutation result 物化 `commandLog` 与 `mnote.tree.domain_event` payload
  - `mnote-web /api/tree/commands` 已在 Rust transport 内持久化 `bridgeLogs.recordCommandLog / recordDomainEvent`
  - artifact 写入失败不会回滚已成功 mutation，响应会暴露 `artifactError` 供观测
  - 3000 `/api/tree/commands`、`/api/media/batch`、`/api/media/upload`、文档类 `page/save/lifecycle/metadata/page-write/documents-block` Rust transport adapter 已改为调用 Rust artifact writer
  - `documents.duplicate` 已补正式 `tree.node.duplicated` domain event plan，3000 duplicate 成功链不再回退 TS artifact helper
  - 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
- file tree selection 的宿主职责已继续收薄：
  - `rust_family` 下 `Sidebar` 不再把 legacy React file tree selection reducer 作为选择真相
  - `Sidebar` 只从 `tree.filetree.selection.changed` 事件物化 renderer selection snapshot，删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费该 snapshot
  - `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop，避免宿主反向控制 renderer selection
  - compat runtime 在 file tree 重绘与可见行归一化后会继续回发 selection snapshot，避免宿主业务消费旧镜像
  - compat runtime 的 Shift 范围选择与右键选中语义已对齐 Rust `filetree_selection` 合同：Shift 默认覆盖旧选择，Ctrl/Cmd+Shift 才叠加；右键已选中行保留多选集合

## 2. 已完成的 final DOM shell 硬边界与仍未完成项

- 2026-04-28 final DOM shell 硬门禁已完成：`TreeShellIframeHost` 的 `iframe_srcdoc` browser bridge 不再是默认 page tree / file tree / picker DOM shell。
- `page tree / file tree / picker` 3000 默认主路径已标识为 `rust_wasm_dom_shell_host`，默认浏览器 bridge 为 `data-tree-browser-bridge="dom_wasm"`。
- `TreeShellIframeHost` 已从 active source 移到 `recycle/`；旧 `LOCAL_TREE_SHELL_TEMPLATE` 与 `buildInline*Html` 不再参与 active 路径。
- 默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 WASM artifact 或同源 Rust HTTP seam 返回的 state、hostEvents、commandEvents；本地 JS fallback reducer 不再参与默认真实流量。
- 仍未完成的是非 DOM shell 范围：`tree.subtree.move` 的 `normalizedMove` compat fallback 删除、`document.snapshot.saved` 独立事件、`tree.node.embed` Page Aggregate/Rust artifact 深层收口等历史条目的后续核验；legacy iframe host 清理已完成。
- 宿主仍承担文件字节读取、外部上传 transport、菜单状态、实际命令调度、乐观 UI 和副作用通知；资源 upload/copy/move、内部 drop preflight、粘贴 preflight、外部上传目标 preflight 的目标与 metadata plan 已进入 Rust command contract。
- `tree.subtree.move` 的 canonical sort plan 已在 Rust 产出并进入 Convex 可选校验，但实际排序写入仍由 Convex `documents.move` 执行。
- `file_tree` 搜索过滤主链已改为 Rust projection query；`maxResults / index.md / 附件 / 导图子附件 / book/pdf` 扩展 fixture 已补齐，后续仅保留更丰富搜索语义 hardening。
- `replace_documents` 已退为 restore/copy 的 fallback；主路径 restore/copy 已是 `upsert_document(s)`。
- Rust command 已生成 tree/resource delta / domain event plan；`mnote-web /api/tree/commands`、3000 tree/media 主写入口与文档类 Rust transport adapter 已可由 Rust artifact writer 落 `command_logs / domain_events`；block/save 复合命令已形成 `page.body.saved`、`block.patched`、`block.moved`、`block.embedded` formal domain-event contract。
- block/save 复合命令已并入正式 Rust artifact 主链；OnlyOffice/media writeback 已切到 Rust artifact writer。`document.snapshot.saved` 是否作为独立事件继续留在 `4-16`，不阻塞本文件 final renderer / host thinning 收口。

## 3. Phase F：final Rust renderer 接入

### 目标

把 `page tree / file tree / picker` 从 `TreeShellIframeHost` 内联 DOM compat shell，迁到正式 Rust family renderer。

状态：默认主路径已完成迁移，legacy iframe host 已移入 `recycle/`，不再作为显式排障开关保留。

### checklist

- [x] 固定 Rust renderer 输入合同：
  - `projection items`
  - `expanded ids`
  - `focused id`
  - `selected row ids`
  - `active picker item`
  - `command dispatcher`
  - 当前已作为 `rendererInput` 嵌入 `mnote-web` tree shell HTML；runtime DOM shell 已优先读该合同初始化局部状态，但尚未完全改为只消费该合同。
  - 历史 3000 inline host 已把 `rendererInput` 与 `items` 注入同一个 appState；当前默认 DOM host 直接消费 projection model，不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 主路径注入第二份 items。
  - 默认 DOM shell 已固定 `family=rust_family / version=1 / executionStrategy=dom_wasm`，并优先使用 3000 同源 `tree-shell-runtime` artifact；active source 不再保留 `browserBridge=iframe_srcdoc` host。
  - runtime artifact manifest 已新增 `runtimeApi`，显式暴露 `TreeShellRuntimeRequest / TreeShellRuntimeResult`、state snapshot、DOM patch、host event、command event kind。
  - runtime artifact manifest 已继续暴露 `reduceEndpoint=/api/tree/runtime/reduce`；3000 同源 thin proxy 与 mnote-web endpoint 保留为 fallback/debug。
  - 正式 Rust/WASM reducer runtime 已落地：`tree-shell-runtime-wasm` 导出 `reduceTreeShellRuntime`，3000 主路径默认优先加载 `mnote-tree-shell-runtime.js` 与 `mnote-tree-shell-runtime_bg.wasm` 执行 reduce。
- [x] 拆出 renderer 内部模块：
  - [x] row model 雏形（page/filetree/picker renderer test id contract）
  - [x] page initial DOM renderer（首屏 page tree Rust HTML + hydrate contract）
  - [x] filetree initial DOM renderer（首屏 file tree Rust HTML contract）
  - [x] picker initial DOM renderer（首屏 picker Rust HTML contract）
  - [x] expansion model（默认展开 / toggle / 祖先展开 state family）
  - [x] focus model（focused id normalize / next / previous / home / end）
  - [x] selection model（filetree selection state family）
  - [x] picker state model（高亮 / Enter / exclude）
  - [x] keyboard model（导航 / 打开 / expand-collapse / context menu intent）
  - [x] drag/drop model（payload normalize / copy-move effect）
  - [x] action registry（page / filetree / picker action set）
  - [x] runtime facade（`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API，输出统一 DOM patch / host event / command event）
- [x] `page tree` 先切 final renderer。
  - 当前状态：默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOM，focus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。
- [x] `picker` 以轻量模式复用同一 renderer state family。
  - 当前状态：默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOM，keyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动，测试已禁止默认 iframe postMessage 成功路径。
- [x] `file tree` 后切 final renderer，并保留 `doc / index / asset-folder / asset` 能力。
  - 当前状态：默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOM，selection、context menu、open、internal/external drop 与 drop target 通过 runtime result/hostEvents 驱动，并保留 doc/index/asset-folder/asset row 口径。
- [x] compat host 退为 debug/internal fallback（本批可验证范围）。
  - 3000 默认主路径不再请求 `/api/tree/shell -> mnote-web:3104`，也不再回退旧 React renderer；host implementation 已切到 `rust_wasm_dom_shell_host`；`mnote-web /tree` 仅保留显式 debug/internal 验证边界，`TreeShellIframeHost` 已进入 `recycle/`。

### 验收

- 默认真实流量不再以 `rust_inline_compat_host`、`rust_runtime_artifact_host` 或 `TreeShellIframeHost` 作为主路径标识；page / picker / filetree 默认路径已不再依赖 `iframe_srcdoc` 内联 DOM renderer。
- `data-tree-host-implementation` 不再以 `mnote_web_iframe_proxy` 代表主执行面。
- `page tree / file tree / picker` smoke 仍覆盖打开、键盘、选择、拖放、菜单。

## 4. Phase G：宿主状态机收薄

### 目标

宿主只保留挂载、认证、文件读取、transport bridge，不再解释树域交互合法性。

### checklist

- [x] 上传执行从 sidebar 宿主抽成正式 resource command。
  - `tree.resource.upload` 已进入 Rust plan / domain event hint / `asset_result` delta
  - 浏览器文件字节读取与 Convex upload URL 仍由 3000 upload route 执行
- [x] 内部 file tree drop payload 归一化进入 Rust command preflight。
  - `tree.filetree.drop.preflight` 已接 3000 同源 route `/api/tree/filetree/drop-preflight`
  - 前端 `file-tree/shell` 只负责从 projection row / parent snapshot 构造 preflight payload
  - Rust plan 输出 target、mindmap subPath、doc/asset 分类、top-level doc、source asset documents、`documentTransferPlan`、`resourceTransferPlan`
- [x] copy/move 区分与非法投放校验进入 Rust command/projection contract。
  - `tree.filetree.drop.preflight` 已按 `copy` 输出文档/资源 transfer plan
  - 非 copy 的页面移动已在 Rust preflight 拒绝自身/后代投放
  - 更完整的跨工作空间/混合对象权限校验仍随正式 Rust 写路径继续下沉
- [x] file tree 删除目标归一化进入 Rust command contract。
  - `tree.filetree.delete.preflight` 已接 3000 同源 route `/api/tree/filetree/delete-preflight`
  - `Sidebar` 删除链已先消费 Rust delete plan，再执行既有 `deleteDocumentCommand` / `deleteFileTreeResourceAssets`
  - legacy `computeFileTreeShellDeleteTargets` 已退出生产代码，只保留 Rust preflight payload builder
- [x] file tree 粘贴目标推导与复制对象分类进入 Rust command contract。
  - `tree.filetree.paste.preflight` 已接 3000 同源 route `/api/tree/filetree/paste-preflight`
  - Rust paste plan 已输出 `docItems` 与 `resourceTransferPlan`，覆盖 `doc -> recursive true`、`index -> recursive false`、真实 asset 过滤和 mindmap `targetSubPath`
  - `Sidebar` 的 `rust_family` 粘贴链已不再本地推导 `docItemsMap / copyableAssetIds`，只消费 Rust paste plan 后触发既有 transport
- [x] file tree 外部上传目标推导进入 Rust command contract。
  - `tree.filetree.upload-target.preflight` 已接 3000 同源 route `/api/tree/filetree/upload-target-preflight`
  - Rust upload target plan 已输出 `workspaceId / targetDocumentId / targetMindmapId / targetSubPath`
  - `Sidebar` 的外部文件 drop 链已不再本地推导 target row / focused row / workspace fallback，只消费 Rust upload target plan 后执行既有 upload transport
- [x] 多选、范围选、右键选中状态进入 renderer state family。
  - Rust `filetree_selection` 已有纯状态测试；`mnote-web /tree` 与 3000 legacy inline compat runtime 均已通过 `rust_filetree_selection_reducer_v1` 合同入口执行选择、右键、可见行归一化与 drag rows 解析；当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] picker 高亮、Enter 选中、排除项规则进入 renderer state family。
  - Rust `picker_state` 已有纯状态测试；`mnote-web /tree` 与 3000 legacy inline compat runtime 已通过 `rust_picker_state_reducer_v1` 合同入口执行 next/previous/home/end/pick/focus；当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] 宿主不再持有 file tree 选择真相，只订阅 renderer state event。
  - `Sidebar` 已拆分 legacy selection 与 renderer selection snapshot；`rust_family` 下只有 `handleFileTreeShellSelectionChange` 写入 renderer snapshot。
  - `SidebarTreeSurface` 已移除 `selectedRowIds` 控制输入，file tree 选择只能由 renderer event 回报给宿主业务。
  - 仍未完成：compat runtime 内部选择算法虽已统一为 `rust_filetree_selection_reducer_v1` 合同入口，但尚未替换为 Rust runtime/wasm 直接执行，继续归入 Phase F final renderer / runtime 收口。

### 验收

- sidebar 宿主不再包含主要 file tree selection truth 分支；drop legality 仍通过 Rust preflight + 宿主 transport 过渡执行。
- 上传和资源移动失败能返回稳定 command error，不依赖前端临时判断。
- renderer state 可独立测试，不需要挂载整个 sidebar。

## 5. Phase H：Rust 搜索 projection

### 目标

`file_tree` 搜索过滤不再由宿主裁剪 `kernel file_tree items`，而是由 Rust 输出搜索 projection。

### checklist

- [x] 定义 `file_tree.search` 基础输入：
  - query
  - root node
  - include ancestors
  - include assets
- [x] 补齐 `file_tree.search` 扩展输入：
  - `index.md` 显式作为可搜索资源；仅自身命中时进入结果，祖先仅用于维持路径
  - `maxResults` 限制直接命中项，必要祖先不计入上限
- [x] 定义搜索结果展开策略：
  - 命中页祖先展开
  - 命中 asset-folder 展开
  - 空结果稳定空态
- [x] Rust projection fixture 覆盖：
  - mindmap folder
  - table
  - 命中项必要祖先
- [x] 补齐搜索 fixture 扩展覆盖：
  - `index.md`
  - 附件
  - 导图子附件
  - book/pdf
- [x] 前端只消费搜索 projection items，不再计算 visible document ids。

### 验收

- 搜索态、空态、正常态都由同一 Rust projection contract 驱动。
- 前端不再持有搜索过滤的对象语义。
- Phase H 当前验收已满足；后续 richer search semantics 作为 hardening，不再阻塞主链。

## 6. Phase I：move 排序执行下沉

### 目标

把 `tree.subtree.move` 的排序执行从 Convex compat mutation 迁到 Rust kernel command 主链。

### checklist

- [x] Rust 已有 canonical move order plan 纯函数。
- [x] Rust command plan 已可输出 `normalizedMove`。
- [x] Convex `documents.move` 增加可选校验：若传入 Rust `normalizedMove`，执行前确认 patch plan 一致。
- [x] TS transport 对 `documents:move` 透传 `normalizedMove`，避免 Rust plan 停在不可观测状态。
- [x] Rust command plan 已产出正式 `tree.subtree.moved` domain event hint。
- [x] `mnote-web /api/tree/commands` Rust 写路径已可直接持久化正式 `tree.subtree.moved` domain event。
- [x] 前端/stream 不再由 Next route 写 `replace_documents` 作为 move 主 delta。
- [x] 细粒度 move delta reducer 同时更新 `sidebar_tree / page_tree / file_tree`。
- [x] `move_document` 不再由 Next route 按 action 手写；当前由 Rust `streamDeltaHint` + mutation result 物化。
- [x] `mnote-web /api/tree/commands` 的 `move_document` artifact 已由 Rust transport 直接生成/落库。
- [x] 3000 Next 主写入口已切到 Rust artifact writer。

### 验收

- sort order clamp、同父移动、跨父移动、重复 sort_order、null sort_order、created_at tie-break 均由 Rust 测试锁定。
- Convex 只作为存储执行层，不再持有唯一排序规则。

## 7. Phase J：正式 delta 主链

### 目标

树域 realtime 从 `replace_documents` 过渡 delta，推进到 Rust domain event 驱动的细粒度 delta。

### checklist

- [x] domain event 可携带并被 SSE 解释 `streamDelta`。
- [x] command log 与 domain event 同一 `command_id` 且 `streamDelta` 一致时，SSE 发一次 `delta` 而不是退回 `resync`。
- [x] 定义正式 domain event type hint：
  - `tree.node.created`
  - `tree.node.renamed`
  - `tree.node.archived`
  - `tree.node.restored`
  - `tree.node.purged`
  - `tree.node.duplicated`
  - `tree.subtree.moved`
  - `tree.subtree.copied`
  - `tree.resource.copied`
  - `tree.resource.moved`
  - `tree.resource.uploaded`
- [x] 定义正式 domain event payload schema（TS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1）
- [x] Rust 侧由 command plan 生成 delta / event hint，Next route 不再按 action 拼主 delta / event type。
- [x] `mnote-web /api/tree/commands` Rust 写路径已由 command result 直接生成并持久化 domain event。
- [x] 显式 compat adapter 写入口已迁出 TS artifact transport。
- [x] block/save-snapshot 复合命令已补齐正式 Rust artifact / domain-event contract。
  - 当前完成口径：`page.body.saved` payload 已携带 snapshot 摘要，`block.patched / block.moved / block.embedded` 已有 formal domain-event contract。
  - `document.snapshot.saved` 是否拆成独立事件继续留在 `4-16`，不阻塞本阶段收口。
- [x] 前端 reducer 支持细粒度 move。
- [x] 前端 reducer 支持细粒度 restore/copy：
  - `upsert_document`
  - `upsert_documents`
- [x] 前端 reducer 支持资源 upsert delta：
  - `upsert_assets`
- [x] 未识别事件继续保守 `resync`。

### 验收

- 新树命令默认有 domain event delta。
- `replace_documents` 仅作为 fallback，不再是 move / restore / copy 主路径。

## 8. 不做事项

- 不为了“看起来 Rust 化”把 React/DOM compat shell 原样翻译成另一层大壳。
- 不在 sidebar 继续新增树对象真相。
- 不把 `3104` 恢复成默认浏览器入口。
- 不在 Next route 继续扩写长期树命令语义。
