# 5-5 [process] 主编辑区与树域单一真源对齐方案 v1

> 更新时间：2026-05-09
>
> 关联文档：
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-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-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`

## 1. 文档目的

这份文档只回答一个当前已经暴露出来的主线问题：

> **当 `leptos-tiptap` 已经成为页面内正式主编辑区后，页面树 / 文件树 / 页面标题 / 页面设置 / 页面正文，是否已经统一成 Rust 主导的单一真源架构。**

当前结论是：

> **还没有。**

当前系统已经不再是：

- `iframe runtime`
- 纯前端假接入
- 纯 `BlockNote` 主链

但也还不是：

- Rust 持有页面聚合真相
- Leptos island 只消费 Rust projection
- 树域与主编辑区共享同一份 page aggregate

它现在更接近于：

> **`Rust snapshot 主读链 + Convex-backed + 前端本地 state 仍存在` 的混合态。**

因此，当前看到的这些“小问题”：

- 标题单一真源问题已经开始收口，但页面聚合仍未完全统一
- 页面宽度和主编辑区宽度看起来不是一套规则
- 页面设置大部分只改了壳层，没有真正进入主编辑器

并不是孤立 UI bug，而是同一个底层断层的表现：

> **树域 projection 与主编辑区 page runtime 之间，还缺一个统一的 page aggregate 真相层。**

---

## 2. 先给结论

### 2.1 当前不能说已经实现了 Rust 单一真源

虽然当前文档页已经：

- 默认使用页面内正式 `leptos-tiptap` island
- 不再依赖外部 iframe bridge 作为主编辑器
- 正文保存已经能按 `workspaceId/documentId` 绑定到正确页面

但以下事实仍然成立：

- 读取主链已固定为 Rust `mnote.page_aggregate.v1` snapshot，不再通过 TS builder 兜底
- 标题、正文、页面设置虽已开始收口到同一组 page aggregate command family，但 projection 回流与运行时语义还没完全闭环
- 页面树 / 文件树与页头标题的一致性已明显改善，但页头标题、页面设置与编辑器 runtime 还没有完全共享同一份聚合真相
- `pageOptions` 已部分进入 `leptos-tiptap` 运行时语义层，但还没有整体收口完成

所以当前不能把这条线描述成：

> **“主编辑区、页面树、文件树已经统一为 Rust 下唯一真源”。**

更准确的表述应该是：

> **树域已经在向 Rust canonical contract 靠拢，但文档页主编辑区仍处于页面聚合尚未完成的混合切流阶段。**

### 2.2 当前最该收口的不是换编辑器，而是统一页面聚合边界

当前不该重新争论：

- 要不要 `Tiptap`
- 要不要 `leptos-tiptap`
- 要不要继续保留 Convex

这些结论都已经足够明确：

- `Tiptap` 继续作为浏览器输入 runtime
- `leptos-tiptap` 继续作为 Leptos 内正式 editor island 接缝
- `Convex` 继续作为当前存储 / 实时 / 协作底座

当前真正要收口的是：

> **页面这一层，到底由谁持有聚合语义，前端到底应该消费什么，编辑器到底应该回发什么。**

### 2.3 后续主线应固定为 Page Aggregate，而不是继续零散补洞

从长期架构看，当前主线不应再描述成：

- “继续补几个 `leptos-tiptap` 细节”
- “再把标题同步修一下”
- “再把页面设置接一点进去”

后续主线应改写成：

> **建立 Rust 主导的 `page aggregate`：让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入，都围绕同一组 page projection 与 page command family 运转。**

---

## 3. 当前问题的精确判断

## 3.1 当前页面数据并不是一个统一聚合

当前文档页读取主链已经固定走 Rust `mnote.page_aggregate.v1` 快照：

- Rust `/api/page-aggregate/:id` snapshot
- 页面本地 aggregate state reducer
- preferred sidebar snapshot / 页头标题补偿链

需要特别区分“Rust route 主读链”和“完整 kernel-native projection”：当前 `/api/page-aggregate/:id` 已经由 Rust route / Rust runtime adapter 对外提供稳定 `mnote.page_aggregate.v1` 契约，前端不再恢复 TS runtime builder 主链；但底层 substrate 仍主要来自 `documents:getMeta + documents:getContent` 的兼容聚合。因此当前 projection provenance 应标记为 `CompatMetaContentJoin`，不能写成已经完全由 Rust kernel 原生投影闭环的 `KernelProjection`。

这意味着当前已经不再是：

- `page.tsx` 手工拉 `meta + content` 再现场拼 props

但也还不是：

- 页面所有读写、标题回流、页面设置运行时语义都只围绕一份 Rust page aggregate 自动闭环

这条链本身已经说明：

> **当前页面已经进入 Rust-first 聚合读取阶段，但 canonical page projection 仍未成为页面域唯一真相。**

### 3.1.1 标题链已开始收口，但尚未完成页面聚合统一

当前页头标题已经不再单纯由 `DocumentContent` 内部的长期本地 `pageTitle` 真相驱动，而是开始依赖与 sidebar / breadcrumb 共享的 preferred sidebar snapshot。

这带来两个直接后果：

1. 标题不同步这一类“前端本地状态长期漂移”问题已经明显减少
2. 但标题仍然没有和正文、页面设置一起统一到单一 page aggregate command / projection 边界中

于是：

- 页头、breadcrumb、sidebar、page tree、file tree 的标题一致性已有一轮真实收口
- 但从聚合语义上看，标题仍是一条相对独立的更新链，不等于 page aggregate 已经完成

这类问题不是“防抖没配好”，而是：

> **标题更新仍然是页面聚合之外的一条独立 side effect 链。**

### 3.1.2 正文和页面树仍然不是同一份 projection family

当前正文编辑器使用的是：

- `leptos-tiptap` runtime
- `/api/documents/save`
- `EditorBlockDocument / Tiptap / blocks` 的转换边界

而页面子树 / outline / evidence 使用的是：

- `pageSubtree`
- 前端本地定义的 TS 结构
- “内容与标题未变化时复用旧快照”的策略

这说明当前主编辑区与树域虽然已经能同时工作，但它们仍然不是：

- 同一个 Rust projection family 的不同视图

而是：

- 一边是编辑器保存链
- 一边是页面子树附带快照

因此当前 `pageSubtree` 更像：

- 页面阅读态和 TOC 的辅助投影

而不是：

- 主编辑区与树域共享的 canonical page aggregate

### 3.1.3 页面设置已部分进入 island，但还不是完整 editor runtime 语义

当前页面设置里至少有三类配置：

#### A. 页面壳层布局类

- `wideLayout`
- `smallText`
- `layoutDensity`

#### B. 页面视图类

- `showToc`
- `collapseBacklinks`
- `hideChildPages`

#### C. 编辑器语义类

- `showHeadingNumbers`
- `showBlockRefCount`
- `embedDefaultBlockId`

现在的主要问题不是“这些字段没有存下来”，而是：

> **它们虽然能持久化到 documents 表，也已有一部分进入 `leptos-tiptap` island，但还没有统一进入正式 page aggregate，更没有完整地进入 editor runtime 语义层。**

结果就是：

- 一部分设置已经作用于 island 布局或排版
- 一部分设置只影响阅读态
- 一部分设置被展示出来，但主编辑器内部几乎不消费

这也是为什么当前会出现：

> **页面设置看起来像是真的，但很多只是 UI 层局部生效。**

### 3.1.4 所谓“Rust 路径”里仍有明显 compat 痕迹

当前 rename / move / archive 等页面操作，虽然已经开始声明：

- `preferredCommandName`

但真实落地仍大量停留在：

- `documents.title.update`
- `documents.move`
- `documents.delete`

这说明当前并不能说：

- 树域命令已经完整切到 Rust tree command family

更不能说：

- 页面编辑域已经和树域命令收敛到同一个 page aggregate contract

因此现状应判定为：

> **Rust bridge 已经进入主链，但 page aggregate command cutover 仍未完成。**

---

## 4. 为什么这些问题会直接影响长期方向

## 4.1 它会削弱 Rust + Leptos 的真正优势

你们要的不是：

- “一个能跑的 Tiptap 页面”

而是：

- Rust 掌握页面与树的真相
- Leptos 成为页面内正式 editor island
- AI 最终直接接入主编辑区写作

如果标题、正文、设置、树节点标题分别走不同链路，那么 Rust + Leptos 的长期优势就会被削弱成：

- 只是“比纯前端多了一层 bridge”

而不是：

- “由 Rust 持有统一对象语义，前端只消费真相投影”

### 4.1.1 AI 直接写主编辑区会缺乏稳定入口

如果后续 AI 要直接进入主编辑区编写，而当前系统没有统一的 page aggregate command family，那么 AI 最终只能选下面几条坏路：

- 直接操作前端 DOM
- 直接操作 Tiptap JSON
- 直接调用不同的标题 / 正文 / 设置接口拼写入

这三条路都不符合长期目标。

长期正确路线必须是：

> **AI 先操作 Rust page aggregate 的命令与块语义，再由 editor island 把结果投影到 live editing surface。**

### 4.1.2 树域与页面域会继续出现“双真相漂移”

只要页面树 / 文件树消费的还是一套资源 meta，而主编辑区头部和页面设置是另一套前端状态，那么后续还会不断出现：

- 标题单一真源已开始收口，但当前尚未和正文、页面设置统一成同一 page aggregate 命令边界
- 页面设置变更只影响一部分视图
- 阅读态与编辑态宽度规则不一致
- 新增一个页面能力时，要改三四条链路

这类问题越到后面越难收。

---

## 5. 正确的目标重写

当前阶段的目标不应再写成：

- “把 `leptos-tiptap` 再打磨得更像官方模板”

而应重写为：

> **建立 Rust 主导的 `page aggregate`，让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入共享同一份页面真相。**

这句话拆开后，意味着下面五点。

### 5.1 Page Aggregate Projection

Rust 侧需要提供一个统一的页面聚合投影，至少包含：

- `page_identity`
  - `workspace_id`
  - `document_id`
  - `node_id`
- `page_head`
  - `title`
  - `icon`
  - `cover`
  - `updated_at`
  - `permissions`
- `page_layout`
  - `wide_layout`
  - `small_text`
  - `layout_density`
  - `show_toc`
  - `show_structure`
- `page_body`
  - `editor_document`
  - `revision`
  - `conflict_detection_key`
  - `stats`
- `page_tree`
  - `outline`
  - `child_pages`
  - `resource_meta`

当前不要求一次把所有字段做满，但必须先把口径冻结。

### 5.2 Page Command Family

后续页面域的操作不应继续被拆成互不统属的零散 mutation，而应在语义上统一到 page aggregate 之下。

最小命令族至少包括：

- `page.head.rename`
- `page.layout.update`
- `page.body.save`
- `page.body.insert_reference`
- `page.body.set_embed_default_block`
- `page.tree.refresh_projection`

这不意味着现在立刻把所有接口名都改掉，而是：

> **从现在开始，所有页面能力都应先判断它属于哪个 page aggregate 子域，而不是继续新增离散 mutation。**

### 5.3 Leptos Island 的正式边界

`leptos-tiptap` island 的职责应收敛为：

- 消费 `page_body`
- 消费必要的 `page_layout` 编辑器相关配置
- 回发正文与块级命令
- 暴露可供 AI / 页面壳使用的稳定 editor handle

它不应再负责：

- 定义页面总真相
- 自己发明页面设置语义
- 在页面壳之外另持有一套页面元信息状态

### 5.4 Tree Domain 的接缝

页面树 / 文件树不需要知道编辑器内部细节，但必须与页面聚合共享：

- 同一份 `document_id`
- 同一份页面标题
- 同一份 `resource_meta`
- 同一份页面可见状态与能力语义

也就是说：

- 树域保持 tree-first
- 页面域保持 page aggregate
- 两者通过稳定 projection contract 对齐

而不是：

- 树域一套标题来源
- 页面域另一套标题来源

### 5.5 Convex 的正确定位

当前不应该先拆 Convex。

正确口径是：

- Convex 继续作为存储 / 实时 / 协作底座
- Rust 持有 canonical contract 与语义编排
- Leptos / Next 负责消费 projection 与呈现 island

因此，“Rust 单一真源”在当前阶段的正确含义不是：

- 只有 Rust 数据库

而是：

- **Rust 持有语义单一真源，Convex 持有当前持久化底座。**

---

## 6. 当前哪些地方需要纠偏

## 6.1 需要纠偏的不是“页面里还有前端状态”

页面壳中存在一些本地临时状态本身没有问题。

问题在于：

- 哪些状态是临时 UI 状态
- 哪些状态却在冒充对象真相

后续必须把两者分清。

### 6.1.1 可以继续留在前端壳的状态

- inspector 开关
- comments drawer 开关
- history drawer 开关
- 当前是否在编辑态
- 当前 host observability

这些都是 UI state，不必进入 Rust 真相层。

### 6.1.2 必须退出前端壳真相地位的状态

- 页面标题
- 页面宽度 / 页面布局正式选项
- 编辑器默认嵌入位置
- 页面 outline 的正式来源
- 页面 stats 的正式来源

这些都不应继续被前端 `useState` 长期定义为页面事实。

## 6.2 需要纠偏的不是继续换保存底座

当前正文保存链已经基本可用，真正的问题不是“保存不到页面”，而是：

- 保存成功不等于 page aggregate 已建立

因此：

- 现在不该推翻 `documents.save`
- 也不该另造一套临时保存格式

而应做的是：

> **把标题 / 正文 / 页面设置的语义边界对齐到同一 page aggregate 之下。**

## 6.3 需要纠偏的是页面设置的产品口径

当前页面设置里已经混入了三类字段：

- 真正属于页面布局的
- 真正属于阅读壳的
- 真正属于编辑器 runtime 的

后续必须先分类，再决定哪些继续保留在页面设置面板里，哪些推迟支持，哪些进入 editor island。

否则只会继续出现：

- UI 有按钮
- 数据能存
- 但主编辑器不真正消费

---

## 7. 分阶段落地

下面的阶段不是完整开发清单，而是主线纠偏顺序。

## 7.1 Phase F: 冻结 Page Aggregate Contract

目标：

- 在设计层明确页面聚合的 canonical contract
- 不再让页面页头 / 正文 / 设置 / 子树各自长字段

完成标准：

- 有单独的 page aggregate contract 文档
- 明确 `page_head / page_layout / page_body / page_tree` 的最小字段
- 明确哪些字段是 UI state，哪些字段是 aggregate truth

## 7.2 Phase G: 主文档页改为消费统一聚合

目标：

- 文档页不再手工拼 `title + options + content + pageSubtree`
- 改为消费统一页面聚合返回值

完成标准：

- 页面头部使用聚合中的 `page_head`
- 页面设置初始值使用聚合中的 `page_layout`
- 主编辑区 bootstrap 使用聚合中的 `page_body`
- 树与 TOC 使用聚合中的 `page_tree`

## 7.3 Phase H: `leptos-tiptap` 正式消费 editor-related page options

目标：

- 把真正属于编辑器 runtime 的设置接入 `leptos-tiptap`

最小优先级建议：

1. `wideLayout`
2. `smallText`
3. `layoutDensity`
4. `showHeadingNumbers`
5. `embedDefaultBlockId`

完成标准：

- inspector 改动后，主编辑区表现真实变化
- 不再出现“设置能存，但编辑器内部几乎没变化”

## 7.4 Phase I: 树域与页面域标题统一

目标：

- rename 后页头、sidebar row、page tree、file tree 一致刷新

完成标准：

- 标题变更只经过一套 page aggregate command 语义
- 所有标题消费者都来自同一份更新后的 projection

## 7.5 Phase J: AI 写入入口对齐 page aggregate

目标：

- AI 不再绕过 page aggregate 直接拼前端对象

完成标准：

- AI 能通过统一页面命令把内容写入主编辑区
- 人工编辑与 AI 编辑共享同一块语义边界

---

## 8. 当前阶段的完成判定

只有当下面这些条件同时成立时，才能把这条线描述成：

> **“主编辑区与树域已经基本统一到 Rust 主导的单一真源架构”。**

- 文档页消费统一 `page aggregate projection`
- 标题 / 正文 / 页面设置不再是三条分裂真相链
- `leptos-tiptap` island 真正消费 editor-related `page_layout`
- 树标题与页头标题来自同一份 projection
- AI 写入入口开始围绕 page aggregate command family 设计

在此之前，正确口径都应保持为：

> **`leptos-tiptap` 主编辑区已基本可用，但页面聚合仍未收口，当前仍处于从混合态向 Rust 单一真源过渡的过程中。**

---

## 9. 本文结论

这次暴露出的页面宽度不统一、页面设置只部分生效，以及标题链虽已收口但聚合仍未统一，并不是坏消息。

它们的价值在于：

> **它们准确暴露了当前真正缺的不是“再修几个细节”，而是“把页面域提升成一个与树域对齐的 Rust page aggregate”。**

因此，后续主线不应继续散落成：

- 修标题
- 修宽度
- 修页面设置

而应统一收口为：

> **Page Aggregate Contract**
>
> **Page Aggregate Projection**
>
> **Page Aggregate Command Family**

这才是让 `tree-first graph kernel`、`leptos-tiptap` 主编辑区、以及后续 AI 直接写入主编辑区三者真正对齐的正确底层方向。
