# 前端编辑器 / 树体验实现偏差审查

> 执行状态：已归档到 `design/10-review/done/`。本文保留为历史审查快照，当前活跃口径以 `./06-execution-checklist-and-acceptance.md` 为准。

## 范围

本报告只审查 `wolai-frontend` 当前文档页、`leptos-tiptap` island host、`Page Aggregate` 消费链、Sidebar / tree shell / tree stream 与设计文档的一致性。

重点对照设计：

- `ARCHITECTURE.md`
- `design/01-05-current-priority-overview.md`
- `design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
- `design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
- `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
- `design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md`
- `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- `design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`

重点读取代码：

- `wolai-frontend/src/app/(app)/documents/`
- `wolai-frontend/src/components/editor/`
- `wolai-frontend/src/lib/documents/`
- `wolai-frontend/src/components/sidebar/`
- `wolai-frontend/src/lib/tree-stream/`
- `wolai-frontend/src/lib/documents/tree-command-client.ts`

## 结论

当前实现总体方向与最新设计主线一致：文档页 SSR 入口已消费 Rust `mnote.page_aggregate.v1`，默认主编辑器已折返到页面内 `leptos_tiptap_island`，tree command 前端主调用已进入 `/api/tree/commands` + `tree.*` 元数据口径，Sidebar 的默认 tree shell 已切到 `rust_wasm_dom_shell_host`。2026-05-16 后，历史 `iframe_srcdoc` host 与显式 legacy flag 已从 active source 移除并进入 `recycle/`。

但不能把当前状态描述为“前端编辑器 / 树体验已经完全收口到 Rust 单一真源”。主要未完成点集中在三处：页面本地 aggregate reducer 仍承担临时真相与补偿选择，`pageSubtree` 在本地正文变更后会被置空而不是形成同一份可持续 projection，Sidebar 仍通过 initial / query / tree stream 三源 freshness 选择维持一致性。此外，部分页面设置仍明确是 `planned` / `downgrade`，属于设计清单中尚未完成的范围。

## 关键发现表格

