# 17 Sidex / MNote Workbench Gap Execution Checklist v1

> 状态：done
>
> 创建时间：2026-05-27
>
> Owner：10-review / 05-editor-mainline / 03-rust-web / 04-tree-domain / 07-ai
>
> 目标：把本轮 Sidex / VSCode 对照审查转成可执行 checklist，优先补齐 MNote 当前最缺的统一工作台服务层，而不是继续扩散 UI 分支或照搬 VSCode 全量复杂度。

## 1. 背景结论

MNote 当前已经具备知识库型 VSCode-like workspace 骨架：

- Rust Web 3000 主壳、SSR 文档页和独立 browser runtime asset。
- Resource Tree / File Tree / Page Tree 三层模型。
- local-first Markdown、Page Aggregate、leptos-tiptap island。
- resource tab、secondary pane、DocumentBuffer、冲突面板。
- tree live WS/SSE、本地文件夹 watcher、本地搜索索引。
- Hermes / Reasonix local-first 文件编辑控制面。

相比 Sidex / VSCode，当前最大差距不是某个控件，而是统一服务层仍分散：

- `ObjectWorkspacePath` / `KernelObjectIdentity` 没有成为 browser runtime 的统一身份消费入口。
- `BufferStore` 已存在，但 tiptap、watcher、AI、resource tab 仍有各自 session / dirty / conflict 状态。
- resource tab / open editors 仍是页面内 Map，不是可恢复、可被 Sidebar / 快捷键 / AI target resolver 共同消费的轻量 workbench editor model。
- command context / when / keybinding / menu enablement 在 Rust core-protocol 与 browser JS 中各有简化版本。
- tree live 仍有 local-folder SSE、watch batch、projection refresh、polling fallback 多链并存。
- `tree.*` 已是 preferred command，但 compat 路径仍保留 `documents.*` 映射。

## 2. Sidex 对照边界

### 2.1 值得借鉴

| Sidex / VSCode 模型 | MNote 对应改进 |
| --- | --- |
| `ExplorerModel -> ExplorerService -> ExplorerView` 三层分工 | FileTree / PageTree 继续拆成 projection data source、runtime service、DOM view state，不让 DOM 自己拼真相 |
| `IEditorService.openEditor` + `IEditorGroup` | 建立轻量 `MNoteEditorInput` / `MNoteEditorGroupState` / `OpenEditorsSnapshot`，统一 page、resource tab、secondary pane |
| `EditorGroupModel` 管 active / preview / sticky / transient / MRU / selection | resource tab 补 preview / pinned / dirty close veto / MRU / keyboard action 的模型层 |
| ContextKeyService + CommandsRegistry + KeybindingsRegistry | MNote 建立统一 command context，用 Rust command contract 驱动 browser menu / shortcut enablement |
| Working Copy dirty / save / backup / conflict | BufferStore 成为 tiptap、watcher、AI、resource tab 的唯一打开态和版本仲裁面 |
| Explorer watcher 节流、编辑中抑制、局部 parent refresh | local-folder watcher / filetree projection 只刷新受影响 parent，避免整树替换与编辑冲突 |

### 2.2 不照搬

- 不引入完整 VSCode DI / service container。
- 不把文件系统 Explorer 当成 MNote 树真相；MNote 真相仍是 Rust kernel projection。
- 不引入完整 Extension Host、Editor Override、Auxiliary Editor、多窗口 grid。
- 不把 Monaco TextModel 当成 Markdown 事实源。
- 不把 Sidex 的 `ResourceFileEdit` 直接替代 `tree.*` / `tree.resource.*` 命令面。
- terminal / git / debug / tasks / extensions 是否进入 MNote，单独做产品边界决策，不混入本 checklist 的 P0。

## 3. 执行原则

- 优先做服务层统一，不做大规模视觉重写。
- 每一批必须有 smoke 或 Rust/JS 定点测试；浏览器可见能力必须有真实用户路径验证。
- 不新增第二套树真相、页面真相或资源生命周期真相。
- 不碰与本 checklist 无关的历史清理、回收区删除或用户已有改动。
- 对旧 compat 只做可证明的瘦身；不能为了“干净”破坏 cloud / remote / legacy 边界。

## 4. Checklist

### Batch 0：基线取证与任务拆分

状态：done

- [x] 对照 Sidex Explorer / EditorGroup / command / working copy 模型。
- [x] 对照 MNote Rust Web / browser runtime 当前能力。
- [x] 对照现有 design / smoke / process backlog。
- [x] 确认本轮不做代码清理、不改用户已有脏改动。
- [x] 输出本执行 checklist，并把后续工作拆成 P0 / P1 / P2。

验收证据：

