# Batch B Worker D：Page Aggregate / GFM AST Tail Audit

> 审计时间：2026-05-21
> 审计范围：
> - `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
> - `design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`

---

## 一、GFM AST 解析器迁移（3-13）审计

### 1.1 已完成可追认

| 清单项 | 证据 | 说明 |
|--------|------|------|
| 11.1 选型：comrak 解析器 | `local_markdown_parser.rs:131-152` 使用 `comrak::parse_document` | 确认选型完成，comrak 为成熟 GFM AST 实现 |
| 11.2 `MarkdownAstDocument` 中间结构 | `local_markdown_parser.rs:14-17` | 结构体已定义 |
| 11.2 block 级节点映射（全类型） | `local_markdown_parser.rs:19-51` enum `MarkdownBlock` | 覆盖 paragraph/heading/list/task/quote/code/divider/table/image/mindmap/media |
| 11.2 inline 级节点映射 | `local_markdown_parser.rs:103-113` struct `MarkdownInline` + `MarkdownInlineStyles` | 覆盖 text/code/bold/italic/strike/underline/link |
| 11.2 task list checked 状态 | `local_markdown_parser.rs:210-215` `TaskItem.symbol` | 正确分离 checked/unchecked |
| 11.2 表格单元格 inline mark | `local_markdown_parser.rs:256-259` 行内组合子复用 | 已验证 `local_folder_source.rs` 测试 `local_markdown_parser_preserves_pipe_table_as_table_block` |
| 11.3 映射集中到单一模块 | `local_markdown_parser.rs` 全文件 | 手写 parser 读侧分支已从 `local_folder_source.rs` 迁出 |
| 11.3 block document 与 web_shell 兼容 | `local_markdown_parser.rs:443-570` 输出格式 | tiptap 表结构、legacy 样式格式都输出 |
| 11.3 block id 稳定性 | `local_markdown_parser.rs:451` `format!("local-block-{block_number}")` | 基于块序号生成稳定 ID |
| 11.3 frontmatter 保存边界 | `local_folder_source.rs:2131-2135` 保存时保留 frontmatter | 有测试 `local_markdown_save_preserves_frontmatter_and_writes_basic_blocks` |
| 11.3 unsupported 节点降级 | `local_markdown_parser.rs:173-179` HTML block→paragraph | 转义为普通段落 |
| 11.4 inline code/bold/italic/strike/link 解析 | `local_markdown_parser.rs:264-305` | 全部通过 comrak AST 节点→MarkdownInline |
| 11.4 回写 inline mark | `local_folder_source.rs:6998-7100` `inline_nodes_to_markdown` | 测试 `local_markdown_save_writes_task_list_and_inline_marks` 验证回写 |
| 11.4 表格单元格 inline mark 回写 | `local_folder_source.rs:7838-7880` | 测试 `local_markdown_save_writes_table_inline_marks` + `local_markdown_save_round_trips_tiptap_table_marks` |
| 11.5 task list 全部 6 项 | 解析→回写→smoke | 测试 `local_markdown_parser_preserves_task_list_and_inline_marks` 解析 + `local_markdown_save_writes_task_list_and_inline_marks` 回写 |
| 11.6 pipe table 解析规则 | `local_markdown_parser.rs:150` `options.extension.table = true` | comrak 原生支持 |
| 11.6 单元格 inline IR 输出 | `local_markdown_parser.rs:593-610` `tiptap_inline_nodes_to_json` | `local_markdown_parser_preserves_pipe_table_as_table_block` 验证单元格内 code/bold |
| 11.6 表头/单元格类型稳定性 | `local_markdown_parser.rs:230-236` 基于 `TableRow(true)` | row header 标志通过 AST 保留 |
| 11.6 保存回写保留表格分隔行和列数 | `local_folder_source.rs:7838-7880` | `local_markdown_save_writes_table_alignment_markers` 验证列对齐标记 |
| 11.7 frontmatter title/mnote_id/页面设置写回 | `local_folder_source.rs:2175-2178` | 测试 `local_markdown_save_preserves_frontmatter` |
| 11.7 正文保存走 page.body.save 命令 | `local_folder_source.rs:2204-2205` | `canonicalCommand: "page.body.save"` |
| 11.8 `local_markdown` 测试组 | `local_folder_source.rs` 多个测试 | 覆盖基础块、task list、inline marks、table、附件链接 |
| 11.8 新增 AST 解析器单测 | `local_markdown_parser.rs:734-747` | 仅一个测试 `markdown_image_parses_as_image_block`（较薄弱） |
| 11.8 保存回写单测（round-trip） | `local_folder_source.rs` `local_markdown_save_writes_*` 系列 | task、inline marks、table 均已覆盖 |
| 11.8 浏览器 smoke | `scripts/task*` 系列 | `task164-page-options-visible-effect-smoke.js` 等，GFM smoke 待确认 |
| 11.8 确认 file tree / page tree / page aggregate 不受影响 | `web_shell.rs` 测试 | `document_shell_renders_local_markdown_with_same_sidebar_surfaces` 验证 |
| 11.9 移除 `local_folder_source.rs` 临时解析函数 | 读侧已使用 `local_markdown_parser.rs` | 手写读 parser 已移除，写侧 `inline_nodes_to_markdown` 保留 |
| 11.9 保留回归测试和兼容层 | 全部测试保留 | 删除策略不会清除验证资产 |

### 1.2 仍需代码修复

| 清单项 | 当前状态 | 问题描述 | 修复建议 |
|--------|---------|---------|---------|
| **11.2 标题优先级**：`frontmatter title > H1 > 文件名` | 标记 [x] 但**未实现** | `parse_markdown_page` (`local_markdown_parser.rs:86-94`) 始终使用 `file_stem_title(file_name)`，完全忽略 frontmatter 中的 title 字段和正文 H1。`tree.rs:7307` 测试显式断言 frontmatter title **不应出现**。 | 在 `resolve_local_markdown_page_aggregate` 中增加优先级解析：解析 frontmatter 取 `title` key，若无则扫描 AST 第一个 H1，若无则 fallback 到 `file_stem_title`。同时修改 `tree.rs:7275-7307` 测试预期。 |
| **11.4 legacy→Tiptap→legacy 不丢 marks** | 标记 [ ] 仍待处理 | `web_shell.rs:831-882` 的 `legacyStylesToTiptapMarks`/`legacyMarkArrayToTiptapMarks`/`mergeTiptapMarks` 做了一次方向转换，但反向（Tiptap→legacy）转换没有等价的自动化测试。 | 新增 `web_shell.rs` 测试：构造带所有 mark 类型（bold/italic/code/strike/link）的 editor block → 调用 legacyInlineContentToTiptap → 结果再反转换回 legacy → 断言 marks 未丢失。 |
| **11.6 列对齐、空单元格、带 mark 单元格的降级策略** | 标记 [ ] 仍待处理 | 列对齐已有测试 `local_markdown_save_writes_table_alignment_markers`（但 checklist 未更新）。**空单元格**和**降级策略**无测试覆盖。 | 为空的 table cell 补充 markdown_to_blocks 测试，明确返回空 paragraph 或 skip。为不支持的对齐/跨列行为补充降级断言。 |
| **11.7 保存失败时保留编辑器状态** | 标记 [ ] 仍待处理 | `save_local_markdown_page` 在冲突/写文件失败时返回 `WebError`，但无机制保证前端编辑器状态保留。前端 side 的 error handler 未在测试中验证。 | 补集成测试：注入错误的 `expected_conflict_detection_key` → 断言返回 `local_markdown_external_change` 错误码 + HTTP 409，且前端编辑缓冲区不清空。 |
| **11.8 web shell 单测：legacy↔Tiptap 转换不丢 marks** | 标记 [ ] 仍待处理 | `document_shell_bootstrap_preserves_inline_mark_conversion` 只验证 JS 转换函数的存在性（字符串包含），未验证实际转换结果。 | 补充利用 `app().oneshot()` 请求文档页 → 从 HTML 中提取 JS 上下文 → `page.evaluate()` 调用转换函数 → 验证输出 JSON 中 marks 完整性。 |
| **11.9 手写 parser 标记为过渡实现** | 标记 [ ] 仍待处理 | `local_folder_source.rs` 中 `inline_nodes_to_markdown` / `editor_blocks_to_markdown_with_rewrite` / `editor_blocks_to_markdown_for_file` 等写侧函数无任何 `#[deprecated]` 或文档注释标注过渡状态。 | 在写侧函数上加 `/// Transitional: will be replaced by AST-based reverse mapping in Step 3` 标注，并在模块文档注释中明确过渡路线。 |
| **11.9 web_shell.rs 临时适配分支** | 标记 [ ] 仍待处理 | `web_shell.rs:831-882` 的 `legacyStylesToTiptapMarks` 等 JS 函数是前端 legacy→Tiptap 适配层，仍无删除计划。 | 标注 `// TODO(step-4): remove when legacy block format is fully deprecated`，或跟踪 issue 链接。 |

