# Worker B：7-27 代码/测试缺口审查

> 审查时间：2026-05-20
> 审查范围：`doc.rs` + `hermes_tools.rs` + `manifest.rs`
> 分级：🟢 已满足 / 🟡 可作为后续增强 / 🔴 必须修复才能归档

---

## 检查点 1：`blocks_to_markdown` 是否能从 legacy `content`/`contentNodes` 读取文本

**结论：🟢 已满足**

`legacy_block_text()` 函数 (`doc.rs:973-986`) 逐级回退：
```rust
fn legacy_block_text(block: &Value) -> String {
    if let Some(text) = block.get("text").and_then(Value::as_str) { ... }
    if let Some(content) = block.get("content") { return inline_text_value(content); }
    if let Some(content_nodes) = block.get("contentNodes") { return inline_text_value(content_nodes); }
}
```

`block_text()` (`doc.rs:1265-1270`) 只读 `text` 字段用于 markdown 序列化，但 `build_page_content` 中的 `update_legacy_block_text_for_markdown_edit` 正确使用了 `legacy_block_text` 做比较，且写回时通过 `replace_single_inline_text_value` 递归处理 `content`/`contentNodes` 中的 `text` 字段。

**证据**: `inline_text_value()` (`doc.rs:988-1000`) 递归处理 Array/Object/String 三种形态。

---

## 检查点 2：注释格式是否与 7-27 文档一致，是否保留 `revisionRef`

**结论：🟡 可作为后续增强**

当前格式：
```
<!-- block:{id}:{type} -->
<!-- block:{id}:{type}:level={level} -->  (heading 块)
```

7-27 §3.3.1 设计为：
```
<!-- block:{id}:{type}:{revision_ref} -->
<!-- block:{id}:{type}:{revision_ref}:level={level} -->  (with level)
```

**差异**：`revisionRef` 不写入注释，而是从原始 `block.revisionRef` 读取。解析器 `extract_block_comment()` 虽然支持 `type:rev` 格式（`splitn(3, ':')` 后 `parts[2]` 可以包含 rev），但 `blocks_to_markdown` 写回时不输出 rev。

**影响**: 低。`revisionRef` 在整个 `build_page_content` 路径中被正确继承（`doc.rs:685-690`），只是注释里看不到。不阻碍归档，可后续增强。

---

## 检查点 3：`full_content` 是否不再被拒绝，是否保留复杂块

**结论：🟢 已满足**

- `doc_markdown_edit` 中 `full_content` 转为 `{search: current_md.trim(), replace: full.trim()}` 单个 operation，进入新路径
- `parse_final_markdown_to_blocks` 对无注释行创建 `is_new=true` 的 paragraph 块
- `build_page_content` 追加新块并保留原始复杂块（`is_complex_markdown_original_block`）
- 测试 `test_build_page_content_full_content_replaces_old_text_blocks_but_keeps_complex_blocks` 验证复杂块保留
- 集成测试 `hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping` 注释确认："7-27 修复后：full_content 不再被拒绝"

---

## 检查点 4：`dryRun` 是否不写入

**结论：🟢 已满足**

`doc_markdown_edit` 中：
```rust
if input.dry_run == Some(true) {
    return Ok(json!({
        "written": false,
        "dryRun": true,
        "diff": changed_blocks
    }));
}
```

本地文件路径也有独立的 `dryRun` 分支只返回预览。

测试 `hermes_tools_markdown_edit_local_dry_run_does_not_write` 验证。

---

## 检查点 5：selection / allowedTargetBlockIds 是否阻止越界写入

**结论：🟢 已满足**

`ensure_markdown_changed_blocks_within_allowed()` (`doc.rs:1050-1073`) 在写回前检查每个 `changedBlocks` 中的 `blockId` 是否在 `allowed_block_ids` 中。越界时返回 `mnote_markdown_edit_target_out_of_scope` 错误。

测试 `hermes_tools_markdown_edit_rejects_selection_out_of_scope` 验证。