- `reference-code/sidex-main/src/vs/workbench/contrib/files/common/explorerModel.ts`
- `reference-code/sidex-main/src/vs/workbench/contrib/files/browser/explorerService.ts`
- `reference-code/sidex-main/src/vs/workbench/common/editor/editorGroupModel.ts`
- `rust/crates/core-protocol/src/kernel.rs`
- `rust/crates/mnote-web/src/document_buffer_store.rs`
- `rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
- `rust/crates/mnote-web/browser/tree-live-controller.js`

### Batch 1：轻量 Workbench Editor Model

状态：done

目标：

- 定义 MNote 轻量 `EditorInput` / `EditorGroupState` / `OpenEditorsSnapshot` 合同。
- 统一 page、resource tab、secondary pane 的打开态。
- 让 Sidebar、快捷键、AI target resolver 和 smoke 都能消费同一个 open editors snapshot。

Sidex 对照：

- Sidex `EditorGroupModel` 持有 active editor、preview editor、sticky count、MRU、selection。
- Sidex Explorer 打开资源统一走 `IEditorService.openEditor({ resource, options })`。
- MNote 不照搬多 group grid，只保留 primary / secondary pane 和 resource tab 的轻量模型。

待办：

- [x] 盘点当前 open editor 数据源：page tab、resource tab registry、secondary pane、URL query、`window.__mnoteOpenEditorsSnapshot`。
- [x] 写一个最小合同文档或 Rust/JS 类型注释，明确字段：
  - `objectIdentity`
  - `workspacePath`
  - `paneRole`
  - `editorKind`
  - `active`
  - `dirtyState`
  - `preview`
  - `pinned`
  - `lastActiveAt`
- [x] 为 `document-resource-tab-runtime.js` 的 open editors snapshot 增加 primary/secondary 分组语义。
- [x] 补 smoke：打开页面、打开 mindmap/office/code/text 资源、切换 tab、刷新后至少能恢复 active page/resource 的可见状态。
- [x] 补 AI target resolver 输入：last focused editor / resource tab 应来自 open editors snapshot，不再从 DOM 临时猜。

本轮最小实现切片：

- [x] `OpenEditorsSnapshot` 保持 `mnote.open_editors_snapshot.v1`，新增 `groups.primary` / `groups.secondary`。
- [x] page editor entry 与 resource editor entry 均输出 `paneRole`、`editorKind`、`preview`、`pinned`、`lastActiveAt`。
- [x] resource tab entry 补 `documentId` / `workspaceId`，为后续 `ObjectWorkspacePath` 消费统一预留字段。
- [x] page / resource editor entry 补 `workspacePath`，合同以 `mnote.workspace_path.v1` 表达 workspace/source/root/relativePath/documentId/objectIdentity。
- [x] passive resource（Office/PDF/image 等无 session tab）激活后也写入 `lastActiveAt`，避免 MRU / AI target resolver 后续只能看到 0。
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 primary markdown resource active、primary pinned page、secondary page editor。
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 Office passive resource active snapshot 的 `documentId` / `workspaceId` / `editorKind` / `lastActiveAt`。
- [x] `task457-main-editor-resource-tab-smoke` 覆盖 canonical `resourceTab` URL 刷新后恢复 active markdown resource tab，并验证 `workspacePath.relativePath`。
- [x] `sidebar-page-ai-runtime.js` 从 `OpenEditorsSnapshot` 生成 `mnote.ai_editor_target.v1`，写入 `pageContext.aiContext.activeEditorTarget`、`openEditorsSnapshot` 与 `/api/hermes/client/runs` 的 `editorTarget`。
- [x] `hermes_client.rs` 在 local source instructions 中保留 `editorTarget`，避免 agent 只靠 URL / DOM 猜当前编辑目标。
- [x] 同步 `task457` 中已过期的 Office 默认 iframe 断言：默认主编辑区是 `/office-preview` 轻量预览，显式 `new-window` 仍走 `/onlyoffice?mode=edit`。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check scripts/task457-main-editor-resource-tab-smoke.js
git diff --check -- design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md rust/crates/mnote-web/browser/document-resource-tab-runtime.js scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node --check scripts/task501-office-preview-light-viewer-smoke.js
node scripts/task501-office-preview-light-viewer-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web office_preview_page_serves_lightweight_viewer_shell -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_body_local_source_uses_file_scope_not_full_page_context -- --test-threads=1
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
```

建议验证：

```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
```

### Batch 2：ObjectWorkspacePath / Resource Identity runtime 消费统一

状态：done

目标：

- browser runtime 不再主要依赖 `documentId + rootUri + relativePath + assetId` 临时拼接身份。
- FileTree、resource open、copy-id、AI scope、tree command 参数统一消费 `ObjectWorkspacePath` / `KernelObjectIdentity`。

Sidex 对照：

- Sidex ExplorerItem 的 identity 基于 root resource + item resource。
- MNote identity 必须多带 `workspaceId/sourceKind/rootUri/relativePath/objectIdentity`，不能照搬纯文件 URI。

待办：

- [x] 盘点 browser runtime 中手拼 identity 的位置：
  - `filetree-runtime.js`
  - `sidebar-filetree-command-runtime.js`
  - `document-resource-tab-runtime.js`
  - `resource-open-runtime.js`
  - `sidebar-filetree-open-runtime.js`
- [x] 为 FileTree row 输出 / 读取建立统一 helper：`readWorkspacePathFromRow(row)`。
- [x] `copy-id` 和 resource tab key 优先使用 workspacePath / objectIdentity。
- [x] local_folder 下绝对路径还原继续从 `rootUri + relativePath` 得出，但只作为展示/复制结果，不作为内部 identity。
- [x] 补 smoke：local_folder Markdown、folder、asset、mindmap、office row 的 identity 一致且复制路径不暴露 `local:*` projection id。

本轮最小实现切片：