| 编号 | 分类 | 严重度 | 发现 | 证据 |
| --- | --- | --- | --- | --- |
| F-01 | 未完成 | 中 | Page Aggregate 读链已 Rust-first，但客户端仍有本地 aggregate reducer 持有标题、正文、设置、子树快照等临时真相；设计中的“页面域单一真源”尚未闭环。 | `wolai-frontend/src/components/editor/page-aggregate-client-state.ts:7` 定义本地 `PageAggregateClientState`，包含 `serverPageTitle / persistedPageTitle / draftPageTitle / options / content / serverPageSubtreeSnapshot / contentRevision / conflictDetectionKey`；`wolai-frontend/src/components/editor/document-content.tsx:144` 使用 reducer 作为文档页核心状态。 |
| F-02 | 实现偏差 | 中 | `pageSubtree` 不是跟随本地正文和标题持续更新的 page projection；一旦本地正文对象与服务器快照不同，就直接返回 `null`，AI 面板和阅读结构面会失去这份 projection。 | `wolai-frontend/src/components/editor/page-aggregate-client-state.ts:163` 通过 `content === serverContentSnapshot` 判断；`wolai-frontend/src/components/editor/page-aggregate-client-state.ts:166` 只有未改动时返回 `serverPageSubtreeSnapshot`，否则 `:169` 返回 `null`；`wolai-frontend/src/components/editor/document-content.tsx:867` 将该选择结果作为 AI snapshot 的 `pageSubtree` 来源，`:1162` 也传给阅读视图。 |
| F-03 | 未完成 | 中 | Sidebar / Breadcrumb / 文档页头已共享 preferred snapshot，但 live cache 仍不是唯一来源；当前仍在 initial、query refetch、tree stream 之间做 freshness 选择。 | `wolai-frontend/src/components/app-layout-shell.tsx:20` 同时创建 `useSidebarData` 与 `useSidebarTreeStream`；`:22` 用 `usePreferredSidebarSnapshot` 选择；`wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts:29` 在 `query / tree_stream / initial` 间选择；`wolai-frontend/src/components/sidebar/sidebar.tsx:201` Sidebar 内部也消费同样选择结果。 |
| F-04 | 未完成 | 低 | 页面设置运行时语义已有代码级分类，但仍有正式 UI 中可见的 planned / downgrade 项；这与 `5-6` 清单中“页面设置未整体收口”的口径一致。 | `wolai-frontend/src/lib/documents/page-option-semantics.ts:60` 将 `protectEditing` 标记为 `planned`，`:84` 将 `showBlockRefCount` 标记为 `planned`；`wolai-frontend/src/components/editor/page-options-sidebar.tsx:416` 对 downgrade 项显示 `待接线`，`:71` 让 `showBlockRefCount` 成为不可交互占位。 |
| F-05 | 方向变化/文档滞后 | 低 | `5-5-1` 中仍写着 `showHeadingNumbers / embedDefaultBlockId` 只完成字段贯通，但当前代码已把它们纳入 island runtime payload；这里更像文档滞后，而不是实现偏差。 | `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md` 第 2.3 节仍说明两项“不可描述成正式支持”；`wolai-frontend/src/lib/documents/page-option-semantics.ts:48` 与 `:88` 均标记为 `wired`；`:121` 的 `pickLeptosTiptapRuntimePageOptions` 会把 `showHeadingNumbers / embedDefaultBlockId` 传给 island，`wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx:717` 运行时更新这些选项。 |
| F-06 | 已收口 | 低 | Tree final DOM shell 默认路径已符合 `4-18`，legacy iframe host 与环境变量回退入口已删除；后续只需保持负向 smoke，防止 active source 重新引入 `iframe_srcdoc`。 | `wolai-frontend/src/components/sidebar/tree-shell-host.tsx` 默认 `rust_family` 且有 workspace 时只使用 `TreeShellRustDomShellHost`；`wolai-frontend/src/components/sidebar/tree-shell-host.test.tsx` 覆盖默认不能进入 `iframe_srcdoc`。 |

## 证据

### 已对齐部分

1. 文档页读取主链已进入 Rust Page Aggregate。
   - `wolai-frontend/src/app/(app)/documents/[id]/page.tsx:51` 调用 `loadPageAggregateFromNextHeaders`。
   - `wolai-frontend/src/lib/documents/page-aggregate-loader.ts:170` 请求 `/api/page-aggregate/:id`。
   - `wolai-frontend/src/lib/documents/page-aggregate-loader.ts:130` 校验 schema 必须是 `mnote.page_aggregate.v1`。
   - `wolai-frontend/src/lib/documents/page-aggregate-loader.ts:225` 的 `loadPageAggregate` 只返回 Rust snapshot，不再在运行时 fallback 到 TS builder。

2. 默认编辑器 host 已是页面内 `leptos_tiptap_island`。
   - `wolai-frontend/src/components/editor/editor-host-config.ts:1` 只保留 `EditorHostKind = "leptos_tiptap_island"`。
   - `wolai-frontend/src/components/editor/editor-host.tsx:7` 动态加载 `leptos-tiptap-island-editor-host`。
   - `wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx:619` 直接调用 runtime module 的 `mount(container, ...)`，不是 iframe。

3. 页面写命令已经开始按 page command family 收口。
   - `wolai-frontend/src/lib/documents/page-command-contract.ts:15` 固定 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。
   - `wolai-frontend/src/lib/documents/page-command-client.ts:72` 标题写入发送 `commandName: page.head.updateTitle`。
   - `wolai-frontend/src/lib/documents/page-command-client.ts:89` 页面设置写入发送 `commandName: page.layout.updateOptions`。
   - `wolai-frontend/src/app/api/documents/save/route.ts:35` 服务端 envelope 使用 `PAGE_COMMAND_NAMES.saveBody`。

