Files
mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md
T

270 lines
18 KiB
Markdown
Raw 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.
# 5-6 [process] Page Aggregate 单一真源对齐执行清单 v1
> 更新时间:2026-04-22
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
## 1. 文档目的
这份清单不是再讨论“问题是不是存在”,而是把:
- `5-5` 里对当前混合态的判断
- 后续 `Page Aggregate` 主线
整理成一份能持续勾选、持续验收、持续防止跑偏的执行清单。
这份清单只围绕一个目标:
> **让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入口,逐步收口到 Rust 主导的 page aggregate 单一真源。**
---
## 2. 当前阶段结论
当前可以确认的事实有两类。
### 2.1 已经成立的事实
- [x] `leptos-tiptap` 已经成为页面内正式主编辑区,不再是 iframe bridge。
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
- [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。
### 2.2 还没有成立的事实
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
- [ ] AI 写入口还没有完整对齐 page aggregate command family。
补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter``/api/documents/title``/api/documents/options``/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。
补充:2026-04-28 树域剩余 runtime 收口中,`page.body.saved``document.snapshot.saved` 已完成职责拆分;`tree.node.embed``pageReference` block 结构与插入位置已由 Rust `pageAggregateEmbedPlan` 生成,3000 route 只保留源/目标内容读取作为 substrate preflight。这使正文保存快照语义和页面嵌入正文 patch 都开始落在 Page Aggregate / Rust artifact 边界内。
---
## 3. 当前明确不做什么
为了避免再次回到“零散补 UI”的老路,下面这些项当前不进入主线前排:
- [ ] 不把当前问题重新降级成“修两个样式 bug”。
- [ ] 不继续通过新增前端本地状态来掩盖 page aggregate 缺失。
- [ ] 不先重做一轮页面设置 UI。
- [ ] 不先扩一批和 page aggregate 无关的编辑器花活。
- [ ] 不先争论替换 `Tiptap`、替换 `leptos-tiptap`、移除 Convex。
---
## 4. Phase F:冻结 Page Aggregate Contract
目标:
> **先把页面聚合的 canonical contract 写清楚,阻止页面头部、正文、设置、子树继续各自长字段。**
### 4.1 设计层完成标准
- [x] 单独补一份 page aggregate contract 文档。
- [x] 文档中固定 `page_identity / page_head / page_layout / page_body / page_tree` 的最小字段。
- [x] 文档中固定哪些字段属于 page aggregate truth,哪些字段只是前端 UI state。
- [x] 文档中固定哪些字段必须由 Rust 主导,哪些字段允许作为前端临时态存在。
### 4.2 代码层完成标准
- [x] 前端与 Rust 侧都出现统一命名的 page aggregate 类型或等价契约。
- [x] 不再继续给 `DocumentPageProps``DocumentContentProps` 零散加字段来扩页面真相。
- [x] 文档页加载入口能明确区分“聚合读取结果”和“局部 UI 临时态”。
补充:当前已新增 `page-aggregate-builder.ts``page-aggregate-loader.ts``/api/documents/page`,文档页 SSR 入口和 `DocumentContent` 的内容重试补拉都已改为消费同一份 `PageAggregateProjection`,不再由 `page.tsx` 手工拼 `meta + content`。Rust 侧仍未直接暴露同名 `Page Aggregate` projection route,但 `storage-convex-bridge``bridge-runtime` 已开始接受 `page.head.updateTitle / page.layout.updateOptions / page.body.save` 这组 page command family 的命名口径,因此这里先按“等价契约已出现”勾选完成。
### 4.3 退出标准
- [x] 后续再讨论标题、页面设置、正文保存时,能够直接定位到它属于 `page_head / page_layout / page_body / page_tree` 的哪一层。
---
## 5. Phase G:主文档页改为消费统一聚合
目标:
> **让 `/documents/[id]` 不再手工拼 `title + options + content + pageSubtree`,而是消费统一页面聚合。**
### 5.1 加载链路收口
- [x] 页面 SSR / route 入口改为优先读取统一 page aggregate,而不是分别读取 meta 和 content。
- [x] `DocumentShell` / `DocumentContent` 的入参改为围绕统一聚合对象组织。
- [x] 页面头部、页面设置初值、主编辑区 bootstrap、TOC/outline 都来自同一份聚合结果。
### 5.2 当前散落 props 的收口
- [x] `title` 不再作为独立真相字段散落传递。
- [x] `initialOptions` 不再作为独立真相字段散落传递。
- [x] `initialContent``initialRevision``initialConflictDetectionKey` 归入统一 `page_body`
- [x] `initialPageSubtree` 归入统一 `page_tree`,并明确其是正式 projection 还是仅兼容过渡项。
### 5.3 退出标准
- [x] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。
- [ ] 后续页面新增字段时,不再需要继续向外层 props 链同时塞多种局部真相。
补充:当前首屏 SSR 与客户端内容重试补拉都已经走 `/api/documents/page -> page aggregate loader -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。
补充:本轮已新增 `page-aggregate-client-state.ts`,并让 `DocumentContent` 把原先分散维护的 `options / content / serverContentSnapshot / serverPageSubtreeSnapshot / serverPageSubtreeTitle / contentRevision / conflictDetectionKey` 开始收口为同一份 client aggregate state reducer。这里代表“页面本地 `body/layout/tree` 真相已经开始统一”,不等于标题链、页面设置运行时分类、AI 对页面设置面的正式入口都已闭环,因此本阶段不提前宣称聚合完成。对应最小回归测试为 `page-aggregate-client-state.test.ts`
---
## 6. Phase H:让 `pageOptions` 真正进入 `leptos-tiptap` island
目标:
> **把当前“能展示、能保存、但主编辑区不真正消费”的页面设置,按优先级接入 island。**
### 6.1 必须先分类
- [x] 把当前页面设置分成:页面壳布局类、阅读视图类、编辑器 runtime 语义类。
- [ ] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。
补充:本轮已新增 `page-option-semantics.ts`,把 `pageOptions` 的运行时语义正式收口为代码契约,不再让这套规则继续散落在 `page-options-sidebar.tsx``editor-host-types.ts``leptos-tiptap-island-editor-host.tsx` 里各写一份。当前至少已经明确:
- `wideLayout / smallText / layoutDensity` 属于 `page_shell_layout + editor_runtime`
- `showHeadingNumbers` 属于 `read_view + editor_runtime`
- `showToc` 属于 `read_view`
- `protectEditing / showBlockRefCount` 属于 `editor_runtime`,但当前仍是 `planned`
- `embedDefaultBlockId` 已进入 `editor_runtime`
对应最小回归测试为 `page-option-semantics.test.ts``leptos-tiptap-island-editor-host.test.tsx``page-options-sidebar.test.tsx`。第二项继续保留未完成,因为“哪些项应降级/隐藏/保留”的产品面纠偏还没完全落到 inspector 与 AI tool surface。
### 6.2 最小接入优先级
- [x] `wideLayout` 真正进入 island 布局语义,而不是只改外层壳宽度。
- [x] `smallText` 真正进入主编辑区排版语义。
- [x] `layoutDensity` 真正进入块间距 / 正文密度语义。
- [x] `showHeadingNumbers` 真正进入 heading 展示语义,或明确标注暂不支持。
- [x] `embedDefaultBlockId` 真正进入主编辑器引用 / 嵌入默认位置逻辑,或明确标注暂不支持。
### 6.3 页面设置面板纠偏
- [x] 没有真正接入 island 的编辑器语义项,不再继续以“已开启/已关闭”假装正式完成。
- [x] 对未支持项给出显式降级说明,而不是仅保存字段。
- [ ] 让“页面设置有值但编辑器内部没变化”这类状态在产品上消失。
### 6.4 退出标准
- [ ] inspector 中至少最小优先级项修改后,主编辑区可见结果真实变化。
- [ ] 用户不再需要猜“这个设置到底有没有真正作用到编辑器”。
---
## 7. Phase I:树域与页面域标题统一
目标:
> **rename 后页头、sidebar row、page tree、file tree 使用同一份标题结果。**
### 7.1 标题真相纠偏
- [x] 页面头部标题不再长期由前端 `pageTitle` 本地状态冒充正式真相。
- [x] rename 命令在语义上进入统一 page aggregate command family。
- [x] 树域消费的标题与页面头部消费的标题来自同一份更新后的 projection。
### 7.2 回归验证
- [x] 当前页重命名后,页头即时更新。
- [x] sidebar 对应节点标题同步更新。
- [x] page tree / file tree 对应节点标题同步更新。
- [x] 刷新后标题一致,不依赖额外手动刷新。
- [x] 切页往返后标题一致,不出现旧快照回闪。
注:当前已在标题持久化成功后广播 `emitDocumentsChanged(documentId)`,并补了 `sidebar-events.test.ts` 覆盖 `documents-changed -> sidebarRefetch` 的刷新链路。
补充:已新增 `usePreferredSidebarSnapshot`,当 `tree stream` 已连上但快照落后、`documents-changed` 触发的 query/refetch 先拿到新标题时,会优先消费更新后的 canonical sidebar snapshot;待 stream 追平后再回到 live stream。对应回归测试为 `use-preferred-sidebar-snapshot.test.tsx`。这修掉了“刷新链发出去了,但 stale stream 仍把 sidebar 标题压回旧值”的一类问题;浏览器层 `sidebar row / page tree / file tree` 真实渲染验收仍待补齐。
补充:已新增 `AppLayoutShell`,把 layout 顶栏 `Breadcrumb` 从 SSR 注入的静态 `documents` 挪到与 `Sidebar` 共享的同一条 live sidebar snapshot 管线;并且 layout shell 会把“已选中的 preferred snapshot”同一对象同时透传给 `Sidebar``Breadcrumb`,不再各自独立选择。对应回归测试为 `app-layout-shell.test.tsx`。这意味着 breadcrumb / sidebar 现在至少共享同一份工作区树 canonical snapshot,不再是 layout 一条静态链、sidebar 一条 live 链并行。页头 `page.head.title` 与这条工作区树链之间的最终统一验收仍待补齐,因此本阶段继续不提前打满。
补充:已新增 `PreferredSidebarSnapshotProvider``usePageHeadTitle`,把文档页头标题从 `DocumentContent` 内部长期持有的 `pageTitle` 本地真相,改为“同一份 preferred sidebar snapshot 的 committed title + 短暂 draft”。同时修正 `useSidebarData.refetch()`,在 Convex live 模式下收到 `documents-changed` 也会主动拉取一份新的 `/api/sidebar` snapshot,再与 tree stream 做 freshness 选择,避免“页头草稿是新的,但 breadcrumb / sidebar / page tree / file tree 还卡在旧快照”。对应单测为 `use-page-head-title.test.tsx``use-sidebar-data.test.tsx`。浏览器烟测 `scripts/task110-page-title-single-truth-smoke.js` 已验证:页头重命名后,breadcrumb、默认 sidebar、page tree、file tree、切页往返、刷新均保持一致,因此本阶段与“标题来自同一份更新后的 projection”相关的勾选正式保留。
### 7.3 退出标准
- [x] 标题不同步问题不再以“前端本地状态漂移”的形式重复出现。
---
## 8. Phase JAI 写入口对齐 Page Aggregate
目标:
> **AI 不再绕过 page aggregate,直接拼前端对象或编辑器内部格式。**
### 8.1 语义边界
- [x] 明确 AI 写页面时优先进入哪组 page aggregate command。
- [x] 明确 AI 写标题、写正文、改页面设置、插入引用时分别走哪些统一命令语义。
- [x] 明确 AI 不直接拼 DOM、不直接拼前端页面壳对象、不直接依赖浏览器临时状态。
补充:当前页面命令名已开始从 `documents.*` 收口到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`,其中标题 route、页面设置 route、正文保存 route 都已切到这组 page command family`storage-convex-bridge``bridge-runtime` 也已补齐对应 alias。
补充:本轮进一步把前端调用面与 Next route 执行面也开始统一到这组 `page.*` family:前端已新增 `page-command-client`,不再让 `DocumentContent`、AI 面板、各编辑器 host 各自维护一套页面写入口;服务端已新增 `page-write-command-adapter``title/options/save` 三条 route 不再分裂在 `metadata/save` 两套执行器里。对应最小回归测试为 `page-command-client.test.ts``page-write-command-adapter.test.ts``route-adapters.test.ts``DocumentAiAgentPanel.runtime.test.tsx``document-content.test.ts`
补充:本轮继续把 AI 面板的读取边界开始收口到统一页面本地快照:`DocumentContent` 不再向 `DocumentAiAgentPanel` 透传 `getLatestBlocks / getLatestPageSubtree / getLatestPersistedMeta` 三组分散 getter,而是改为一份 `getLatestPageAggregateSnapshot`,由 `page-aggregate-client-state` 导出 `blocks / pageSubtree / persistedMeta / pageOptions`。这代表 AI 面板已开始消费同一份页面本地 aggregate state,而不是继续拼接独立局部真相;但页面设置写工具本身仍未进入 Hermes 正式 tool surface,因此这里仍然只算“开始收口”,不提前打满。
补充:本轮还把 `pageOptions``editorRuntimePageOptions` 一并带入 `/api/ai-agent/run -> buildHermesInstructions`,因此 AI 在服务端至少能看到“当前页面设置是什么”以及“哪些设置已经进入 island runtime payload”,不再只依赖正文块快照和树快照来猜测页面语义。对应回归测试为 `src/app/api/ai-agent/run/route.test.ts`。这推进的是 AI 读侧上下文,不等于页面设置写工具已经具备正式入口。
### 8.2 与主编辑区的关系
- [x] 人类编辑与 AI 编辑共享同一块语义边界。
- [x] AI 改写结果能通过主编辑区 island 正式回显。
- [ ] AI 改写后树标题 / 页面头部 / 页面设置不再走各自独立副作用链。
### 8.3 退出标准
- [x] AI 写入口已经可以被明确描述为“操作 page aggregate command family”,而不是“绕过系统写编辑器”。
注:当前 `/api/ai-agent/run``Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。其后:
- `doc_insert_blocks / doc_replace_range` 继续按 `page.body.save` 语义落到 `/api/documents/save`,再正式回显主编辑区 island。
- `slash_run(rename current page)` 会把结构化结果回接到当前页 `DocumentContent` 的同一条标题提交链,并继续广播 `emitDocumentsChanged(documentId)`,因此页头标题与树标题不再靠 AI 面板内部本地状态各自漂移。
- 当前 `pageOptions` 仍没有进入 Hermes 正式 tool surface,因此“页面设置类 AI 命令”尚未收口;`8.2` 的最后一项继续保留未完成,避免误判为整条线已经闭环。
---
## 9. 推荐实施顺序
当前推荐顺序固定为:
1. `Phase F`
2. `Phase G`
3. `Phase H`
4. `Phase I`
5. `Phase J`
约束如下:
- [x] 没有完成 `Phase F` 之前,不再继续零散补标题 / 宽度 / 页面设置。
- [ ] 没有完成 `Phase G` 之前,不把当前文档页描述成“已经统一聚合完成”。
- [ ] 没有完成 `Phase H` 之前,不把页面设置大量打钩为“正式可用”。
- [ ] 没有完成 `Phase I` 之前,不把标题问题视为已从底层解决。
- [ ] 没有完成 `Phase J` 之前,不把 AI 直接写主编辑区描述成已经具备正式稳定入口。
---
## 10. 当前阶段的总退出标准
只有当下面这些条件同时成立时,才可以把这条线描述成:
> **“主编辑区与树域已经基本统一到 Rust 主导的 page aggregate 单一真源架构”。**
- [x] 文档页消费统一 `page aggregate projection`
- [ ] 标题 / 正文 / 页面设置不再是三条分裂真相链
- [x] `leptos-tiptap` island 真正消费 editor-related `page_layout`
- [x] 树标题与页头标题来自同一份 projection
- [x] AI 写入口开始围绕 page aggregate command family 实现
补充:当前未勾选“标题 / 正文 / 页面设置不再是三条分裂真相链”的原因,不再只是命令名或 route 分裂。虽然前端页面写入口、Next route 执行器、`DocumentContent` 内部的 `body/layout/tree` client state、以及 `pageOptions` 的代码级运行时分类都已经开始收口,但 projection 回流与 AI 对页面设置面的正式写入口仍未完全统一。也就是说,命令执行面、页面本地状态面、页面设置语义面都已开始统一,但页面域单一真源仍未闭环。
在此之前,正确口径都应保持为:
> **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**