- [x] `filetree-runtime.js` 新增 `readWorkspacePathFromRow(row, deps)`，统一读取 `workspaceId/sourceKind/rootUri/relativePath/objectIdentity/rowId/documentId/assetId/title`。
- [x] `fileTreeAssetDownloadDetail` 与 filetree context menu detail 开始携带 `workspacePath`，为后续 copy/open/command context 统一消费铺底。
- [x] `task495-filetree-copy-id-absolute-path-smoke` 增加 `readWorkspacePathFromRow` 定点断言，同时保持 local_folder 复制 ID 返回绝对路径。
- [x] `sidebar-tree-runtime.js` 的 filetree open / asset open event detail 携带 `workspacePath`，resource open 不再只能从 DOM 临时拼身份。
- [x] `sidebar-filetree-open-runtime.js` 用 `workspacePath.objectIdentity/rootUri/relativePath/assetId` 生成 resource tab object identity，覆盖 local file、mindmap、office、pdf 和侧栏打开。
- [x] `document-resource-tab-runtime.js` 在 resource tab entry 中保留 `workspacePath`，snapshot 输出优先沿同一 workspacePath 合同。
- [x] `local_folder_source.rs` 的真实 local_folder projection 为 mindmap/office/asset row 写入 `workspacePath.objectIdentity.assetId`，避免资源行 identity 碰撞。
- [x] `filetree-runtime.js` 收窄 `isLocalFolder` 判定：只信 `sourceKind=local_folder`，或 sourceKind 缺失时的明确 local row/doc 前缀，避免误伤 cloud/compat id。
- [x] 移除已退役表格引擎的 legacy 分类分支；表格资源只按当前 `table` object/icon kind 识别。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check rust/crates/mnote-web/browser/filetree-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-open-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_file_tree_classifies_mindmap_and_office_assets -- --test-threads=1
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task501-office-preview-light-viewer-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-resource-tab-runtime.js rust/crates/mnote-web/browser/filetree-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-open-runtime.js rust/crates/mnote-web/browser/sidebar-tree-runtime.js rust/crates/mnote-web/src/routes/local_folder_source.rs scripts/task495-filetree-copy-id-absolute-path-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
```

边界记录：

- `node scripts/task456-resource-object-shell-sync-smoke.js` 当前走已退役 Convex compat，失败为 `503 convex_retired`，不作为 local-first Batch 2 验收入口。
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js` 暴露上传后 dirty conflict 与删除附件后等待超时，归入 Batch 3/资源生命周期后续处理，不在本 identity 切片扩大修复面。

建议验证：

```bash
node --check rust/crates/mnote-web/browser/filetree-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
node scripts/task456-resource-object-shell-sync-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```

### Batch 3：BufferStore / Working Copy 唯一打开态

状态：done

目标：

- `BufferStore` 成为 tiptap、watcher、AI、resource tab 的统一 dirty / stale / external modified / deleted 仲裁面。
- browser session 的 dirty/conflict 状态不再独立形成第二套事实。

Sidex 对照：

- VSCode WorkingCopyService 统一 dirty、save、backup、conflict。
- MNote 不引入完整 backup service；先把 local-first Markdown 的 dirty/stale/deleted 状态统一到 BufferStore。

待办：