4. tree command 前端调用主面已以 `tree.*` 为结果元数据口径。
   - `wolai-frontend/src/lib/documents/tree-command-client.ts:26` 定义 `tree.node.create / tree.node.rename / tree.subtree.move / tree.node.archive` 等 preferred command。
   - `wolai-frontend/src/lib/documents/tree-command-client.ts:185` 统一 POST 到 `/api/tree/commands`。
   - `wolai-frontend/src/app/api/tree/commands/route.ts:205` 由 `action` 分发 tree command，`:278` 创建命令使用 `tree.node.create`。

5. tree final DOM shell 默认路径已符合硬门禁。
   - `wolai-frontend/src/components/sidebar/tree-shell-host.tsx:139` 默认 implementation 为 `rust_wasm_dom_shell_host`。
   - `wolai-frontend/src/components/sidebar/tree-shell-surface.tsx:113` 旧 React page tree renderer 已显示为 removed fallback，不再作为正常 renderer。
   - `wolai-frontend/src/components/sidebar/tree-shell-host.test.tsx:189` 测试断言默认 implementation 是 `rust_wasm_dom_shell_host`，`:194` 断言没有 iframe host。

### 仍需收口部分

1. 客户端 `PageAggregateClientState` 仍是混合态。
   - 它把 server snapshot、persisted title、draft title、本地 content、server subtree snapshot、revision/conflict key 放在同一个前端 reducer 中。
   - 这符合当前过渡态，但不等于 Rust page aggregate 已成为页面域唯一运行时真相。

2. `pageSubtree` 与正文编辑没有同源更新。
   - 本地正文变更只会触发 `apply_local_content_snapshot`，不会同步生成新的 `pageSubtree`。
   - selector 在内容对象不同于 server snapshot 时返回 `null`，因此 `AI / read view / TOC` 只能等待后续持久化与重新拉取。

3. Sidebar live cache 仍有 freshness 选择层。
   - `AppLayoutShell` 和 `Sidebar` 都依赖 `usePreferredSidebarSnapshot`。
   - 当前算法没有基于 Rust stream cursor / projection version 做统一仲裁，而是通过 sync key 与 `treeStreamStatus` 在 `query` 与 `tree_stream` 之间选择。

4. 页面设置中仍有显式未完成项。
   - `protectEditing` 与 `showBlockRefCount` 的代码状态已经诚实标为 `planned / downgrade`。
   - 这避免了误导用户，但也说明 `Page Aggregate -> island runtime` 的设置语义还没有完全完成。

## 建议优先级

### P0

暂无需要立即阻断主链的 P0。默认 Page Aggregate 读链、默认 island host、默认 DOM tree shell 均未发现与最新主线相反的实现。

### P1

1. 收口 `pageSubtree` 与本地正文编辑的关系。
   - 至少明确它是“server projection only”还是“本地编辑也应生成临时 projection”。
   - 如果 AI 面板需要稳定结构上下文，不应在用户正常编辑后直接拿到 `null`。

2. 将 Sidebar 的三源 preferred snapshot 推进为统一 live cache。
   - 建议以 Rust stream cursor / projection version / query snapshot version 为仲裁字段，减少仅靠 sync key 的 freshness 判断。

### P2

1. 继续移除或强门禁 legacy tree iframe host。
   - 当前显式 env flag 符合设计，但应保留负向 smoke，避免后续默认路径误回退。

2. 同步修正文档滞后。
   - `5-5-1` 对 `showHeadingNumbers / embedDefaultBlockId` 的描述已经落后于当前代码，应在后续文档整理时更新。

3. 页面设置 planned 项保持降级展示，直到真正进入 island runtime 或被产品侧移除。

## 修改的文件

- `/mnt/Data1T/mnote/design/10-review/done/02-frontend-editor-tree-review.md`