### 1.3 追认修正：应标记为 [x] 但未标记

| 清单项 | 证据 | 操作建议 |
|--------|------|---------|
| 11.6 保存回写保留列对齐标记 | `local_markdown_save_writes_table_alignment_markers` 测试 | 应勾选 |
| 11.8 浏览器 smoke 验证 checkbox/code/strong 等 | `scripts/task*` smoke 系列（需确认具体文件名） | 如果存在则勾选 |

### 1.4 仅参考保留 / future

| 项 | 说明 |
|----|------|
| 11.2 空段落/空表格单元格/空引用块 | 这是 comrak 行为边界，G FM AST 天然处理，但**规则未文档化**。建议只补一个测试断言当前行为，不必在 P1 前重做。 |
| 11.6 列对齐降级策略 | 列对齐已实现，剩下"空单元格"和"不支持对齐方式的降级"可降为 future |
| 11.9 移除 web_shell.rs 临时适配分支 | 只有 legacy block 格式全面下线后才能移除，当前为 P2+ 项 |

---

## 二、Page Aggregate 清单（5-6）审计

### 2.1 已完成可追认

| 清单项 | 证据 |
|--------|------|
| Phase F 全部 4.x 设计层和代码层标准 | 大量补充记录确认 contract 已冻结、`page-aggregate-loader.ts` 消费 Rust endpoint、写入命令面收口 |
| Phase G 加载链路收口 + 散落 props 收口 | `/api/page-aggregate/:id` 为主读链、TS builder 降级为 410、`page-aggregate-client-state.ts` 统一 client state |
| Phase H 最小接入优先级 + 页面设置面板纠偏 | `wideLayout/smallText/layoutDensity/showHeadingNumbers/embedDefaultBlockId` 全部 wired，Inspector 文案统一 |
| Phase I 标题真相纠偏 + 回归验证 | `task110-page-title-single-truth-smoke.js` 验证页头/面包屑/sidebar/page tree/file tree/刷新/切页一致性 |
| Phase J 语义边界 + 命令名收口 | 命令名统一为 `page.head.updateTitle / page.layout.updateOptions / page.body.save` |
| 补充：EditorBlockDocument 块文档优先级 + 原生快照保留 | `build_page_aggregate_projection_result` 按 `editorDocument→blockDocument→documents.content` 优先级构建 |
| 补充：page.body.save 原生快照失败语义 | 不可解析 `editorDocument` 时返回 validation error，不回退到 `tiptapDocument` |