- [x] 盘点 `documentSessionRegistry`、localStorage draft、`conflictDetectionKey`、`expectedFileVersion` 当前写读链。
- [x] 让 watcher 标记外部修改后，前端 session 从 BufferStore / aggregate 读取状态，而不是只靠本地 session flag。
- [x] 资源 tab close guard 读取统一 dirty state。
- [x] AI 写入前检查目标 buffer dirty/stale；dirty 时必须显式确认或阻断。
- [x] 补 smoke：clean buffer 外部写入可同步；dirty buffer 外部写入弹冲突；删除当前文件后 tab 不静默保留旧正文。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 `WorkingCopyService` 的统一 dirty/event 仲裁；不引入 VSCode 完整 backup/hot-exit/URI provider。
- [x] `documents.rs` 增加 `POST /api/documents/buffer-state/dirty`，浏览器编辑变 dirty 时写入 Rust `BufferStore::mark_dirty`，保存成功仍由 Rust 写链 `mark_saved` 清理。
- [x] `document-session-runtime.js` 增加 `fetchSessionBufferState()` / `applyBufferStateToSession()`，外部刷新前先读取 `/api/documents/buffer-state`，对 `Dirty/Stale/Deleted` 进入冲突/删除保护，不再只靠本地 session flag。
- [x] `queueSessionSave()` 在 autosave 排队时同步标记 BufferStore dirty，形成 watcher / AI / save 共享的打开态。
- [x] clean 外部写入、dirty 外部写入、保存失败保留编辑器内容、AI dirty 写入冲突路径均已通过 smoke。
- [x] `sidebar-page-ai-runtime.js` 在发起 Hermes run 前读取 OpenEditorsSnapshot 与 `/api/documents/buffer-state`，当目标 buffer 为 `Dirty` / `Stale` / `Deleted` / `ExternalModified` 时阻断 AI 写入，不再让 dirty buffer 进入 agent 写盘后冲突。
- [x] `document-resource-tab-runtime.js` 的 page entry 从当前 `documentSessionRegistry` 输出 dirty state，避免页面刚输入但 `/buffer-state/dirty` 仍未完成时 Page AI 被放行。
- [x] `document-resource-tab-runtime.js` 的 resource tab close 在关闭前 await 读取 BufferStore dirty state，并把 `Dirty` / `Stale` / `Deleted` / `ExternalModified` 纳入 close veto；本地 resource session 的小写 `dirty` 同时被 Page AI 阻断逻辑规范化识别。
- [x] `local-upload-runtime.js` 上传保存成功后广播 `mnote:local-upload-editor-save-completed`，`document-session-runtime.js` 同步 session fileVersion / conflict key，避免编辑器自己写盘被 watcher 误判为外部冲突。
- [x] `sidebar-attachment-open-runtime.js` 为 local_folder 附件链接增加存在性刷新、missing 状态缓存、stat 请求序号和幂等 DOM 写入；删除真实附件后 Markdown 链接保留，点击进入 resource tab missing/error state；stat 非 OK 不再伪装成 missing，而是写入诊断属性。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/document-session-runtime.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_buffer_state_falls_back_to_document_id_without_relative_path -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_mark_dirty_and_saved_round_trip -- --test-threads=1
node scripts/task436-local-markdown-open-document-external-change-smoke.js
node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js
node scripts/task486-local-markdown-save-error-editor-preserves-content-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```

补充验证证据（2026-05-28）：

```bash
node --check rust/crates/mnote-web/browser/document-session-runtime.js
node --check rust/crates/mnote-web/browser/local-upload-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
node --check scripts/task460-resource-tab-close-dirty-smoke.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
node --check scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-resource-tab-runtime.js rust/crates/mnote-web/browser/document-session-runtime.js rust/crates/mnote-web/browser/local-upload-runtime.js rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js scripts/task453-local-folder-page-ai-changed-files-smoke.js scripts/task460-resource-tab-close-dirty-smoke.js scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_buffer_state_falls_back_to_document_id_without_relative_path -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web document_buffer_mark_dirty_and_saved_round_trip -- --test-threads=1
node scripts/task436-local-markdown-open-document-external-change-smoke.js
node scripts/task451-local-markdown-conflict-resolution-ui-smoke.js
node scripts/task486-local-markdown-save-error-editor-preserves-content-smoke.js
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task460-resource-tab-close-dirty-smoke.js
node scripts/task457-main-editor-resource-tab-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```

边界记录：

- Sidex / VSCode WorkingCopyService 的统一 dirty / conflict / backup 模型只借鉴为 BufferStore 仲裁面；MNote 暂不引入完整 hot-exit / backup service。
- Page AI dirty buffer 策略本批选择阻断，不做确认弹窗；后续若需要“显式确认后继续”，应进入 07-ai 单独交互设计。
- `task479` 的 broken link 检查在 headless 中显式调用现有 attachment refresh hook，避免浏览器定时器调度导致误判；真实 runtime 仍保留 tree event 与定时刷新。该项只证明 refresh/runtime 行为正确，不单独证明定时器或 tree event 调度可靠，后续 Batch 5 需要补真实 event 链路覆盖。

### Batch 4：Page Aggregate legacy fallback 退役

状态：done

目标：

- 继续减少 `documents.content` / legacy body payload / compat projection 在 runtime 主链中的占比。
- Page Aggregate 成为页面读取和编辑刷新事实入口。

Sidex 对照：

- Sidex editor model 有明确输入和持久化模型。
- MNote 的主模型不是 Monaco TextModel，而是 local Markdown -> Page Aggregate -> tiptap projection。

待办：

- [x] 盘点 `document-tiptap-conversion-runtime.js` 中 legacy conversion 的 runtime 调用面。
- [x] 区分 local-first `.md` projection、cloud compat projection、fixture/test helper。
- [x] 为 Page Aggregate 输出增加或验证 source 标记：避免 UI 偏好 / 页面事实 / compat fallback 混淆。
- [x] 逐步把可控 runtime fallback 改成显式 degraded / compat owner。
- [x] 补 smoke：刷新后 body/blockDocument/options/source 一致，不能静默读旧 legacy 内容。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 `EditorInput` 显式 input/resource/capabilities 边界；MNote 不照搬 EditorResolver，只把页面正文 source 分类写入 runtime session。
- [x] `document-tiptap-conversion-runtime.js` 增加 `pageBodyTiptapDocumentSource()`，把 `local_markdown.content`、`page_aggregate.block_document`、`compat.legacy_content`、`degraded.fallback_text`、`empty` 显式分类，避免无标记地从 legacy `body.content` 猜正文来源。
- [x] `document-session-runtime.js` 保存 `pageBodySource/projectionSource/blockProjectionVersion`，`document-editor-adapter-runtime.js` 在编辑器 root 上写入 `data-mnote-page-body-source` / `data-mnote-projection-source` / `data-mnote-block-projection-version`。
- [x] `task167-local-markdown-title-body-options-no-convex-smoke.js` 断言 local-first Page Aggregate 的 `projectionSource=local_markdown.content` 与 `blockDocument` 存在，保存后不退回 legacy `documents.content`。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/document-tiptap-conversion-runtime.js
node --check rust/crates/mnote-web/browser/document-session-runtime.js
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
node --check scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
git diff --check -- rust/crates/mnote-web/browser/document-tiptap-conversion-runtime.js rust/crates/mnote-web/browser/document-session-runtime.js rust/crates/mnote-web/browser/document-editor-adapter-runtime.js scripts/task167-local-markdown-title-body-options-no-convex-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task484-local-folder-page-body-refresh-readback-smoke.js
node scripts/task493-page-settings-sqlite-preferences-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_returns_local_markdown_readonly_snapshot -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_options_metadata_flows_into_page_aggregate -- --test-threads=1
```

边界记录：

- `node scripts/task-page-aggregate-body-sync-smoke.js` 与 `node scripts/task-page-aggregate-refresh-persistence-smoke.js` 当前通过旧 `/api/tree/commands` 入口创建文档，失败为 `503 convex_retired`；它们属于 legacy/cloud helper，不作为 local-first Batch 4 验收入口。
- 本批只把 fallback 分类显式化并锁住 local-first source，不删除 legacy conversion 函数；cloud compat / fixture 仍可显式走 `compat.legacy_content`。

建议验证：

```bash
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task-page-aggregate-body-sync-smoke.js
node scripts/task-page-aggregate-refresh-persistence-smoke.js
```

### Batch 5：Tree live cache / FileTree data source 收敛

状态：done

目标：

- 减少 watcher/SSE/projection refresh/polling 多链并存。
- FileTree refresh 按受影响 parent 局部刷新，保留展开、选择和焦点。

Sidex 对照：

- Sidex AsyncDataTree 按 node resolve children，有 in-flight 去重、slow state、view state。
- MNote 已有 lazy/cache/view state 设计，继续把它固化为 data source 而不是整树 DOM 替换。

