Files
mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md
T
lix-2026 1569699fbb docs: separate design reference queue
- 将不直接执行的 process-reference 文档迁入各域 reference 目录

- 更新 design/README、AGENTS 和总序文档,固定 process/draft/reference/done 目录语义

- 修正活跃文档中指向旧 process 位置的参考链接

验证:git diff --check;codegraph sync .
2026-05-21 10:19:02 +08:00

343 lines
34 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-05-18
>
> 2026-05-18 口径补充:
> - 本清单继续有效,但 Page Aggregate 的默认落点调整为 local-first workspace;本地 `.md`、`.mnote/page-options.json` / frontmatter 和 `{mdBase}.assets/` 是早期产品默认闭环。
> - Convex-backed Page Aggregate 只作为兼容 / cloud source 路径,不再作为新增能力默认主存储目标。
>
> 2026-05-19 口径补充:
> - local-first 下 `.md` 文件是正文真相,`EditorBlockDocument` 是 runtime-native projection / cache,不是第二份正文真相。
> - 写侧必须退出 `/api/documents/save` 长期主路径,收口到带 `expectedFileVersion` 的 `page.body.write` / LocalFS executor。
> - AI 直改文件、tiptap autosave、外部编辑器修改必须共用 VSCode-like 文件版本冲突模型。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/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] 文档页正式读链已固定消费 Rust `mnote.page_aggregate.v1` snapshotTS builder 已退出 runtime 主链。
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
- [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。
- [x] local-first 正文真相已明确为 `.md` 文件;tiptap / EditorBlockDocument / Page Aggregate 均不再被描述为独立正文真相。
### 2.2 还没有成立的事实
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
- [ ] 写侧还没有完全退出 `/api/documents/save` 兼容面;`page.body.write` / LocalFS executor 与文件版本冲突模型仍待落地。
- [ ] tiptap dirty 状态与 AI / 外部文件变更之间还没有完整 VSCode-like conflict UI。
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
- [ ] AI 写入口还没有完整对齐“授权文件引用 + 白名单目录 + 后台文件写入 + 前台同步”模型。
补充:本轮已新增前端统一 `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。
补充:当前也不打算把 `documents/save` 继续扩成正文真相主入口。local-first 下它最多是 compat adapter;长期写侧必须按本地文件版本号仲裁,像 VSCode 一样处理 dirty / clean / external change 冲突。
---
## 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-loader.ts`、文档页 SSR 入口与 `DocumentContent` 的内容重试补拉都已经直接消费 Rust `/api/page-aggregate/:id` 返回的 `PageAggregateProjection`,不再由 `page.tsx` 手工拼 `meta + content`。Next `/api/documents/page` 已降级为显式 `410` 的 compat 边界;`page-aggregate-loader.ts` 现在只校验并消费这条 Rust 正式读链,不再在 runtime 中回退到 TS builder。与此同时,`storage-convex-bridge``bridge-runtime` 已开始接受 `page.head.updateTitle / page.layout.updateOptions / page.body.save` 这组 page command family 的命名口径,因此这里按“读取契约已进入 Rust-first,写入命令面已开始收口”勾选完成。
补充:这里的“Rust-first”只表示 `/api/page-aggregate/:id` 的对外 route、契约校验和前端消费主链已经收口到 Rust,不表示底层已经完全 kernel-native。2026-05-13 复核后,当前 route 仍由 Rust runtime adapter 消费 `documents:getMeta + documents:getContent` substrate 构建聚合快照,因此 response `source` / `x-mnote-page-aggregate-owner` 应反映为 `CompatMetaContentJoin` / `compat-join`。只有底层真实改为 kernel 原生 page aggregate projection 后,才应标记为 `KernelProjection` / `rust-kernel`
补充:2026-05-16 复核 `page-aggregate-loader.ts``page-aggregate-builder*``/api/documents/page` compat route 后,确认读取主链仍然只消费 Rust `/api/page-aggregate/:id`TS builder 仅保留为历史 adapter / 单测材料,Next `/api/documents/page` 继续明确返回 `410`。验证命令:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate``cd wolai-frontend && pnpm test src/app/api/documents/page/route.test.ts src/lib/documents/page-aggregate-builder.test.ts`。全仓 `git diff --check` 因既有删除的 `rust/spikes/leptos-tiptap-spike/trunk-8123.err` 无法生成 checkdiff,本轮未处理该无关脏改动。
补充:2026-05-16 在 `http://127.0.0.1:3000` 的 Rust `mnote-web` 主入口实跑 Page Aggregate smoke。`task110-page-title-single-truth-smoke.js` 验证标题修改后页头、Breadcrumb、Sidebar、Page Tree、File Tree 与刷新后标题一致,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/task110.stdout.json`。文档打开 smoke 验证文档 HTML 包含 `data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-page-tree-source="page_aggregate.tree.pageSubtree"`,同一临时页 `/api/page-aggregate/:id` 回读 `schema=mnote.page_aggregate.v1`,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-open-snapshot.stdout.json``page-open-snapshot.png`。字段完整性 smoke 验证 `identity/head/body/tree/stats` 全部存在,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-aggregate-fields.stdout.json`。当前 `projectionSource=documents.content`,仍符合“Rust-first 读取链、非 kernel-native 落库完成态”的过渡口径。
补充:2026-05-18 继续推进 Page Aggregate 读侧块文档优先级。`build_page_aggregate_projection_result` 现在按 `editorDocument -> blockDocument -> documents.content` 构建 `body.blockDocument`,并用 `projectionSource` 暴露真实来源;同时 `body.content` 也会按原生来源重建,避免编辑器/session 下游继续从旧 legacy content 初始化。新增 `page_aggregate_get_prefers_editor_document_over_legacy_content``page_aggregate_get_projects_body_content_from_block_document_source` 与非法原生块文档防回退测试。验证命令:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate -- --nocapture`。这仍不等于 EditorBlockDocument 原生落库完成,`documents:updateContent` 兼容写入与底层 schema 收口继续留在后续阶段。
补充:2026-05-18 同步推进 `page.body.save` 写侧原生快照保留。`mnote-web` transport 不再在调用 `documents:updateContent` 前剥离 `editorDocument / tiptapDocument`,只剥离 Rust artifact 事件字段。验证命令:`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_keeps_editor_runtime_fields_for_page_body_save -- --nocapture`。真实部署时仍需确认当前 Convex substrate 的 schema/mutation 已接受并返回这两个字段。
补充:2026-05-18 继续收紧 `page.body.save` 原生快照失败语义。`editorDocument` 一旦出现在保存 payload 中就必须可解析;不可解析时返回 validation error,不再静默回退到 `tiptapDocument` 或 legacy `content`。验证命令:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_command_plan -- --nocapture`
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-body-sync-smoke.js`,验证真实页面正文编辑后 `/api/documents/save``/api/page-aggregate/:id` 回读闭环。证据为 `tmp/page-aggregate-body-sync-smoke/mp87mgz7.json``mp87mgz7.png``body.revision``0``1``body.conflictDetectionKey``tree_1778927703753_1:0``tree_1778927703753_1:1``body.blockDocument.blocks[0]` 回读到新段落文本和 `revisionRef`。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_save_route_executes_page_body_save_command``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate_get_projects_legacy_content_to_block_document` 已通过。
### 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/page-aggregate/:id -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。
补充:入口级痕迹消失不等于来源语义消失。当前前端 loader 仍只接受 `mnote.page_aggregate.v1`,不会恢复 TS builder 主链;但 Rust response 的 provenance 必须继续暴露真实构建来源,避免把兼容 join 误验收成 kernel-native projection。
补充:本轮已新增 `page-aggregate-client-state.ts`,并让 `DocumentContent` 把原先分散维护的 `title / options / content / serverContentSnapshot / serverPageSubtreeSnapshot / contentRevision / conflictDetectionKey` 继续收口为同一份 client aggregate state reducer;标题的 draft / committed / persisted 语义也已并入这份 reducer,不再额外维护独立 `usePageHeadTitle` hook 或 `serverPageSubtreeTitle` 双轨状态。这里代表“页面本地 `head/body/layout/tree` 真相已经开始统一”,不等于页面设置运行时分类、AI 对页面设置面的正式入口都已闭环,因此本阶段不提前宣称聚合完成。对应最小回归测试为 `page-aggregate-client-state.test.ts``document-content.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。
补充:当前统一口径已经开始落到代码级 `PAGE_OPTION_PANEL_POLICY`,最小分类如下:
- 保留在页面设置面板且继续作为正式项:`wideLayout / smallText / showHeadingNumbers / showToc / showWordCount / collapseBacklinks / pageFont / layoutDensity / hideChildPages / embedDefaultBlockId`
- 保留但降级展示、不再冒充正式完成项:`protectEditing / showBlockRefCount`
- 不再作为页面级正式项,继续留在全局偏好:`showStructure`
这里的“保留 / 降级 / 全局”是当前 Inspector、AI 页面设置写回白名单与后续 smoke 验收都必须共用的同一份产品口径,不允许再由单个 UI 文件私自重写说明。
### 6.2 最小接入优先级
- [x] `wideLayout` 真正进入 island 布局语义,而不是只改外层壳宽度。
- [x] `smallText` 真正进入主编辑区排版语义。
- [x] `layoutDensity` 真正进入块间距 / 正文密度语义。
- [x] `showHeadingNumbers` 真正进入 heading 展示语义,或明确标注暂不支持。
- [x] `embedDefaultBlockId` 真正进入主编辑器引用 / 嵌入默认位置逻辑,或明确标注暂不支持。
### 6.3 页面设置面板纠偏
- [x] 没有真正接入 island 的编辑器语义项,不再继续以“已开启/已关闭”假装正式完成。
- [x] 对未支持项给出显式降级说明,而不是仅保存字段。
- [x] 让“页面设置有值但编辑器内部没变化”这类状态在产品上消失。
补充:本轮已把 AI 页面设置结构化结果接入 `DocumentContent` 现有的 `patch_page_options + page.layout.updateOptions` 正式链路,页面设置不再只能靠人类点击 inspector 才能进入同一条页面命令面。同时,AI 正式写回白名单已限制为 `page-option-semantics.ts``runtimeSupport === "wired"` 的字段,`protectEditing / showStructure / showBlockRefCount` 继续排除在正式写回外,避免把 planned / ui_only 选项误描述为稳定能力。历史 `mnote-cli host` 侧也已新增最小服务端分支:命中页面设置 patch 时,直接执行 `page.layout.updateOptions` 并产出结构化 `tool_result(action=update_page_options)`,不再只把整段结果退化成 assistant 文本。2026-05-13 起后续主线改为 Hermes tool call -> mnote skill/plugin -> `page.layout.updateOptions`,该 host 分支只保留为过渡证据。对应最小回归测试为 `DocumentAiAgentPanel.runtime.test.tsx``mnote-cli-agent-host.test.ts`
补充:本轮继续把 Inspector 产品态与这份分类对齐:`showHeadingNumbers / embedDefaultBlockId` 已按“正式接通项”更新文案,不再继续显示“已保存字段但未接通”;`showBlockRefCount` 作为纯占位项也不再继续暴露可点击的假开关,而是明确显示 `待接线`。对应最小回归测试为 `page-options-sidebar.test.tsx``page-option-semantics.test.ts`
### 6.4 退出标准
- [x] inspector 中至少最小优先级项修改后,主编辑区可见结果真实变化。
- [x] 用户不再需要猜“这个设置到底有没有真正作用到编辑器”。
补充:本轮已新增 `scripts/task164-page-options-visible-effect-smoke.js`,并在 `http://127.0.0.1:3000` 实跑确认:
- `wideLayout``false -> true` 后,页面根属性 `data-page-wide-layout` 立即切到 `true`,主内容列最大宽度从默认态切到 `980px`
- `smallText``false -> true` 后,编辑器字号从 `16px` 变为 `15px`
- `layoutDensity``normal -> compact` 后,段落底部间距从 `8px` 变为 `4px`
同时,Inspector 文案与状态也已经按“正式接通 / 待接线 / 全局项”统一,不再让用户靠猜测判断设置是否真正生效。
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-options-sync-smoke.js`,在 `http://127.0.0.1:3000` 的 Rust `mnote-web` 主入口实跑页面设置写入与 Page Aggregate 回读闭环。该 smoke 新建临时页后依次通过页面设置 UI 修改 `wideLayout=true``smallText=true``layoutDensity=compact`,确认 `/api/documents/options` 返回 `page.layout.updateOptions`,随后轮询 `/api/page-aggregate/:id` 直到 `layout.pageOptions` 同步,并同时断言 island/runtime DOM 属性同步。证据为 `tmp/page-aggregate-options-sync-smoke/mp87y6j3.json``tmp/page-aggregate-options-sync-smoke/mp87y6j3.png`Page Aggregate 回读到 `wideLayout=true / smallText=true / layoutDensity=compact`,运行时同步为 `data-page-wide-layout="true"``data-page-small-text="true"``data-layout-density="compact"`,编辑器字号从 `16px``15px`,段落间距从 `8px``4px`。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options``cd wolai-frontend && pnpm test src/lib/documents/page-command-client.test.ts src/components/editor/leptos-tiptap-island-editor-host.test.tsx src/lib/documents/page-option-semantics.test.ts` 已通过。
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-refresh-persistence-smoke.js`,在同一临时页内通过页面头、正文编辑和页面设置 UI 依次写入最新值,等待 `/api/page-aggregate/:id` 回读最新 `head/body/layout` 后刷新页面,再断言标题、正文、页面设置和 runtime DOM 不回退。证据为 `tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json``tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.png`:刷新前后 `head.title=Page Aggregate refresh mp88fr6k``body.revision=1``conflictDetectionKey=tree_1778929070007_1:1``blockDocument.blocks[0].text=Page Aggregate refresh body mp88fr6k``layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact` 均保持稳定;刷新后页头、`.ProseMirror` 正文、设置控件、`documentElement``.document-shell`、island root 和 `.editor-surface` 都保留最新值。修复点为 `rust/crates/mnote-web/src/ssr/pages/layout.rs``initializePageUiSurfaces` 延后到 `DOMContentLoaded` 后执行。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate``cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options` 已通过。
---
## 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`,并把文档页头标题的本地 draft / committed / persisted 语义直接并入 `page-aggregate-client-state``DocumentContent` 现在只从同一份 preferred sidebar snapshot 读取 live committed title,再与聚合 reducer 内的短暂 draft / persisted title 做合成,不再额外维护独立标题 hook。与此同时,`useSidebarData.refetch()` 在 Convex live 模式下收到 `documents-changed` 也会主动拉取一份新的 `/api/sidebar` snapshot,再与 tree stream 做 freshness 选择,避免“页头草稿是新的,但 breadcrumb / sidebar / page tree / file tree 还卡在旧快照”。对应单测为 `page-aggregate-client-state.test.ts``use-sidebar-data.test.tsx`。浏览器烟测 `scripts/task110-page-title-single-truth-smoke.js` 已验证:页头重命名后,breadcrumb、默认 sidebar、page tree、file tree、切页往返、刷新均保持一致,因此本阶段与“标题来自同一份更新后的 projection”相关的勾选正式保留。
补充:2026-05-14 已把“文件树 / 页面树无需刷新可见”的验收口径固定到 Rust 3000 主文档壳:`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task426-mnote-web-main-no-reload-smoke.js` 覆盖 `/documents/<id>` 中页面 create、页面 delete,以及页面内新建 mindmap 后 File Tree asset row 即时出现。后续不能再用 3001/Next 路径或 `/tree` debug route 代表用户实际使用的 3000 主入口;页面内 mindmap create 是 `/api/mindmap/<doc>/<mindmap>` 资产写入链路,必须和页面 `tree.node.create/delete` 分开验收。
### 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`。2026-05-14 起同类上下文的当前主线已改为 Hermes run/session context,并由 Hermes 通过 mnote plugin 回读最新 page aggregate;页面设置写入也已由 `mnote.page.update_options` 回到 `page.layout.updateOptions`,旧 `/api/ai-agent/run` 证据只保留为历史过渡记录。
### 8.2 与主编辑区的关系
- [x] 人类编辑与 AI 编辑共享同一块语义边界。
- [x] AI 改写结果能通过主编辑区 island 正式回显。
- [ ] AI 改写后树标题 / 页面头部 / 页面设置不再走各自独立副作用链。
补充:进入这一步时,最大的真实 blocker 已经明确下来;以下为历史 blocker 记录,不再作为 2026-05-14 之后的当前状态描述:
- 页面 AI 历史实现当时仍残留 `/api/ai-agent/run` / `mnote-cli host` 路径
- Hermes 页面内客户端、Hermes session 真相和 mnote skill/plugin tool surface 当时还没有在页面 AI 主链落地
- 因此,当时新增的 `page_options_patch -> page.layout.updateOptions -> structured tool_result` 只是“历史主路内的最小结构化兼容分支”,不是完整正式 Hermes tool surface
这意味着 `8.2` 后续完成标准必须至少包含:
1. 页面设置进入 Hermes tool call -> mnote plugin -> Rust runtime 的正式执行环,而不是继续靠 host 内部的自然语言/最小规则分支识别
2. 页面设置结构化结果由正式 mnote plugin tool 调用产出,而不是只由 host 自己补一条兼容 `tool_result`
3. 树标题 / 页头 / 页面设置三条 AI 写回链在同一条正式 page aggregate command family 中闭环,并补 smoke 验证
2026-05-14 追加说明:上述 blocker 中“页面 AI 历史实现仍残留 `/api/ai-agent/run` / `mnote-cli host` 路径”和“Hermes 页面内客户端、Hermes session 真相、mnote skill/plugin tool surface 还没有落地”的判断已被 `design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md` 覆盖。当前代码已新增 Hermes client proxy 与 mnote Hermes tool routes,页面 AI 主链已改为 Hermes session/run/events,标题、页面设置、正文、artifact 写入已回到 Rust runtime / Page Aggregate command family;旧 `/api/ai-agent/run` 在 mnote-web 中已收口为 `legacy_ai_agent_run_retired` guard。`7-4` 的正式 `3000` smoke 矩阵与持久化审计已通过,剩余文档治理以 `7-4` 的 N 项为准。
### 8.3 退出标准
- [x] AI 写入口已经可以被明确描述为“必须经过授权 scope 与页面写入仲裁”,而不是“绕过系统写编辑器”。
- [ ] local-first 下,普通正文 AI 写入必须进一步落到“授权文件 patch 或带 `expectedFileVersion``page.body.write`”,不能继续以 `/api/documents/save` 兼容面作为长期验收终点。
注:历史 `/api/ai-agent/run``Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。2026-05-14 起这只保留为过渡证据;新的主线已由 Hermes 发起 mnote plugin tool call,再由 Rust runtime / kernel 返回 tool result。其后:
- `doc_insert_blocks / doc_replace_range` 这类历史 AI 写入曾按 `page.body.save` 语义落到 `/api/documents/save`local-first 新口径下,该链路只作为 compat 证据,后续普通正文写入应改为授权文件 patch 或带 `expectedFileVersion``page.body.write`
- `slash_run(rename current page)` 会把结构化结果回接到当前页 `DocumentContent` 的同一条标题提交链,并继续广播 `emitDocumentsChanged(documentId)`,因此页头标题与树标题不再靠 AI 面板内部本地状态各自漂移。
- 当前 AI 面板已经能消费结构化 `update_page_options` 结果,并把 `pageOptionsPatch` 回接到当前页 `DocumentContent` 的同一条 `patch_page_options + page.layout.updateOptions` 提交链;同时只允许 `runtimeSupport === "wired"` 的字段进入正式写回,避免 planned / ui_only 页面设置混入主链。对应最小回归测试为 `DocumentAiAgentPanel.runtime.test.tsx``document-content.test.ts`
- 历史 `mnote-cli host` 能在命中页面设置 patch 时直接执行 `page.layout.updateOptions`,并向前端回放结构化 `tool_call/tool_result` 事件;这意味着“服务端完全没有页面设置结构化写回结果”的状态已经结束。
- 当前 `pageOptions` 已进入 mnote Hermes plugin 的第一批正式 tool surface`mnote.page.update_options` 只允许 `runtimeSupport === "wired"` 字段写入 `page.layout.updateOptions`planned / ui_only 字段返回 warning 或 ignored 结果。后续剩余项不再是“页面设置类 AI 命令是否闭环”,而是 slash AI / selection toolbar AI 等更细粒度入口如何复用同一条 Hermes tool chain。
---
## 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 单一真源过渡的过程中。**