### 2.2 仍需代码修复

| 项 | 当前状态 | 问题描述 | 修复建议 |
|----|---------|---------|---------|
| **P1.1 写侧未完全退出 `/api/documents/save`** | 标记 [ ] 部分解决 | `local_folder` 已切到 `/api/page-body/write`，但 Convex workspace 仍走 `/api/documents/save` | 明确 `documents/save` 的定位：是 Convex 独占路径还是中间态 compat。如果是前者则更新文档说明，如果是后者则排期移除。 |
| **P1.2 tiptap dirty + AI/外部文件变更的 conflict UI** | 标记 [ ] 仍待处理 | 本地 folder 已有 `external-change-conflict` DOM 和 `mnote-conflict-*` 面板元素（`web_shell.rs:4990-5015`），但前端 side 的 conflict 流程覆盖测试不完备。 | 补充 Playwright smoke：watch 外部修改文件 → 断言 conflict 面板出现 → 选择"保留当前"或"接受磁盘" → 断言结果正确。 |
| **P1.3 pageOptions 收口到 leptos-tiptap island 运行时语义** | 标记 [ ] 部分解决 | 分类已完成（`page-option-semantics.ts`），但 `protectEditing/showBlockRefCount` 仍为 `planned` 未真正 wired。 | 如果 P1 目标不要求这些，建议在文档中明确标注"P2: protectEditing/showBlockRefCount 接线"。 |
| **P1.4 AI 写入口对齐"授权文件引用+白名单目录+后台文件写入+前台同步"** | 标记 [ ] 部分解决 | 命令面已收口到 page family，但"授权文件引用范围"（allowedRoots/aiAccessScope）的实现未在审计中验证。 | 检查 Hermes tool surface 上是否限制了 `allowedRoots`，以及 `page.body.write` 是否做了路径 escape 检查。 |
| **Phase G 退出标准 2：新增字段不再需 props 链外扩** | 标记 [ ] 部分解决 | 入口级痕迹消失但新增字段仍需补 loader/route/island 消费链。 | 建议在 `page-aggregate-loader.ts` + `page-aggregate-client-state.ts` 中增加一个 `passthroughUnknownFields` 机制，使新增聚合字段无需修改 loader 即可透传到消费侧。 |