待办：

- [x] 盘点 `/api/local-folder/events?treeLive=true`、`tree:delta`、`watch_batch`、projection refresh、polling fallback 的调用关系。
- [x] 定义统一 local-folder tree event payload 到 filetree parent refresh 的映射。
- [x] 整树替换只保留在 root scope 切换或 resync 场景。
- [x] view state 保存只存 expanded/selected/focused 等用户状态，不存 rows/cache 派生数据。
- [x] 补大目录真实用户路径 smoke：从首页进入本地 workspace，点击星标/Explorer 逐级打开目标 Markdown，记录 longtask 和 editor ready。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 `ExplorerModel` / `ExplorerService` / `AsyncDataTree` 的节点身份、局部 children resolve、in-flight 去重和 view state 思路；MNote 不照搬 VSCode 完整树控件，仍以 Rust projection / watcher revision 为事实源。
- [x] `sidebar-tree-live-apply-runtime.js` 在 `watch_batch` 的 `fallbackResync/requiresResync` 下显式走 `refreshLocalFolderSidebarSnapshot()`，非 resync 情况只刷新受影响 parent。
- [x] `watch_batch` 缺少 `affectedParents` 但包含 `changedPaths` 时，从 changed path 派生 parent scope，并写入 `data-mnote-local-folder-watch-batch-derived-parents` 作为诊断标记。
- [x] 修复 scoped navigation shell 缺 primary editor host 导致 `openPrimaryDocument()` 抛 `pane_root_missing_primary` 并回退整页 reload 的问题：`HomePage` navigation 分支保留导航 placeholder，同时渲染隐藏 primary `DocumentPane` 供 pane runtime 原地替换。
- [x] `document-editor-adapter-runtime.js` 在 primary 文档成功激活后隐藏 navigation placeholder，保持点击 scoped FileTree Markdown 后不重建侧栏、不丢 JS marker。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js
node --check rust/crates/mnote-web/browser/document-editor-adapter-runtime.js
rustfmt --edition 2021 --check rust/crates/mnote-web/src/ssr/pages/home.rs
git diff --check -- rust/crates/mnote-web/browser/sidebar-tree-live-apply-runtime.js rust/crates/mnote-web/browser/document-editor-adapter-runtime.js rust/crates/mnote-web/src/ssr/pages/home.rs scripts/task494-filetree-lazy-loading-dedup-smoke.js design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md
node scripts/task494-filetree-lazy-loading-dedup-smoke.js
node scripts/task492-sidebar-starred-shortcuts-smoke.js
node scripts/task497-local-page-tree-filetree-open-performance-smoke.js
node scripts/task499-sidebar-tree-view-state-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder_without_page_renders_navigation_page_without_editor_bootstrap -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder_scope_renders_navigation_without_root_sibling_document -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_active_page_includes_document_panes_bootstrap -- --test-threads=1
```

边界记录：

- `watch_batch` 到 FileTree parent refresh 只消费 Rust local-folder event payload；前端不新增 rows/cache 事实源。
- `view-state` smoke 证明持久化内容仍是 expanded / selected / focused / active / scroll 等用户状态，不把 rows 或 projection cache 写入 view state。
- scoped navigation shell 的 primary `DocumentPane` 是 editor host 能力补齐，不改变 local_folder navigation page 的 Page Aggregate / editor bootstrap 初始加载边界；导航页仍不输出 `__MNOTE_PAGE_AGGREGATE__` 或 `__MNOTE_EDITOR_BOOTSTRAP__`。

建议验证：

```bash
node scripts/task492-sidebar-starred-shortcuts-smoke.js
node scripts/task494-filetree-lazy-loading-dedup-smoke.js
node scripts/task497-local-page-tree-filetree-open-performance-smoke.js
node scripts/task499-sidebar-tree-view-state-smoke.js
```

### Batch 6：Command Context / Keybinding / Menu Enablement 统一

状态：done

目标：

- 统一 readonly、selection、resource kind、dirty、AI write capability、source kind 等上下文。
- browser menu、快捷键、tree command enablement 消费同一 context，而不是多处硬编码。

Sidex 对照：

- Sidex 由 ContextKeyService + CommandsRegistry + KeybindingsRegistry 统一驱动。
- MNote 只做轻量实现，最终执行仍落到 Rust kernel / `tree.*` command。

待办：

- [x] 对齐 Rust `CommandContext` 和 browser `when` parser 的字段名。
- [x] 建立 `buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState)`。
- [x] 右键菜单、键盘动作、toolbar action 统一从 command context 判断 enable/disabled/reason。
- [x] 为危险动作补 destructive / requiresApproval 元数据。
- [x] 补 smoke：readonly、dirty、folder、asset、markdown、多选下的菜单与快捷键状态一致。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 `ContextKeyService` + `CommandsRegistry` + `KeybindingsRegistry` 的“同一 command id / 同一 context / 同一 when”模型；MNote 不照搬 VSCode DI、Extension Host、完整 menu contribution 或复杂 when 语法。
- [x] `filetree-context-menu-runtime.js` 增加 `buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState, deps)`，统一输出 `workspace.sourceKind`、`workspace.readonly`、`tree.focusKind`、`tree.selectionCount`、`tree.selectionResourceKind`、`tree.targetResourceKind`、`tree.targetIsFolder`、`tree.targetIsAsset`、`editor.dirty`、`editor.dirtyState`、`editor.hasSelection`、`ai.canWrite`。
- [x] `core-protocol::CommandContext` 对齐新增 target / dirtyState keys，并补单测锁住 Rust when evaluator 语义。
- [x] `sidebar-filetree-command-runtime.js` 菜单打开时从 open editors snapshot / bufferState / row selection 生成统一 command context，并把 context、`when`、`data-disabled-reason`、`data-destructive`、`data-requires-approval` 暴露到菜单 DOM。
- [x] `filetree-keyboard-runtime.js` 与 `sidebar-tree-runtime.js` inline fallback 的 F2 / Delete / Ctrl+V 使用同一 `buildSidebarFileTreeContext` + when；dirty 目标和 readonly workspace 下不执行写动作。
- [x] `task495-filetree-copy-id-absolute-path-smoke.js` 增加 command context 定点断言：markdown dirty target 禁用 rename/delete 类写操作，readonly target 禁用写操作，同时保持 local_folder copy-id 返回绝对路径。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/filetree-context-menu-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js
node --check rust/crates/mnote-web/browser/filetree-keyboard-runtime.js
node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js
node --check scripts/task495-filetree-copy-id-absolute-path-smoke.js
rustfmt --edition 2021 --check rust/crates/core-protocol/src/command.rs rust/crates/mnote-web/src/ssr/pages/layout.rs
git diff --check -- rust/crates/mnote-web/browser/filetree-context-menu-runtime.js rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js rust/crates/mnote-web/browser/filetree-keyboard-runtime.js rust/crates/mnote-web/browser/sidebar-tree-runtime.js rust/crates/core-protocol/src/command.rs rust/crates/mnote-web/src/ssr/pages/layout.rs scripts/task495-filetree-copy-id-absolute-path-smoke.js
cargo test --manifest-path rust/Cargo.toml -p core-protocol command_context -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_command_context_helper_functions_exist -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu_runtime_contains_command_context_helpers -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_context_menu_items_have_when_for_readonly -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_context_evaluator_uses_readonly_and_selection_count_for_delete_key -- --test-threads=1
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task495-filetree-copy-id-absolute-path-smoke.js
```

