# 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 文件版本冲突模型。
>
> 2026-05-22 复核补充：
> - local_folder 文档页、Hermes `mnote.page.save`、历史 Hermes `mnote.doc.markdown_edit` 的本地写入链已回到 `write_local_markdown_page_body` / `page.body.write`，并携带 `expectedFileVersion` 或聚合快照中的 file version。2026-05-22 之后，`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback；当前 AI 编辑口径以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。
> - BufferStore 已进入保存、watcher、Hermes 写入与冲突信封；`task451` 已覆盖冲突信封中的 `externalActor / dirtyState / bufferFileVersion`，`task484` 已覆盖保存后刷新读回。
> - cloud / compat `/api/documents/save` 边界已固定为非 local-first 正文主写链；local-first 默认写侧走 `/api/page-body/write` / LocalFS executor。
> - 2026-05-22 继续补证：`acp_runtime_env_limits_*`、`hermes_client_run_body_local_source_uses_file_scope_not_full_page_context`、`ensure_write_authorized_*`、`local_path_read_access_rejects_root_escape`、`local_file_open_rejects_root_escape` 均通过；ResourceTab 跨文档 owner 错配已由 `sidebar_filetree_asset_open_uses_owner_document_id` 修复并覆盖；旧 cloud `task443` / `task445` browser smoke 已迁移为 local-first fixture 并复跑通过。
> - 2026-05-22 收尾补证：`task453-local-folder-page-ai-changed-files-smoke.js` 已扩展为 AI 写入前台同步 smoke；clean path 验证 Hermes `run.completed.agentAudit.changedFiles` 后 changed-files 工具卡、磁盘 `.md`、Page Aggregate、ProseMirror 和 clean editor 非冲突态；dirty path 验证同一路径触发 `external-change-conflict`、冲突面板显示 agent run、冲突信封包含 `externalActor / dirtyState / bufferFileVersion`，diff 同时显示本地未保存 token 与 AI 写盘 token。
>
> 关联文档：
> - `/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` snapshot，TS builder 已退出 runtime 主链。
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
- [x] 当前问题已经不再是“能不能接入主编辑器”，而是“接入后如何收口为单一真源”。
- [x] local-first 正文真相已明确为 `.md` 文件；tiptap / EditorBlockDocument / Page Aggregate 均不再被描述为独立正文真相。

### 2.2 还没有成立的事实

- [x] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区已经在主路径上消费同一份 page aggregate / preferred sidebar projection；后续新增字段仍可能需要补 loader -> state -> consumer 链，但不再作为 P1 阻塞。
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
- [x] local-first 写侧已退出 `/api/documents/save` 长期主路径；文档页保存使用 `/api/page-body/write`，Hermes 本地正文写入回到 `write_local_markdown_page_body`，并复用文件版本冲突模型。
- [x] tiptap dirty 状态与 AI / 外部文件变更已具备 VSCode-like conflict UI 与 BufferStore 信封；`task451` 覆盖 accept-disk / keep-current / merge-save / agent-conflict-source。
- [x] `pageOptions` 已整体收口到 `leptos-tiptap` island 的正式运行时语义层；`protectEditing / showBlockRefCount` 仍是 planned / 降级展示项，不再冒充正式完成。
- [x] AI 写入口已经补齐前台同步回归证明：local-first 下 allowedRoots / path escape / commandContext 有 targeted tests；`task453` 覆盖 AI 写入后 changed files、Page Aggregate、ProseMirror、clean 非冲突态和 dirty conflict UI。

补充：本轮已新增前端统一 `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] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。
- [x] 后续页面新增字段时不再需要继续向外层 props 链同时塞多种局部真相；现有模式是补 Page Aggregate -> client state -> island/consumer 链。新增字段仍要改 consumer，但不再扩第二份页面事实源。

补充：当前首屏 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 语义类。
- [x] 明确哪些项应继续保留在页面设置面板中，哪些项应降级或隐藏，哪些项必须进入 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 J：AI 写入口对齐 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 正式回显。
- [x] AI 改写后树标题 / 页面头部 / 页面设置已回到同一组 page aggregate command family；剩余验证重点转为前台同步 smoke 的 local-first fixture 化。

补充：进入这一步时，最大的真实 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 与页面写入仲裁”，而不是“绕过系统写编辑器”。
- [x] local-first 下，普通正文 AI 写入已落到“授权文件 patch 或带 `expectedFileVersion` 的 `page.body.write`”；`mnote.page.save` 与历史 `mnote.doc.markdown_edit` 在 local_folder 分支均回到 `write_local_markdown_page_body`。当前 active 口径进一步收口为 `7-18`：普通 Markdown 编辑不再保留 MNote 专用编辑工具。