### 2.3 仅参考保留 / future

| 项 | 说明 |
|----|------|
| Phase G exit 2（props 链不扩容） | 这是理想终态，当前架构下每个新字段仍需 route→loader→state→consumer 链改动。建议标记为 P2 方向性目标，不作为 P1 exit criterion。 |
| AI 写入口完整对齐 | 命令面已收口，但"授权引用"和"后台写入+前台同步"涉及 Hermes skill 层和 BufferStore，建议在专项 Hermes 审计中评估。 |
| Convex workspace 写侧仍走 `documents/save` | 如果 Convex 作为 cloud source 长期保留，`documents/save` 就是它的正式路径而不是 compat，应调整文档口径。 |

---

## 三、下一步最小执行包建议

### 立即修复（P1，本 worker 批次）

| # | 描述 | 难度 | 涉及文件 |
|---|------|------|---------|
| 1 | 修复 11.2 标题优先级：`parse_markdown_page` 改为 frontmatter > H1 > filename | 低 | `local_markdown_parser.rs` + `tree.rs` 测试预期 |
| 2 | 补 11.6 空单元格 / 降级策略测试 | 低 | `local_markdown_parser.rs` 测试 |
| 3 | 补 11.8 web shell 单测验证 legacy→Tiptap 转换不丢 marks | 中 | `web_shell.rs` 测试 |
| 4 | 标注写侧函数为过渡实现（11.9） | 低 | `local_folder_source.rs` 文档注释 |
| 5 | 标注 web_shell.rs 临时 JS 适配为过渡（11.9） | 低 | `web_shell.rs` JS 注释 |

### 文档追认（本 worker）

| # | 描述 | 涉及文档 |
|---|------|---------|
| 1 | 3-13 清单 11.2 标题优先级应改为 [ ]（当前误标为 [x]） | `design/03-rust-web/process/3-13-*.md` |
| 2 | 3-13 清单 11.6 列对齐已有测试，可追认 [x] | `design/03-rust-web/process/3-13-*.md` |
| 3 | 5-6 清单补充本次审计时间戳和裁定结论 | `design/05-editor-mainline/process/5-6-*.md` |

### 后续优先级（P2+）

| # | 描述 |
|---|------|
| 1 | 写侧从 `inline_nodes_to_markdown` 手写规则迁移到 AST 反向映射（3-13 Step 3） |
| 2 | 移除 `web_shell.rs` legacy→Tiptap JS 适配层 |
| 3 | Convex workspace 写侧统一到 `page.body.write` |
| 4 | conflict UI Playwright smoke |

---

## 四、风险

- **标题优先级误标 [x]**：如果 frontmatter title 与 filename 不一致，用户看到的是文件名而非意图标题。当前文件取名时 frontmatter title 通常与文件名一致，所以影响有限，但仍须修复。
- **手写写侧函数仍无 AST 反向映射**：`inline_nodes_to_markdown` 等函数维护着一套独立的样式→markdown 规则，与 AST 解析器的理解可能产生细微不一致（如 `_` 与 `*` 的选择）。不是 P1 blocker，但建议在 Step 3 统一。
- **`documents/save` 双重语义**：对 `local_folder` 是 compat，对 `convex_workspace` 是正式路径。如果口径不统一，未来新增 workspace type 时会困惑。