边界记录：

- 本批只统一 enablement / reason / metadata / keyboard gating，不改变最终执行面；写动作仍落到现有 Rust `tree.*` / `tree.resource.*` command 路径。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree -- --test-threads=1` 宽过滤额外暴露两个历史/相邻断言失败：`sidebar_filetree_blocks_readonly_paste_and_drop_with_action_status` 仍期待旧 `ensureFileTreeWritableTarget('paste'` 字符串，`sidebar_filetree_runtime_does_not_keep_retired_table_engine_branches` 命中 legacy table 分支；两者未进入本批定点验收，后续如要清理应单独归入 tree/filetree runtime 测试债。
- `node scripts/task476-filetree-editor-context-menu-download-smoke.js` 在菜单验证前等待 `report-a.pdf` asset row 超时；`node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js` 在 restore 后等待 `docs/report.txt` row 超时。两者失败点早于 Batch 6 command context enablement，记录为 FileTree projection / restore 可见性后续风险，不作为本批 command context 验收入口。

建议验证：

```bash
node scripts/task490-runtime-surfaces-smoke.js
node scripts/task476-filetree-editor-context-menu-download-smoke.js
node scripts/task473-local-folder-trash-restore-no-refresh-focus-gap-smoke.js
```

### Batch 7：`tree.*` / `tree.resource.*` compat 瘦身

状态：done

目标：

- `tree.*` / `tree.resource.*` 成为正式命令面。
- `documents.*` 只保留为显式 compat alias，不继续承载长期语义。

Sidex 对照：

- Sidex Explorer file actions 统一进入 file operation / bulk edit。
- MNote 的统一入口应是 Rust kernel tree/resource command，不是直接文件 bulk edit。

待办：

- [x] 盘点 `routes/tree.rs` 中 `documents.*` legacy mapping。
- [x] 标注每个 compat mapping 的 source kind、保留原因和退役条件。
- [x] local-first 路径禁止静默走 legacy cloud command。
- [x] resource trash / restore / purge 统一走 `tree.resource.*`。
- [x] 补 smoke：create/rename/move/delete/restore/purge 在 local_folder 下不触发 legacy owner。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 file action 明确区分 `moveFileToTrash` / `deleteFile` 的语义分叉和回收站 fallback；MNote 保持 `archive/restore/purge` 三段语义，不照搬 `ResourceFileEdit` 或 file system-only undo 模型。
- [x] `routes/tree.rs` 在 `/api/tree/commands` 中对缺失 `sourceKind` 但 `rootUri` 为 `file://` 的请求推断为 `local_folder`，避免 local-first 请求静默走 legacy cloud executor。
- [x] `routes/tree.rs` 增加 `TREE_DOCUMENT_COMPAT_ALIAS_CATALOG`，把 `documents.*` / `tree.*` 兼容边界、保留原因与退役条件显式化。
- [x] local_folder 资源 archive / restore / purge smoke 断言 canonicalCommand 分别为 `tree.resource.archive` / `tree.resource.restore` / `tree.resource.purge`。
- [x] 本轮不改 `bridge-runtime` 旧 compat executor 的退役状态；`documents.*` 相关 legacy 兼容桥测试属于已退役 cloud 验证面，失败原因是旧 Convex compat bridge 已退役，不作为 local-first Batch 7 主验收入口。

本轮验证证据：

```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_create_rename_copy_trash_restore_and_purge_use_same_endpoint -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder_asset_trash_restore_and_purge_use_trash_index -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_documents_compat_alias_catalog_marks_cloud_retirement_boundary -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_local_folder -- --test-threads=1
```

边界记录：

- `cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_lifecycle_aliases_are_marked_as_deprecated_tree_protocol_aliases -- --test-threads=1` 与 `tree_lifecycle_command_aliases_keep_tree_command_names` 当前失败，错误为“旧 Convex 兼容桥已退役；请改用 local-first Rust/SQLite control-plane 路径”。这说明旧 bridge-runtime 兼容桥已不再是 Batch 7 的主线验收目标，不应回拉为当前实现依赖。
- local-first 请求的 `sourceKind` 推断只在 `rootUri` 为 `file://` 时触发，不会把非本地请求静默改成本地。

建议验证：

```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --test-threads=1
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task471-local-folder-bulk-resource-trash-smoke.js
```

### Batch 8：AI target / changed_files / diff 审计

状态：done

目标：

- local-first AI 默认走授权文件引用 + agent 原生 patch/diff。
- MNote 负责 target resolver、allowed roots/files、dirty conflict、changed_files/diff 审计和前台同步。

Sidex 对照：

- VSCode agent/workspace 模式的价值是目标、权限、working copy、diff 和审计统一。
- MNote 不继续扩普通 Markdown 的专用 `mnote.doc.markdown_edit` 主路径。

待办：

- [x] target chip / picker 使用 open editors snapshot。
- [x] run 开始后冻结 target 摘要，避免执行中 DOM 焦点变化改目标。
- [x] changed_files 回收并展示：路径、版本、diff 摘要、actor。
- [x] dirty buffer 下 agent 写入必须阻断或进入冲突合并。
- [x] 补 smoke：clean 写入同步、dirty 写入阻断、跨 workspace target 不串、changed_files 可见。

本轮最小实现切片：

- [x] Sidex 对照：借鉴 VSCode/Sidex 的 working copy / SCM / diff 审计分层，把 target、权限、dirty、diff 和 changed files 放在统一 run/audit 边界；MNote 不照搬 SCM provider、QuickDiff provider 或完整 VSCode agent workspace 模型。
- [x] `sidebar-page-ai-runtime.js` 的 Page AI target resolver 继续从 `OpenEditorsSnapshot` 生成 `mnote.ai_editor_target.v1`；发 run 前新增 workspace/root/source guard，阻断跨 workspace 或陈旧 target。
- [x] `sidebar-page-ai-runtime.js` 新增 `mnote.page_ai_run_target_snapshot.v1`，在 run 发起时冻结 `editorTarget/openEditorsSnapshot/contextScope/rootUri/workspaceId/documentId`，并写入 `/api/hermes/client/runs` payload 与 `pageContext.aiContext.runTargetSnapshot`。
- [x] `hermes_client.rs` 对 local source 的 `runTargetSnapshot` 做白名单 sanitization，只保留 target/workspace/open editor 摘要，不把 `pageText/pageXml/contextBlocks` 等正文塞进本地 agent instructions。
- [x] `agent.changed_files` 工具卡显示路径、版本/hash/mtime 摘要、diff 摘要和 actor 信息；前台仍只消费 local agent before/after snapshot，不新增第二套 diff 真相。
- [x] `task453-local-folder-page-ai-changed-files-smoke.js` 覆盖 clean 写入同步、dirty buffer 阻断、跨 workspace target 不发 run、changed_files 卡片可见、runTargetSnapshot payload 可见。

本轮验证证据：

```bash
node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js
node --check scripts/task453-local-folder-page-ai-changed-files-smoke.js
rustfmt --edition 2021 --check rust/crates/mnote-web/src/routes/hermes_client.rs
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_body_local_source_uses_file_scope_not_full_page_context -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_agent_audit_snapshot_detects_changed_files -- --test-threads=1
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
git diff --check
codegraph sync .
```

边界记录：

- `node scripts/task-hermes-page-ai-audit-smoke.js` 与 `node scripts/task488-acp-multi-session-stability-smoke.js` 当前通过旧 helper 调 `/api/tree/commands` 创建 Convex 文档，失败为 `503 convex_retired`；两者属于 legacy/cloud 验证入口，需要后续改造为 local-first fixture 后再纳入默认 Batch 8 回归。
- Batch 8 不把普通 Markdown 编辑主路径扩回 `mnote.doc.markdown_edit`；local-first 默认仍是授权文件引用 + agent 原生 patch/diff + watcher / Page Aggregate 同步。
- `codegraph sync .` 与 `codegraph index . --force` 均已成功执行；`codegraph status .` 仍持续显示 `Pending Changes: Added 3 files`，属于本轮前后均存在的 CodeGraph pending 残留，已记录但不清理用户工作区。

建议验证：

```bash
node scripts/task453-local-folder-page-ai-changed-files-smoke.js
node scripts/task-hermes-page-ai-audit-smoke.js
node scripts/task488-acp-multi-session-stability-smoke.js
```

### Batch 9：是否扩展 IDE 面的产品决策

状态：done

目标：

- 明确 MNote 是否需要 Sidex 式 terminal / git / debug / tasks / extension host。
- 若需要，单独写 `design/10-review/reference` 或对应 owner 的 process checklist；不混进当前 P0 服务层收口。

待办：

- [x] 列出 MNote 用户工作流里真实需要 terminal/git/tasks 的场景。
- [x] 区分“知识库 workspace 内嵌工具”与“完整 IDE”。
- [x] 给出引入、不引入、延后引入三种方案。
- [x] 由用户确认产品边界后再进入实现。

本轮产品边界结论：

- [x] Sidex 对照：可借鉴 Sidex terminal 的 PTY/session/event-stream 模型、git porcelain status/diff/log 解析、tasks 的 detect/parse/run/output-stream 模型，以及 extension host 的贡献点/隔离 runtime 概念。
- [x] 不引入完整 VSCode/Sidex workbench、DAP debug、VSCode extension host 或通用 Git 写操作；MNote 继续以 Rust kernel / tree-first projection 作为 workspace 真相。
- [x] Terminal 若后续引入，只作为 workspace-root-scoped、allowed-roots 约束下的受控 terminal session，不默认开放全局 shell。
- [x] Git 若后续引入，优先只做 changed_files / diff / log / status 审计；commit / push / pull / restore / clean / checkout 等 destructive 操作不进 MVP。
- [x] Tasks 若后续引入，只支持白名单知识库任务：导出、索引、链接检查、附件整理、smoke；不做完整 `.vscode/tasks.json` 兼容。
- [x] Debug 能力收口为 agent runtime / projection / session log inspector；不引入 DAP、breakpoints 或 debug adapter manager。
- [x] Extension host 延后，仅保留 importer / exporter / renderer / action provider 的 MNote-native 插件设想，不接 VSCode marketplace 生态。

三方案：

- 引入：只引入 MNote-native `Workspace Command Center`，包含受控 terminal session、只读 git status/diff、白名单 tasks、流式输出、changed_files/diff 审计；不引入 VSCode extension host，不暴露 destructive Git。
- 不引入：继续让 Hermes/Reasonix 用自身文件与 shell 能力，MNote 只做 target / allowed roots / audit / watcher；这是当前 local-first AI 主线成本最低的方案。
- 延后：先把 terminal/git/tasks 作为 Phase B/C 设计冻结项，只落 checklist，不实现；等 changed_files/diff 审计、BufferStore 冲突、workspace readonly 策略稳定后，再开最小 MVP。

当前建议：

- 选择“延后 + 小范围引入只读 Git/diff 审计”的产品边界；Terminal/tasks 等 agent runtime 审计闭环稳定后再单独立项，Debug/DAP/VSCode extension host 不进入 MVP。

## 5. 当前运行状态

历史运行记录：Batch 1 轻量 Workbench Editor Model。

第一步只做只读核验和现有状态盘点，不改代码：

- [x] 读取 `document-resource-tab-runtime.js` 的 open editors snapshot 构建逻辑。
- [x] 读取 `document-editor-adapter-runtime.js` 的 page / secondary pane 打开入口。
- [x] 读取 `resource-open-runtime.js` 与 FileTree open runtime。
- [x] 输出 Batch 1 的具体文件边界与最小 smoke 切片。

只读核验结论（2026-05-27）：

- `document-resource-tab-runtime.js` 已有 `resourceTabRegistry`、`resourceTabMru`、`buildOpenEditorsSnapshot()` 和 `window.__mnoteOpenEditorsSnapshot`。
- 当前 snapshot 已包含 `schema/generatedAt/activeObjectIdentity/editors/resourceEditors`，但没有显式 `groups`，调用方需要自己按 `paneRole` 过滤。
- Page entry 已按 `primary/secondary` 生成；secondary entry 仅在有 documentId 或 pane visible 时进入 snapshot。
- Resource entry 已有 `objectIdentity/title/kind/badgeKind/active/dirtyGuard/assetId/path`，但缺少 `paneRole/documentId/workspaceId/lastActiveAt/preview/pinned` 等轻量 editor group 字段。
- `document-editor-adapter-runtime.js` 的公开入口集中在 `window.__mnoteDocumentPaneRuntime`，包括 `openPrimaryDocument`、`openSecondaryDocument`、`openResourceInActiveTab`、`openResourceAsSideTarget`。
- `sidebar-filetree-open-runtime.js` 仍在多处手拼 `resource:file:<rootUri>:<relativePath>`、`resource:mindmap:<documentId>:<assetId>`、`resource:onlyoffice:<documentId>:<assetId>` 等 identity；这应进入 Batch 2，而不是阻塞 Batch 1。

Batch 1 最小文件边界：

- 修改：`rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
- 可选测试：复用或扩展 `scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js`
- 暂不修改：`document-editor-adapter-runtime.js`、`sidebar-filetree-open-runtime.js`、`resource-open-runtime.js`

Batch 1 最小实现切片：

- [x] `buildOpenEditorsSnapshot()` 增加 `groups` 字段，按 `primary/secondary` 输出每个 pane 的 active object 与 editors。
- [x] `openEditorsSnapshotEntry()` 补 `paneRole`、`documentId`、`workspaceId`、`lastActiveAt`、`preview:false`、`pinned:false`。
- [x] Page entry 补 `preview:false`、`pinned:true`、`lastActiveAt` 占位，保持页面 tab 是固定 tab。
- [x] 更新 smoke 断言：`window.__mnoteOpenEditorsSnapshot.groups.primary` 存在，secondary 打开后 `groups.secondary` 存在。

## 6. 最小成功标准

本 checklist 完成到 P0 时，MNote 至少应满足：

- 打开页面 / 资源 / secondary pane 的状态有统一 snapshot，可恢复、可被 AI target resolver 消费。
- local-first Markdown dirty/stale/conflict 只由 BufferStore / Page Aggregate 主链表达。
- FileTree row identity、resource tab identity、AI target identity 不再多处临时拼接。
- tree live / local-folder watcher 不再常规整树替换，view state 不丢。
- 菜单、快捷键、dangerous action 的 enabled/disabled/reason 有统一 context。
- local-first tree/resource command 不静默退回 legacy `documents.*` 主链。

## 7. 验证总入口

按改动范围选择运行：

```bash
node scripts/task114-rust-web-gateway-entry-smoke.js
node scripts/task159-auth-entry-smoke.js
node scripts/task164-desktop-hot-local-folder-main-entry-smoke.js
node scripts/task166-local-first-managed-workspace-no-convex-smoke.js
node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
node scripts/task490-runtime-surfaces-smoke.js
cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib -- --test-threads=1
git diff --check
codegraph sync .
codegraph status .
```