注：历史 `/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` 之前，不再继续零散补标题 / 宽度 / 页面设置。
- [x] 没有完成 `Phase G` 之前，不把当前文档页描述成“已经统一聚合完成”。
- [x] 没有完成 `Phase H` 之前，不把页面设置大量打钩为“正式可用”。
- [x] 没有完成 `Phase I` 之前，不把标题问题视为已从底层解决。
- [x] Phase J 的 local-first browser 回归已补齐；AI 写入主编辑区可按 local-first 主路径描述为稳定入口，仍需遵守 allowed roots、文件版本和冲突 UI 约束。

---

## 10. 当前阶段的总退出标准

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

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

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

补充：2026-05-22 复核后，标题 / 正文 / 页面设置三条链已经在 local-first 主路径上回到 Page Aggregate / page command family / client aggregate state；`documents.save` 只作为 cloud / compat 边界保留。AI 写入后的前台同步 browser 证据、dirty conflict UI 真实 Hermes changed-files 路径均已由 `task453` 补齐，因此本文件可从 `process/` 归档到 `done/`。

归档后的正确口径为：

> **local-first 主编辑区、页面聚合、BufferStore 冲突模型与 AI 写入同步已经在 Rust 主导的 Page Aggregate 主路径上基本闭环；后续新增字段仍按 Page Aggregate -> client state -> island/consumer 链扩展，不再新增第二份页面事实源。**

---

## 11. 审计记录（2026-05-21 Batch B Worker D）

### 11.1 各 Phase 完成状态

| Phase | 审计结论 | 说明 |
|-------|---------|------|
| F（冻结 Page Aggregate Contract） | ✅ 全部完成 | 设计层和代码层标准均已锁定，读取链已 Rust-first |
| G（主文档页消费统一聚合） | ✅ 基本完成 | 加载链路、散落 props、退出标准 1 已满足；exit 2（新增字段不扩容 props 链）为 P2 方向目标 |
| H（pageOptions 进入 island） | ✅ 基本完成 | 优先级项已 wired，Inspector 口径统一，AI 写回白名单限制；`protectEditing/showBlockRefCount` 仍为 planned |
| I（树域与页面域标题统一） | ✅ 全部完成 | task110 smoke 验证全部标题同步场景 |
| J（AI 写入口对齐 Page Aggregate） | ✅ 完成 | local_folder 写入已回 `page.body.write`；`allowedRoots` / 路径 escape / commandContext targeted tests 已覆盖；`task453` 覆盖 AI clean 前台同步和 dirty conflict UI |

### 11.2 2.2 节未成立事实的状态更新

| 项 | 审计状态 | 说明 |
|---|---------|------|
| 页面树/文件树/页面头部/页面设置/主编辑区尚未消费同一份 projection | ✅ 主路径完成 | 主编辑区、页头、标题投影、pageOptions runtime 语义已统一；新增字段仍需补 consumer 链但不扩第二事实源 |
| 写侧未完全退出 `/api/documents/save` | ✅ local-first 完成 | `local_folder` 已切到 `/api/page-body/write`；`/api/documents/save` 仅保留 cloud / compat 边界 |
| tiptap dirty + AI/外部变更的 VSCode-like conflict UI | ✅ 完成 | `task451` 覆盖冲突 UI 四路径，并断言 BufferStore 字段进入冲突信封 |
| `pageOptions` 收口到 island 运行时语义 | ✅ 基本完成 | `page-option-semantics.ts` 分类、Inspector 口径、AI 写回白名单和可见效果 smoke 已覆盖；`protectEditing/showBlockRefCount` 为 planned / 降级展示 |
| AI 写入口对齐授权/白名单/后台写入/前台同步 | ✅ 完成 | targeted tests 已覆盖 ACP env、file scope、CommandContext 写守卫和 LocalFS root escape；`task453` 汇总 AI 写入后 Page Aggregate / ProseMirror / changed files / clean 非冲突态 / dirty conflict UI |

### 11.3 退出标准状态

- Section 9：Phase G/H/I/J 约束均已可追认完成。
- Section 10：标题 / 正文 / 页面设置三条分裂真相链已在 local-first 主路径收口；AI 前台同步浏览器证据已补齐。

### 11.4 建议

1. **归档到 `done/`**：本清单不再占用 active `process/`。
2. **下一执行包转向**：当前新稿为 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`，直接围绕 allowed roots、file refs、agent 原生 patch/diff、watcher、BufferStore、Page Aggregate 前台同步和审计链设计；`design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md` 仅作为反例 / 历史材料。
3. **归档前复跑文档检查**：`git diff --check -- design rust/crates/mnote-web/src/ssr/pages/layout.rs rust/crates/mnote-web/src/routes/web_shell.rs scripts/task443-filetree-mindmap-click-active-row-smoke.js scripts/task445-filetree-mindmap-switch-no-flicker-smoke.js scripts/task453-local-folder-page-ai-changed-files-smoke.js`，并同步 CodeGraph。
