# 3-18 [done] Local Folder Tree Live Consumer 收敛 checklist v1

> 创建时间：2026-05-21
>
> 当前状态：`DONE`
>
> 上位入口：
> - `design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
> - `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
>
> 目标：把 local-first 默认路径的 Sidebar / FileTree 从 `local-folder-watch` 轮询补拉 projection，收敛到与 cloud / sync path 相同的 tree live consumer 事件口径；完成后再判断 `3-3` 是否可归档。

## 1. 当前事实

- `3000` cloud / compat 工作区已通过 `TREE_LIVE_CONTROLLER_JS` 优先连接 `/api/realtime/ws`，失败后回退 `/api/tree/events`。
- `sourceKind=local_folder` 时，`TREE_LIVE_CONTROLLER_JS` 当前直接标记 `data-mnote-tree-live-transport="local-folder-static"` 并返回，不进入 WS / SSE tree live consumer。
- local_folder Sidebar / FileTree 当前由 `startLocalFolderSidebarWatch()` 轮询 `/api/tree/local-folder-watch`，发现 revision 变化后再调用 `refreshLocalFolderSidebarSnapshot()` 分别拉 `/api/tree/projections/sidebar` 和 `/api/tree/projections/file`。
- `web_shell.rs` 已有 `/api/local-folder/events` SSE，用于 tiptap 文档外部变更刷新，但它还没有统一到 Sidebar / FileTree 的 `tree:snapshot` / `tree:delta` / `tree:resync` consumer。

## 2. 不做事项

- 不恢复 Convex 正文主存储。
- 不把 local_folder 变化写回 Convex command log。
- 不重开旧 Next / BlockNote 默认路径。
- 不一次性重写所有 Sidebar/FileTree 逻辑；本轮只替换轮询补拉链路的 transport 和 consumer 入口。

## 3. Worker 拆分

### Worker A：后端 local_folder tree live stream

Owner：

- `rust/crates/mnote-web/src/routes/local_folder_events.rs`
- `rust/crates/mnote-web/src/routes/stream_support.rs`
- `rust/crates/mnote-web/src/routes/mod.rs`
- 必要时只读 `rust/crates/mnote-web/src/local_folder_watcher_registry.rs`

目标：

- 给 local_folder 提供 tree live 事件形状：`tree:snapshot` / `tree:resync` 至少携带 `kernel_sidebar_projection` 与 `kernel_file_tree_projection`。
- 复用现有 LocalFolderWatcherRegistry，不新增第二套 watcher。
- 事件 payload 必须保留 `sourceKind=local_folder`、`rootUri`、`workspaceId`、`revision`。
- 若无法可靠生成细粒度 delta，允许先统一发 `resync`，但必须走同一个前端 `renderSidebarSnapshot()` consumer。

验收：

- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_events -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_support -- --test-threads=1`

### Worker B：前端 tree live controller 接入 local_folder

Owner：

- `rust/crates/mnote-web/src/ssr/pages/layout.rs`

目标：

- 移除 `sourceKind=local_folder` 直接 `local-folder-static` 返回的长期阻塞。
- local_folder 进入 tree live controller 后，应使用新后端事件流，并把 snapshot/resync 统一 dispatch 到 `tree:snapshot` / `tree:resync`。
- 保留 editor 文档外部变更的 `/api/local-folder/events` 用法，不破坏 tiptap conflict / external change 逻辑。
- 尽量让 `startLocalFolderSidebarWatch()` 退为 fallback，不再是默认主链。

验收：

- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web web_shell -- --test-threads=1`

### Worker C：浏览器 smoke 与证据

Owner：

- `scripts/task435-local-folder-watch-no-reload-smoke.js`
- 可新增 `scripts/task487-local-folder-tree-live-consumer-smoke.js`
- `design/03-rust-web/process/3-18-local-folder-tree-live-consumer-convergence-checklist-v1.md`

目标：

- 增加或更新 smoke，验证 local_folder 文档页 HTML / DOM 不再显示 `data-mnote-tree-live-transport="local-folder-static"` 作为默认主链。
- 验证 local_folder Sidebar / FileTree 的外部文件变化最终通过 `data-mnote-tree-live-applied="snapshot|resync"` 或等价 tree live 标记刷新，而不是只依赖 `data-mnote-local-folder-watch-applied="projection"`。
- smoke 必须可在 `http://127.0.0.1:3000` 环境运行，并输出 JSON 证据和截图路径。

验收：

- `node --check scripts/task435-local-folder-watch-no-reload-smoke.js`
- 若新增脚本：`node --check scripts/task487-local-folder-tree-live-consumer-smoke.js`

## 4. Codex 复核项

- [x] 复核 Worker A diff，确认没有新增第二套 watcher 或 Convex 写回。
- [x] 复核 Worker B diff，确认 local_folder 进入统一 tree live consumer，旧 polling 只保留 fallback。
- [x] 复核 Worker C smoke，确认断言覆盖真实 UI/DOM 状态。
- [x] 运行 targeted Rust tests。
- [x] 运行相关 smoke 或记录环境阻塞。
- [x] 更新 `3-3` 与 `1-8` 状态。

## 5. 本轮执行记录

- 2026-05-21：Codex 复核 `3-3` 发现 local_folder 默认路径仍是 `local-folder-static` + `/api/tree/local-folder-watch` 轮询补拉 projection，因此 `3-3` 暂不能归档，拆出本 checklist。
- 2026-05-21：**Worker C（本批次）**：
  - 新增 `scripts/task487-local-folder-tree-live-consumer-smoke.js`
  - 未修改 `scripts/task435-local-folder-watch-no-reload-smoke.js`，该脚本保持 GREEN（覆盖 no-reload 语义，与 tree live consumer 正交）
  - 先前使用旧进程和首页入口时 smoke 呈 RED；切换到新编译的 3000 进程并改为 `documents/local-md:README.md?sourceKind=local_folder...` 后，`task487` 变为 GREEN：`data-mnote-tree-live-transport=local-folder-events`、`data-mnote-tree-live-status=connected`、外部创建/删除后 `data-mnote-tree-live-applied=resync`
  - 本记录

### Worker C smoke 覆盖点

| # | 断言 | 当前状态 | 期望状态（Workers A+B 后） |
|---|------|----------|---------------------------|
| 1 | page tree 加载后 `data-mnote-tree-live-transport` 不是 `"local-folder-static"` | ✅ GREEN — 当前为 `"local-folder-events"` | ✅ 已满足 |
| 2 | page tree 加载后 `data-mnote-tree-live-status` 不是 `"static"` | ✅ GREEN — 当前为 `"connected"` | ✅ 已满足 |
| 3 | 外部创建 `.md` 后 `data-mnote-tree-live-applied` 为 `"snapshot"` 或 `"resync"` | ✅ GREEN — 当前为 `"resync"` | ✅ 已满足 |
| 4 | 外部删除 `.md` 后 `data-mnote-tree-live-applied` 为 `"snapshot"` 或 `"resync"` | ✅ GREEN — 当前为 `"resync"` | ✅ 已满足 |
| 5 | FileTree 视图加载后 transport/status 非 `"local-folder-static"` / `"static"` | ✅ GREEN — 当前为 `"local-folder-events"` / `"connected"` | ✅ 已满足 |
| 6 | FileTree 外部创建后 tree-live-applied 为 snapshot/resync | ✅ GREEN — 当前为 `"resync"` | ✅ 已满足 |
| 7 | `data-mnote-local-folder-watch-applied` 不再是唯一刷新证据 | ✅ GREEN — 当前同时存在 `data-mnote-tree-live-applied` | ✅ 已满足 |