`selected_block_ids()` (`doc.rs:1075-1094`) 从 `selectedBlockIds` / `allowedTargetBlockIds` / `selectedBlockId` / `blockId` 四个参数中收集。

---

## 检查点 6：旧 `build_block_ops_from_markdown_edit` 推导路径

**结论：🟢 已满足（已退役）**

`doc.rs:1911` 末尾注释：
```rust
// 退役：7-27 改为 final_md → blocks → execute_page_body_save_from_aggregate 直接写回
```

函数体仍保留但不再被任何调用者引用（`doc.rs` 内只有一处调用已在步骤 4 中被替换）。可用 `cargo +nightly deadlinks` 确认死代码状态，但已足够安全。

---

## 检查点 7：测试覆盖是否足以归档

**结论：🟢 已满足**

### 单元测试覆盖（doc.rs 内）

| 测试 | 验证点 |
|------|--------|
| `test_search_replace_exact` | 精确匹配替换 |
| `test_search_replace_normalized_chinese_byte_boundaries` | 中文忽略空白 + byte 边界保留 |
| `test_search_replace_full_content` | 全文替换 = 全文精确匹配 |
| `test_blocks_to_markdown_with_ids` | 块 ID 注释输出 |
| `test_blocks_to_markdown_heading` | heading 类型 + level 注释 |
| `test_blocks_to_markdown_complex_block_uses_raw_marker` | 复杂块 raw marker |
| `test_build_page_content_full_content_replaces_old_text_blocks_but_keeps_complex_blocks` | full_content 新块 + 复杂块保留 |
| `test_build_page_content_preserves_missing_original_text_block_when_some_ids_remain` | ID 消失时保留原始块 |
| `test_build_page_content_preserves_unchanged_inline_content_nodes` | 行内 marks 保留（不变时） |
| `test_build_page_content_preserves_single_inline_node_marks_when_text_changes` | 行内 marks 保留（文本改变时） |

### 集成测试覆盖（hermes_tools.rs）

| 测试 | 验证点 |
|------|--------|
| `hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping` | full_content 不再拒绝 |
| `hermes_tools_markdown_edit_merges_same_block_operations` | 同块多次操作累积 |
| `hermes_tools_markdown_edit_maps_normalized_search_to_block` | 非精确匹配 + changedBlocks 验证 |
| `hermes_tools_markdown_edit_online_page_body_save_carries_revision_conflict_key` | revision/conflict key 传递 |
| `hermes_tools_markdown_edit_rejects_no_applied_operations` | 空 operations 拒绝 |
| `hermes_tools_markdown_edit_rejects_selection_out_of_scope` | selection 越界拒绝 |
| `hermes_tools_markdown_edit_reports_empty_block_mapping_before_apply` | 空 block mapping 错误报告 |
| `hermes_tools_markdown_edit_shared_read_is_forbidden` | 共享文档只读拒绝 |
| `hermes_tools_markdown_edit_local_dry_run_does_not_write` | local dryRun |
| `hermes_tools_markdown_edit_local_folder_writes_same_markdown_file` | local folder 写入 |
| `hermes_tools_markdown_edit_local_requires_write_contract` | local write contract 要求 |

### 缺口：建议后续增强（非阻塞）

1. **无块注释的多段 markdown 还原**：当前行级解析器假设每行一个块；多段文本（如代码块）需 GFM fallback（§5.3 有分析但未实现）
2. **`revisionRef` 在注释中可见**：当前只在内存中保留，注释格式未输出 rev（§3.3.1 设计差异）
3. **`build_block_ops_from_markdown_edit` 函数体删除**：当前已退役但函数体仍在文件中

---

## 总结

| 分级 | 数量 | 项目 |
|------|------|------|
| 🔴 必须修复才能归档 | 0 | — |
| 🟡 后续增强 | 3 | 注释写 rev, GFM fallback 多段文本, 死代码体删除 |
| 🟢 已满足 | 7/7 | 全部检查点通过 |
