# 3-19 Rust Web 浏览器 Runtime 模块外置化重构 v1

> 创建时间：2026-05-24
> 状态：`done`
>
> 触发背景：
> - CodeGraph 对 `mnote-web` 前端内嵌 JS 的符号定位效果一般。
> - 字面搜索命中太多，尤其是 `layout.rs` / `tree.rs` 内的大型 raw string。
> - 需要判断是更换 graph 工具，还是调整源码组织方式。
>
> 参考实现：
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/views/explorerView.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/explorerService.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts`

## 1. 结论

当前不应优先更换 CodeGraph。主要问题不是 graph 项目选型，而是 `mnote-web` 的大量浏览器 runtime 仍作为 Rust raw string / HTML inline script 存在。对 CodeGraph、IDE、ast-grep、浏览器 stack trace 来说，这些逻辑首先是 Rust 字符串，不是一等 JavaScript / TypeScript 源文件。

短期可以做“内嵌 JS 提取索引”作为定位辅助，但它只解决查询体验，不改变维护边界。长期重构方向应是：保留 Rust SSR / projection / reducer 的语义主导权，把浏览器端 DOM 绑定、host bridge、transport controller、DND、context menu、resource open adapter 等 runtime 分阶段迁出 Rust raw string，变成可索引、可测试、可复用的 `.js` 或 `.ts` 模块。

补充结论（2026-05-24）：最近 `bugs/0524.md` 的修复耗时说明，低效并不只发生在 `layout.rs` 的 tree/filetree runtime，也发生在 `web_shell.rs` 的文档页 host adapter 和 `leptos-tiptap-spike/src/lib.rs` 的编辑器 runtime 巨型文件。本设计只覆盖 `03-rust-web` 的浏览器 shell runtime 外置化；`leptos-tiptap` 编辑器命令、附件、history、DOM bridge 需要并行进入 `05-editor-mainline` 设计，不应塞进本设计里一起实施。

## 2. 当前代码基线

### 2.1 主壳内嵌 runtime

- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
  - `SIDEBAR_TREE_JS` 从文件顶部开始，是当前主壳最大浏览器 runtime。
  - 职责混合了：侧栏宽度、工作区切换、page tree / file tree 事件、文件树选择、拖拽、资源打开、附件上传、Page AI panel、账号菜单、本地文件夹 watcher。
  - `TREE_LIVE_CONTROLLER_JS` 负责 tree live bootstrap、WS / SSE / local-folder event transport、snapshot / delta / resync 分发。
  - `PageLayout` 通过 `<script inner_html={...}>` 直接注入这些 runtime。

### 2.2 Debug tree shell 内嵌 runtime

- `rust/crates/mnote-web/src/routes/tree.rs`
  - `build_tree_shell_html()` 内嵌整页 HTML、CSS、JS。
  - JS 负责 page/filetree/picker 模式、DOM render、keyboard、DND、context menu、runtime reduce endpoint 调用、local fallback。
  - `/api/tree/runtime/reduce` 已存在，入口为 `reduce_tree_shell_runtime()`。

### 2.3 已经存在的 Rust 侧良好边界

- `rust/crates/mnote-web/src/tree_shell/runtime_api.rs`
  - 已定义 `TreeShellRuntimeRequest`、`TreeShellRuntimeResult`、`TreeShellDomPatch`、`TreeShellHostEvent`、`TreeShellCommandEvent`。
- `rust/crates/mnote-web/src/tree_shell/filetree_selection.rs`
  - 已定义文件树选择 reducer contract。
- `rust/crates/mnote-web/src/tree_shell/filetree_renderer.rs` / `page_renderer.rs`
  - 已有初始 HTML renderer 与测试。

这说明重构不需要推翻 Rust 主线。相反，应该把现有 Rust reducer / renderer 合同作为稳定边界，让浏览器 runtime 外置后继续消费这些合同。

### 2.4 需要另线处理的编辑器 runtime 巨型文件

- `rust/spikes/leptos-tiptap-spike/src/lib.rs`
  - 约一万行，虽然是 Rust 源码而不是 raw string，但混合了 Tiptap bridge、编辑器命令、附件链接增强、上传、history、mindmap node view、DOM overlay、page option、AI surface。
  - CodeGraph 能索引 Rust 符号，但单文件职责过大，worker 仍会反复字面搜索和误判边界。
  - 该文件不属于 `03-rust-web` shell runtime 外置化范围，应由 `05-editor-mainline` 拆出编辑器命令和附件/history 相关模块。

## 3. Sidex / VSCode Explorer 对照结论

Sidex 参考实现的关键价值不是具体 UI，而是模块边界：

- `ExplorerView`：视图容器，持有 tree、renderer、context key、focus、layout。
- `ExplorerService`：模型、文件变更、refresh、cut/copy 状态、view 注册。
- `FileDragAndDrop`：DND 独立类，包含 `onDragOver`、`drop`、`getDragURI`、`onDragStart`。
- `fileActions.contribution.ts`：命令和快捷键独立注册，通过 context condition 控制可用性。
- `IExplorerView` / `IExplorerService`：视图与服务之间通过接口通信。

MNote 不应照搬 VSCode 的完整 DI / Workbench 架构；当前产品主线也不是 BlockNote-first 或 VSCode clone。但应采用同一类边界原则：

- 视图绑定与 DOM patch 是浏览器 runtime 职责。
- 树语义、资源归属、projection、command 语义仍由 Rust kernel / mnote-web reducer 主导。
- DND、context menu、keyboard、transport、resource open adapter 不应继续混在单个全局 IIFE 中。

## 4. 目标

- 让主壳浏览器 runtime 成为一等源文件，能被 CodeGraph / IDE / ast-grep / lint 正常解析。
- 让 `layout.rs` 回到 SSR layout 与 bootstrap 注入职责，不再承载万行浏览器行为。
- 让 `/tree` debug shell 与主壳 sidebar 复用同一套 tree/filetree runtime 模块，减少两套行为漂移。
- 保留 Rust `tree_shell` reducer / renderer / contract 作为语义边界，不把树真相迁到前端。
- 改善浏览器调试：错误堆栈能落到稳定 JS 模块文件和行号。
- 为后续 tree live cache、filetree DND、resource tab、local folder watcher 的继续收口降低定位成本。

## 5. 非目标

- 不更换 CodeGraph。
- 不引入完整 React / Next / BlockNote 主链。
- 不把 Rust kernel / Rust projection 的语义迁回前端。
- 不一次性重写整个 sidebar。
- 不在本阶段改变现有 UI 形态、Wolai 对齐目标、tree command 协议或 resource object identity 合同。
- 不把 debug `/tree` 重新提升为默认产品入口；它仍是显式 debug/internal 边界。

## 6. 方案选择

### 6.1 方案 A：只做内嵌 JS 提取索引

做一个脚本从 Rust raw string 中提取 `<script>` / `*_JS` 常量为临时 `.js` 文件，并带回源文件行号映射。

优点：
- 改动小。
- 对定位问题见效快。
- 不影响 runtime。

缺点：
- 只是辅助索引，真实维护边界仍然是 Rust 字符串。
- 浏览器 stack trace、代码 review、模块复用、单测边界不会根本改善。
- 提取器会成为额外维护成本。

### 6.2 方案 B：直接整体重写 sidebar runtime

一次性把 `SIDEBAR_TREE_JS` 全部迁出并重写成新模块体系。

优点：
- 最终形态干净。
- 可一次性消除万行 inline script。

缺点：
- 风险过大。
- 当前 sidebar 混合了 tree、资源、AI、账号、本地文件夹、多 pane 等多个活跃主线，整体重写容易引入回归。
- smoke 覆盖面需要非常大，不适合作为第一阶段。

### 6.3 推荐方案 C：分阶段外置化，先提取边界清楚的 runtime

先从职责边界最清晰、状态输入最明确、验证最容易的模块开始迁出。每个阶段都保持旧行为，完成后跑对应 smoke，再进入下一阶段。

推荐顺序：

1. resource open / local upload / attachment action adapter
2. `TREE_LIVE_CONTROLLER_JS`
3. debug `/tree` shell runtime 的 bridge 层
4. 主壳 filetree selection / keyboard / DND bridge
5. Page AI panel 等低相关大块 runtime 另立后续设计，不混入本次 tree runtime 重构

推荐理由：
- 每步可独立验收。
- 不破坏当前 Rust reducer / projection 主线。
- 能最快验证 CodeGraph 定位改善。
- 便于回滚单一模块，而不是回滚整个 sidebar。
- 最近缺陷集中在上传目标、附件打开、secondary pane、资源 tab、OnlyOffice/PDF/code 打开路径。先拆 resource/local-upload adapter，比先拆 tree live 更能直接降低后续 bug 修复成本。
- `TREE_LIVE_CONTROLLER_JS` 仍适合作为低风险模板切片：边界清楚、smoke 现成、能验证 asset route 和 ES module 注入方式。

## 7. 目标架构

### 7.1 文件布局建议

新增浏览器 runtime 源文件目录：

```text
rust/crates/mnote-web/browser/
  resource-open-runtime.js
  local-upload-runtime.js
  tree-live-controller.js
  tree-shell-runtime.js
  sidebar-tree-runtime.js
  filetree-selection-runtime.js
  filetree-dnd-runtime.js
  filetree-context-menu-runtime.js
  local-folder-events-runtime.js
```

如后续决定引入 TypeScript，可迁移为：

```text
rust/crates/mnote-web/browser/src/
rust/crates/mnote-web/browser/dist/
```

第一阶段建议先用 plain ES module `.js`，避免为了重构引入额外 build chain。待模块边界稳定后，再评估是否升级 TypeScript。

### 7.2 Rust 注入边界

Rust 继续负责：

- SSR HTML layout。
- bootstrap JSON script，例如 `__MNOTE_TREE_LIVE_BOOTSTRAP__`。
- initial page tree / file tree HTML。
- `/api/tree/runtime/reduce`。
- `/api/tree/commands`。
- `/api/realtime/ws`、`/api/tree/events`、`/api/local-folder/events`。
- mnote-web 静态 runtime asset 路由。

Rust 不再负责：

- 在 raw string 中维护大块浏览器行为。
- 在 inline script 中拼出完整 DOM runtime。
- 在 `layout.rs` 中混合 sidebar、AI、resource、tree live 的所有事件处理。
- 在 `web_shell.rs` 中继续堆叠资源打开和 pane host adapter 的长期浏览器逻辑；文档页 host adapter 应逐步迁到外置模块，但编辑器内部 Tiptap 命令仍归 `05-editor-mainline`。

### 7.3 浏览器模块职责

浏览器 runtime 负责：

- 读取 bootstrap JSON。
- 绑定 DOM event。
- 收集 DOM 当前可见状态，构造 reducer request。
- 调用 Rust runtime reduce endpoint。
- 应用 `domPatches`。
- 执行 `hostEvents`，例如 open page、open resource、context menu、upload files。
- 连接 tree live transport 并向页面分发 `tree:snapshot` / `tree:delta` / `tree:resync`。

浏览器 runtime 不负责：

- 决定页面/资源父子真相。
- 决定资源归属。
- 决定 tree command 语义。
- 维护第二套 file tree projection。

## 8. 合同草案

### 8.1 Runtime asset manifest

新增或复用类似 `leptos_tiptap_manifest()` 的 runtime manifest：

```json
{
  "schema": "mnote.browser_runtime_manifest.v1",
  "assets": {
    "treeLiveController": "/api/mnote-browser-runtime/tree-live-controller.js",
    "sidebarTreeRuntime": "/api/mnote-browser-runtime/sidebar-tree-runtime.js",
    "treeShellRuntime": "/api/mnote-browser-runtime/tree-shell-runtime.js"
  }
}
```

### 8.2 Tree live bootstrap

沿用当前 `mnote.tree_live_bootstrap.v1`，但由外置模块读取：

```json
{
  "schema": "mnote.tree_live_bootstrap.v1",
  "transport": "convex-command-log-ws",
  "endpoint": "/api/tree/events",
  "wsEndpoint": "/api/realtime/ws",
  "views": ["page-tree", "file-tree"]
}
```

### 8.3 Host bridge

主壳提供最小 host bridge：

```js
window.__mnoteTreeHost = {
  openPage(detail) {},
  openResource(detail) {},
  dispatchCommand(command) {},
  applyTreeSnapshot(payload) {},
  applyTreeDelta(payload) {},
  reportRuntimeError(error, context) {}
};
```

该 bridge 只是浏览器适配层，不是树真相。

### 8.4 Worker 任务切片合同

外置模块后，ClaudeCode / Reasonix 任务书必须优先指向单一模块和单一 smoke，不再让 worker 直接搜索整个 `layout.rs` / `web_shell.rs`：

```text
允许读取：
- rust/crates/mnote-web/browser/resource-open-runtime.js
- rust/crates/mnote-web/src/ssr/pages/layout.rs 中 module bootstrap 小段
- scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js

禁止：
- 重写 sidebar 全局 IIFE
- 修改 Rust tree command 语义
- 修改不相关 Page AI panel
```

采纳 worker diff 的最低条件：

- diff 落在指定模块和测试内。
- handoff 说明调用链、未覆盖边界和验证命令。
- 主控能用 CodeGraph / `rg` 直接定位被修改符号。
- 浏览器 smoke 或截图证据可复核。

## 9. 执行策略

本设计按 Codex 主控、Reasonix 短跑 worker 协同执行：

- Codex 负责设计口径、模块边界、任务拆分、最终 diff 复核、测试和提交。
- Reasonix 只承接窄任务，每个 run 必须有任务书、允许文件、禁止事项、输出表格和 handoff。
- 同时最多启动 2 个 Reasonix worker；每批结束后 Codex 先读取 `process-handoff.md/json` 和 Hindsight `reasonix` recall，再决定是否采纳。
- 每个 worker 完成或超时后都应结束该 run，不在同一个长会话里连续追加不相关任务。
- Batch 0 只做只读审计，不修改文件、不跑 git 写操作，用来生成热点归属表和风险清单。
- Batch 1 起才允许实施单模块搬迁；每个实施 worker 只能修改一个 runtime 模块和对应 smoke。

当前建议批次：

- Batch 0 / R1：只读审计 `resource open / local upload / attachment action adapter`，输出函数归属表、目标模块、风险和 smoke 建议。
- Batch 0 / R2：只读审计 `leptos-tiptap` history / `set_content` 风险，结果写回 `05-editor-mainline` 设计，不混入本设计实现。
- Batch 1：外置 `resource-open-runtime.js` 的最小可验证切片，优先迁出纯 URL builder 和 layout bridge，不一次迁走 `web_shell.rs` 的完整 resource tab/session 生命周期。
- Batch 2：外置 `TREE_LIVE_CONTROLLER_JS`。
- Batch 3：按审计结果选择 debug `/tree` bridge 或主壳 filetree runtime。

Batch 0 验收标准：

- R1 能列出 `layout.rs` / `web_shell.rs` 中资源打开、上传、附件动作相关入口和调用位置。
- R1 能给出“先拆哪个模块”的具体建议，并绑定现有 smoke。
- R2 的 editor 侧结论必须只作为跨设计参考；不得要求 3-19 修改 Tiptap 内部命令。
- Codex 必须把可采纳结论整合进本设计或 `5-28`，未采纳项记录原因。

### 9.1 Batch 0 执行记录（2026-05-24）

Reasonix R1 run：

- run id: `reasonix-2026-05-24T12-38-22-672Z-6ffcfa23`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-38-22-672Z-6ffcfa23/process-handoff.md`
- 类型：只读审计，无工作区改动。
- Codex 复核：已抽样用 `rg` 核对关键符号位置；Hindsight `reasonix` bank 可 recall 到该 handoff。

可采纳结论：

- `resource-open-runtime.js` 是 Batch 1 更合适的首个实施目标，但第一刀应只迁 `layout.rs` 中资源打开的纯函数和 bridge，例如：
  - `buildOnlyOfficeOpenPath`
  - `buildLocalFileOpenUrl`
  - `openLocalOfficeFileInActiveTab`
  - `openLocalResourceInActiveTab`
  - `openConvexAssetFromFileTree`
  - `openEditorAttachmentDetail`
  - `openEditorAttachmentEditTab`
- `web_shell.rs` 中 `openResourceInActiveTab`、`createResourceTabDom`、`openTiptapResourceTab`、`replacePaneDocument`、`createEditorViewBinding` 等实际属于 resource tab / document pane host 生命周期，风险高，不应塞进 Batch 1 的同一个 worker。
- `local-upload-runtime.js` 是第二个高价值目标；真实入口是 `uploadFileToMediaAsset`、`insertUploadedAssetIntoEditor` 和 debug shell 的 `executeLocalFileTreeExternalDrop`，不是 `uploadFilesToLocalFolderTarget`。
- `TREE_LIVE_CONTROLLER_JS` 仍适合作为 Batch 2：边界清楚，现有 smoke 对 WS/SSE fallback 覆盖更明确。

本轮发现的 worker 协同问题：

- Reasonix 能产出有用的只读审计表，但 runner 在产出 `completed` 结果后底层 `reasonix acp` 进程没有自动退出；主控必须显式检查并清理本轮启动的 worker 进程。
- Hindsight recall 可找到 handoff，但召回结果会混入同日其它 Reasonix 经验；主控仍必须读取本次 `process-handoff.md/json` 原文。

### 9.2 Batch 1 执行记录（2026-05-24）

Reasonix implementation run：

- run id: `reasonix-2026-05-24T12-49-55-633Z-be2844cb`
- worktree: `/mnt/Data1T/mnote-wt-3-19-resource-open-b1`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-49-55-633Z-be2844cb/process-handoff.md`
- Codex 采纳方式：从隔离 worktree 选择性合入主工作区；未提交 worker branch。

已落地的最小切片：

- 新增 `rust/crates/mnote-web/browser/resource-open-runtime.js`。
- 新增固定 asset route：`GET /api/mnote-browser-runtime/resource-open-runtime.js`。
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理，保留原实现 fallback：
  - `buildOnlyOfficeOpenPath`
  - `buildLocalFileOpenUrl`
  - `localFilePathFromAssetId`

采纳边界：

- 本批次没有迁移 `openConvexAssetFromFileTree`、`openEditorAttachmentDetail`、`openEditorAttachmentEditTab` 等复杂行为函数。
- 本批次没有迁移 `web_shell.rs` 的 `openResourceInActiveTab`、resource tab DOM、session、pane host 生命周期。
- 固定 route 使用 `include_str!` 暴露单个 JS 文件，不提供动态 path，因此本批次不需要引入通用路径校验。

验证：

```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell
cargo test -p mnote-web sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools
node --check rust/crates/mnote-web/browser/resource-open-runtime.js
```

全部通过。

### 9.3 Batch 2 执行记录（2026-05-24）

Reasonix implementation run：

- run id: `reasonix-2026-05-24T13-14-25-162Z-f98927dc`
- worktree: `/mnt/Data1T/mnote-wt-3-19-tree-live-b2`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T13-14-25-162Z-f98927dc/process-handoff.md`
- Codex 采纳方式：读取 Hindsight recall 和 handoff 后，从隔离 worktree 选择性合入主工作区；未提交 worker branch。

已落地的切片：

- 新增 `rust/crates/mnote-web/browser/tree-live-controller.js`。
- 新增固定 asset route：`GET /api/mnote-browser-runtime/tree-live-controller.js`。
- `PageLayout` 将原 inline `TREE_LIVE_CONTROLLER_JS` 替换为 `type="module"` 外置脚本。
- `layout.rs` 不再保留大块 `TREE_LIVE_CONTROLLER_JS` raw string；测试改为 `include_str!` 外置文件内容。

采纳边界：

- 本批次只做同构搬迁，不改 tree live 语义。
- WS 优先、SSE fallback、local-folder events、snapshot/delta/resync/block.delta 分发保持原逻辑。
- 后端 `/api/realtime/ws`、`/api/tree/events`、`/api/local-folder/events` 未修改。

验证：

```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web tree_live_controller_marks_transport_and_closes_source_on_pagehide
node --check rust/crates/mnote-web/browser/tree-live-controller.js
```

全部通过。

浏览器轻量验收：

- dev server: `npm run dev:hot`
- 页面：`http://127.0.0.1:3000/`，测试账号快速登录后进入主壳。
- `/api/mnote-browser-runtime/tree-live-controller.js` 返回 `200`，`content-type=application/javascript; charset=utf-8`。
- `document.scripts` 中存在 1 个 tree live module script。
- `window.__mnoteTreeLiveControllerStarted === true`。
- `<html>` 上出现 `data-mnote-tree-live-transport="disabled"` 和 `data-mnote-tree-live-status="disabled"`，证明外置 module 已执行。
- 控制台仍出现 `/api/local-folder/events` 500，这是既有 local folder event stream 问题，不属于本批次 route/module 加载回归。

CodeGraph 复核：

- 初始外置文件仍包在 IIFE 内，CodeGraph 只能识别外层匿名符号，不能定位 `startWithWebSocket`。
- Codex 将外置 module 调整为模块级函数声明，并把启动 guard 放到底部。
- 复核结果：
  - `codegraph_search startWithWebSocket` -> `rust/crates/mnote-web/browser/tree-live-controller.js`
  - `codegraph_search startWithSseFallback` -> `rust/crates/mnote-web/browser/tree-live-controller.js`

### 9.4 Batch 3 执行记录（2026-05-24）

Reasonix implementation run：

- run id: `reasonix-2026-05-24T14-22-14-401Z-ea414358`
- worktree: `/mnt/Data1T/mnote-wt-3-19-local-upload-b3`
- 结果：未采纳。该 run 卡在提交 plan 后没有产生工作区 diff，也没有写出 handoff；主控终止残留 runner / acp 进程并删除临时 worktree。

Codex 主控直接完成的最小切片：

- 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js`。
- 新增固定 asset route：`GET /api/mnote-browser-runtime/local-upload-runtime.js`。
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下上传/附件展示 helper 先做 runtime 代理，保留原实现 fallback：
  - `uploadedAssetTitle`
  - `uploadedAssetUrl`
  - `localAssetOpenUrl`
  - `uploadedAssetType`
  - `fileTreeIconKindForFileName`
  - `isLocalUploadedAsset`
  - `uploadedAssetExtension`
  - `isNonOfficeAttachmentName`
  - `attachmentExtensionFromFileName`
  - `isPdfAttachmentFileName`
  - `isCodeAttachmentFileName`
  - `inferCodeAttachmentLanguage`
  - `attachmentClassForFileName`
  - `uploadedAttachmentClass`
  - `uploadedFileSize`

采纳边界：

- 本批次不迁移 `uploadFileToMediaAsset`、`insertUploadedAssetIntoEditor`、`uploadFilesWithResolvedTarget` 主流程。
- 本批次不修改 `/api/local-folder/assets/upload` 请求字段、secondary pane 目标选择、resource tab、OnlyOffice、PDF/code 打开逻辑。

验证：

```bash
node --check rust/crates/mnote-web/browser/local-upload-runtime.js
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```

结果：

- `local-upload-runtime.js` route 返回 `200`，`content-type=application/javascript; charset=utf-8`。
- `task479` 最终 `ok=true`，覆盖主编辑区上传目录、filetree folder drop、broken link 保留、无 active document 附件 tab、文件树滚动、标题来源、单附件删除保留相邻附件。

### 9.5 Batch 4 执行记录（2026-05-24）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T15-27-18-395Z-12afda31`
- worker worktree: `/mnt/Data1T/mnote-wt-3-19-local-upload-context-b4`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-27-18-395Z-12afda31/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。

采纳内容：

- `local-upload-runtime.js` 新增 `resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`。
- `SIDEBAR_TREE_JS` 中保留同名 wrapper 和 inline fallback；优先委托 `window.__mnoteLocalUploadRuntime`，模块加载失败时保留旧行为。
- 本批不迁 `uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor`。这些函数仍混合 API POST、file tree refresh、Tiptap chain 插入和 OnlyOffice/resource tab 逻辑，后续需要继续拆边界。
- `scripts/task472-side-target-secondary-pane-smoke.js` 补齐 `/api/local-folder/assets/upload` 当前必需的 `uploadIntent=editor.markdown.attach`，让 secondary pane smoke 恢复可运行。

已通过验证：

- `node --check rust/crates/mnote-web/browser/local-upload-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`
  - 证据：命令 stdout JSON，覆盖 `editor-upload-to-page-resource-dir`、`filetree-folder-drop-to-target`、`broken-link-after-real-file-delete`、`direct-attach-open-without-active-md`、`editor-delete-one-attachment-preserves-adjacent` 等检查。
- `node scripts/task472-side-target-secondary-pane-smoke.js`
  - 结果：`ok=true`
  - 证据：命令 stdout JSON，`assetId=local:asset:README/side-target-resource.md`、`officeAssetId=local:asset:README/side-target-office.docx`。

### 9.6 Batch 5 执行记录（2026-05-24）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T15-57-06-903Z-51c4452a`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-57-06-903Z-51c4452a/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、`process-handoff.json`。
- 采纳结论：可采纳；本批是 debug `/tree` shell runtime 同构迁出，未改变 `/api/tree/runtime/reduce` 合同和 page/filetree/picker 行为。

采纳内容：

- 新增 `rust/crates/mnote-web/browser/tree-shell-runtime.js`，从 `tree.rs` 原 inline runtime 同构迁出。
- `build_tree_shell_html()` 保留 `tree-shell-state` bootstrap JSON，改为加载 `/api/mnote-browser-runtime/tree-shell-runtime.js` module script。
- 新增 `/api/mnote-browser-runtime/tree-shell-runtime.js` 路由，返回 `application/javascript; charset=utf-8`。
- `tree.rs` 测试继续通过 `include_str!("../../browser/tree-shell-runtime.js")` 校验 tree shell runtime 的 page/filetree/picker 关键字符串。
- 主控复核后额外采纳一个小型结构化 hoist：`buildFileTreeRuntimeEnvironment`、`reducePageActionWithRuntime`、`reconcilePageRuntimeResult` 从 IIFE 内部移到文件顶层，通过 `runtimeContext` 显式接收依赖，使 CodeGraph 可以定位这些函数；未改变 runtime API 合同。

已通过验证：

- `node --check rust/crates/mnote-web/browser/tree-shell-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell`
  - 结果：42 个 tree_shell 相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web runtime`
  - 结果：57 个 runtime 相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree`
  - 结果：28 个 filetree 相关测试通过。
- `GET /api/mnote-browser-runtime/tree-shell-runtime.js`
  - 结果：`200`，`content-type=application/javascript; charset=utf-8`，浏览器读取内容包含 `startTreeShellRuntime`、`tree.ready`、`hydrateInitialFileTree`、`reducePageActionWithRuntime`、`buildFileTreeRuntimeEnvironment`、`reconcilePageRuntimeResult`。
- `codegraph_search reducePageActionWithRuntime` / `buildFileTreeRuntimeEnvironment` / `reconcilePageRuntimeResult`
  - 结果：3 个函数均可定位到 `rust/crates/mnote-web/browser/tree-shell-runtime.js`。
- 真实 3000 服务运行态说明：
  - 本轮直接访问 `/tree?...` 返回 `404`，说明 debug shell 入口当前不作为公开默认入口暴露；运行态 JS asset 加载由固定 asset route 浏览器验证，debug shell page/filetree/picker HTML 行为由 Rust route 单测覆盖。
  - 主壳登录后确认 `tree-live-controller.js` 等已外置 runtime 正常挂载，P4 主壳 filetree runtime 仍待后续切片。

剩余风险：

- `type="module"` 自带作用域，`tree-shell-runtime.js` 已移除外层 IIFE，并只 hoist P3 关键函数；其他 DOM runtime 仍按原局部闭包组织，避免扩大改动。
- debug tree shell runtime 与主壳 `SIDEBAR_TREE_JS` 仍是两套 runtime，后续 P4/P5 继续拆主壳 filetree runtime 和 document pane host adapter。

### 9.7 Batch 6 执行记录（2026-05-25）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T16-11-49-842Z-94562749`
- worktree: `/mnt/Data1T/mnote-worktrees/p4-filetree-audit`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-11-49-842Z-94562749/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、工作区 diff 与验证结果。
- 采纳结论：主壳 filetree 第一批应优先迁纯 DOM/字符串读取 helper、path 解析和下载详情构造；DND、paste、菜单 DOM、上传执行链继续留在后续批次。

采纳内容：

- 新增 `rust/crates/mnote-web/browser/filetree-runtime.js`。
- 新增固定 asset route：`GET /api/mnote-browser-runtime/filetree-runtime.js`。
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理，保留原实现 fallback：
  - `fileTreeRowDocumentId`
  - `fileTreeRowAssetId`
  - `decodeLocalEncodedPath`
  - `fileTreeRowLocalRelativePath`
  - `fileTreeRowLocalUploadTargetRelativePath`
  - `fileTreeRowKind`
  - `isFileTreeDownloadableAssetRow`
  - `isFileTreeDownloadableRow`
  - `fileTreeAssetDownloadDetail`
  - `fileTreeRowsByRowIds`
  - `fileTreeChildCount`
- 新增 `rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`，先承接 `buildSidebarFileTreeContext` 与 `evaluateSidebarFileTreeWhen` 的 CommandContext / when 表达式求值；菜单 DOM 构建仍留在 `SIDEBAR_TREE_JS`。

采纳边界：

- 本批次不迁移 `selectFileTreeRow`、`openFileTreeContextMenu`、`pasteSidebarFileTreeClipboard`、`downloadSelectedFileTreeAssetRows`、内部/外部 DND、上传执行链。
- `fileTreeChildCount` 虽与 paste sortOrder 有关，但本批仅做同构 runtime 代理并保留 inline fallback，未改变 paste 命令语义。
- filetree context menu 本批只迁 CommandContext/when evaluator，不迁 menu item DOM、事件绑定或右键定位逻辑。

已通过验证：

- `node --check rust/crates/mnote-web/browser/filetree-runtime.js`
- `node --check rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime -- --test-threads=1`
  - 结果：相关测试通过；本轮另以 `sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback`、`filetree_runtime_contains_row_accessor_helpers` 和完整 `filetree` 过滤测试复核。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu -- --test-threads=1`
  - 结果：相关测试通过；本轮另以 `filetree_context` 过滤测试复核。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
  - 结果：runtime asset route 测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree`
  - 结果：29 个 filetree 相关测试通过。
- `node scripts/task487-local-folder-tree-live-consumer-smoke.js`
  - 结果：`ok=true`，证据文件 `tmp/task487-local-folder-tree-live-consumer-smoke/result.json`。
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
  - 结果：`ok=true`，覆盖 editor upload、filetree folder drop、resource tab missing/error、无 active document 附件 tab、filetree scroll、标题来源、附件删除保留相邻附件；证据包含 stdout JSON 和 smoke 生成截图。
- `codegraph_search fileTreeRowLocalRelativePath` / `fileTreeAssetDownloadDetail`
  - 结果：均可定位到 `rust/crates/mnote-web/browser/filetree-runtime.js`。
- `codegraph_search buildSidebarFileTreeContext` / `evaluateSidebarFileTreeWhen`
  - 结果：均可定位到 `rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`。
- 真实浏览器轻量验收：
  - dev server: 复用 / 启动 `MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web`
  - 登录后访问 `http://127.0.0.1:3000/`
  - `document.scripts` 包含 `/api/mnote-browser-runtime/filetree-runtime.js` 与 `/api/mnote-browser-runtime/filetree-context-menu-runtime.js`
  - `window.__mnoteFileTreeRuntime.fileTreeRowLocalRelativePath` 和 `window.__mnoteFileTreeContextMenuRuntime.evaluateSidebarFileTreeWhen` 均为函数。
  - 两个 runtime asset 均返回 `200`，`content-type=application/javascript; charset=utf-8`。
  - 截图证据：`p4-filetree-runtime-mounted.png`。

### 9.8 Batch 7 执行记录（2026-05-25）

Reasonix read-only audit：

- run id: `reasonix-2026-05-24T16-25-22-379Z-3e3df753`
- worktree: `/mnt/Data1T/mnote-worktrees/p4-filetree-selection-audit`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-25-22-379Z-3e3df753/process-handoff.md`
- 类型：只读审计，主控已读取 Hindsight recall、`process-handoff.md`、工作区 diff 与验证结果。
- 采纳结论：主壳 filetree selection 应独立为 `filetree-selection-runtime.js`，不并入纯 accessor 的 `filetree-runtime.js`；DND、clipboard、keydown listener 和菜单 DOM 仍留后续批次。

采纳内容：

- 新增 `rust/crates/mnote-web/browser/filetree-selection-runtime.js`。
- 新增固定 asset route：`GET /api/mnote-browser-runtime/filetree-selection-runtime.js`。
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理，保留原实现 fallback：
  - `visibleFileTreeRows`
  - `syncSidebarFileTreeSelection`
  - `selectSidebarFileTreeRow`
  - `selectedSidebarFileTreeRowIdsForDrag`
  - `selectedSidebarFileTreeRows`

采纳边界：

- 本批次不迁移 click / contextmenu / keydown / dragstart / dragover / drop / dragend 事件监听器。
- 本批次不迁移 `pasteSidebarFileTreeClipboard`、`downloadSelectedFileTreeAssetRows`、`ensureFileTreeWritableTarget`、`deleteSelectedSidebarFileTreeRows`。
- selection runtime 只承接选择状态同步、shift range、ctrl/meta toggle 和拖拽行 id 读取；文件树命令语义仍由主壳和 Rust API 合同决定。

已通过验证：

- `node --check rust/crates/mnote-web/browser/filetree-selection-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_selection_runtime -- --test-threads=1`
  - 结果：2 个 selection runtime 相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
  - 结果：runtime asset route 测试通过。
- 真实浏览器轻量验收：
  - dev server: `MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web`
  - 临时 local folder 入口 `sourceKind=local_folder&treeView=filetree`。
  - `/api/mnote-browser-runtime/filetree-selection-runtime.js` 返回 `200`，`content-type=application/javascript; charset=utf-8`。
  - `window.__mnoteFileTreeSelectionRuntime.selectSidebarFileTreeRow` 为函数。
  - 对两个可见 filetree row 依次触发 click / ctrl-click 后，`data-selected="true"` 保留两行，`tree.filetree.selection.changed` 派发 2 次，最后事件包含两个 `selectedRowIds`。

## 10. 分阶段执行清单

归档说明（2026-05-25）：本节保留原始分阶段路线，但不再作为当前 `done/` 状态的未完成 checklist。已经满足归档条件的范围以第 13 节 Done Gate 为准；仍未实施或未专项验证的条目改为普通状态项，后续需要另起设计或 bug/checklist 承接，不能在本 done 文档中继续以未勾选 checklist 表达。

### P0：定位辅助与基线保护

- 已记录：当前 CodeGraph 对内嵌 JS 的限制，确认不更换 graph 项目。
- 未采用：可选新增 `scripts/extract-embedded-js-for-index.js`，生成临时索引文件，不进入 runtime；本批通过真实模块外置优先解决定位问题。
- 已记录：热点函数归属表，把最近 bug 中高频搜索的函数归到待拆模块：
  - `openResource` / `openLocalOfficeFileInActiveTab` / `openEditorAttachmentEditTab` -> `resource-open-runtime.js`
  - `executeLocalFileTreeExternalDrop` / `uploadFileToMediaAsset` / `insertUploadedAssetIntoEditor` -> `local-upload-runtime.js`
  - `startWithWebSocket` / `startWithSseFallback` -> `tree-live-controller.js`
  - `selectFileTreeRow` / `openFileTreeContextMenu` -> `filetree-selection-runtime.js` / `filetree-context-menu-runtime.js`
  - `createEditorViewBinding` / `replacePaneDocument` -> `document-pane-host-runtime.js`，归 `03-rust-web` 与 `05-editor-mainline` 交界，需单独切片。
- 后续：为 `layout.rs` / `tree.rs` 中计划迁出的 script 增加最小字符串快照测试，防止迁移时漏挂载。
- 基线候选：建立 smoke 基线：
  - `node scripts/task487-local-folder-tree-live-consumer-smoke.js`
  - `node scripts/task446-tree-rename-dual-browser-live-smoke.js`
  - `node scripts/task472-side-target-secondary-pane-smoke.js`
  - `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`

### P1：外置 resource open / local upload adapter

- [x] 新增 `rust/crates/mnote-web/browser/resource-open-runtime.js`。
- [x] 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js`，或先与 resource open 合并为单个不超过 800 行的 `resource-open-runtime.js`。
- [x] 第一刀只迁 `layout.rs` 中 resource open 的纯函数和 bridge；`web_shell.rs` 的 resource tab DOM、session、pane host 生命周期保持在后续 `document-pane-host-runtime.js` 批次。
- 后续：从 `SIDEBAR_TREE_JS` / `web_shell.rs` 中迁出资源打开路径适配，不改变现有 open intent：
  - page row -> 当前 pane page tab
  - filetree doc row -> Markdown page tab
  - local Markdown asset -> active resource tab / secondary pane
  - pdf/code -> 对应 lightweight viewer
  - office -> OnlyOffice path builder
- 后续：从 `SIDEBAR_TREE_JS` 中迁出本地文件夹上传目标计算和 adapter 调用；资源归属语义仍由 Rust API / kernel 合同决定。
  - 2026-05-24 / `reasonix-2026-05-24T15-17-16-430Z-18054487` 只读审计：`leptos-tiptap` 只应发出上传 intent；本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL、`markdownRelativePath` 与真实上传落盘都归本设计的 shell host runtime。
  - 2026-05-24 / Batch 4 已迁：`resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`，并保持调用 `uploadFilesWithResolvedTarget`，未同时迁上传主流程。
  - 暂不迁：`uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor`。这些函数同时涉及 API POST、file tree refresh、Tiptap chain 插入、OnlyOffice/resource tab，需在后续批次继续拆边界。
- 已保留/需按后续切片复跑：现有 smoke：
  - `node scripts/task472-side-target-secondary-pane-smoke.js`
  - `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
- 后续验证：CodeGraph 或等价结构搜索能定位 `openResource`、`buildOnlyOfficeOpenPath`、`executeLocalFileTreeExternalDrop`。

### P2：外置 `TREE_LIVE_CONTROLLER_JS`

- [x] 新增 `rust/crates/mnote-web/browser/tree-live-controller.js`。
- [x] `layout.rs` 只注入 `__MNOTE_TREE_LIVE_BOOTSTRAP__` 和 `<script type="module" src="...">`。
- [x] 新增 `/api/mnote-browser-runtime/tree-live-controller.js` 路由，返回 `application/javascript; charset=utf-8`。
- [x] 保持现有 WS 优先、SSE fallback、local-folder event fallback 行为。
- [x] 浏览器断言：
  - `document.documentElement.dataset.mnoteTreeLiveTransport` 正确。
  - 外置 module 实际执行并设置 `window.__mnoteTreeLiveControllerStarted`。
- 后续专项浏览器回归：WS 推送仍更新 page/filetree。
- 后续专项浏览器回归：强制禁用 WS 时仍 fallback 到 SSE。

### P3：外置 debug `/tree` shell runtime bridge

- [x] 将 `tree.rs` 中 `tree-shell-state` 后的大块 JS 迁到 `browser/tree-shell-runtime.js`。
- [x] `build_tree_shell_html()` 只渲染 HTML、CSS、bootstrap JSON 和 module script。
- [x] 保持 `/api/tree/runtime/reduce` 合同不变。
- [x] debug shell page/filetree/picker 三个模式 smoke 保持通过。
  - 2026-05-24：`cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell` 通过 42 个 tree shell 相关测试；运行态 `/tree` 需 debug route + auth/Convex 或 local workspace 登录态，不作为本批硬门槛。
- [x] 验证 CodeGraph 能定位 `reducePageActionWithRuntime`、`buildFileTreeRuntimeEnvironment`、`reconcilePageRuntimeResult` 等符号。

### P4：抽出主壳 filetree runtime

- 部分完成/后续：从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
  - 2026-05-25 / Batch 6 已完成 context menu / filetree row helper 的首个外置切片。
  - 2026-05-25 / Batch 7 已完成 filetree selection helper 的首个外置切片。
  - 暂不迁：keyboard、clipboard、DND DOM 绑定、菜单 DOM、上传执行链。
- [x] 先拆纯函数和 reducer bridge，再拆 DOM 绑定。
  - 2026-05-25 / Batch 6 已迁：filetree 行 accessor、local relative path、download detail、row lookup、child count wrapper，以及 context menu CommandContext / when evaluator。
  - 2026-05-25 / Batch 7 已迁：visible rows、selection sync、shift range、ctrl/meta toggle、drag row id helper、selected rows helper。
  - 暂不迁：keyboard listener、clipboard action、DND listener、菜单 DOM、上传执行链。
- 后续：主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护，但启动函数迁到外置模块。
- 后续审计：与 Rust `FileTreeSelectionState` 合同对齐，不在前端新增第二套选择真相。
- 后续覆盖：
  - 多选、shift range、ctrl/meta toggle。
  - cut/copy/paste。
  - 内部拖拽 move/copy。
  - 外部文件拖入 local folder。
  - readonly target 拒绝。

### P5：抽出 document pane host adapter

- 后续：从 `web_shell.rs` 的 inline module 中拆出文档 pane host adapter，例如：
  - `document-pane-host-runtime.js`
  - `document-session-runtime.js`
  - `document-conflict-panel-runtime.js`
- 后续：只迁 host/session/pane 生命周期，不迁 Tiptap 编辑器内部命令。
- 后续：与 `05-editor-mainline` 的编辑器 runtime 拆分保持边界：host 负责挂载、pane、session、资源 tab；editor 负责命令、history、附件链接、DOM overlay。
- 后续覆盖：
  - primary / secondary pane 切换
  - conflict panel unmount 清理
  - attachment tab 打开
  - resource tab error placeholder

### P6：收尾与旧 inline script 瘦身

- 后续：`layout.rs` 中只保留小型 bootstrap 和脚本引用。
- 后续：`tree.rs` 中不再内嵌大型 runtime JS。
- 后续：`web_shell.rs` 中只保留 page aggregate/bootstrap JSON 和 module script 引用。
- 已完成：更新 CodeGraph 索引，确认新增 JS 模块被索引。
- 不适用：删除或降级 P0 提取脚本；本批未新增提取脚本。

## 11. 测试与验收

### 11.1 Rust 测试

```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web tree_shell
cargo test -p mnote-web filetree
cargo test -p mnote-web runtime
```

### 11.2 Browser smoke

```bash
node scripts/task446-tree-rename-dual-browser-live-smoke.js
node scripts/task447-tree-move-order-dual-browser-live-smoke.js
node scripts/task448-tree-resync-recovery-dual-browser-smoke.js
node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
node scripts/task472-side-target-secondary-pane-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task487-local-folder-tree-live-consumer-smoke.js
```

### 11.3 CodeGraph 验收

迁移后必须能通过 CodeGraph 或等价结构搜索定位以下浏览器符号：

- `startWithWebSocket`
- `startWithSseFallback`
- `buildOnlyOfficeOpenPath`
- `openEditorAttachmentEditTab`
- `reducePageActionWithRuntime`
- `buildFileTreeRuntimeEnvironment`
- `selectFileTreeRow`
- `executeLocalFileTreeExternalDrop`
- `openFileTreeContextMenu`
- `openResource`

验收标准：

- 这些符号不再只出现在 Rust raw string 中。
- `codegraph_status` 显示新增 JS 文件被索引。
- 字面搜索命中数量减少，定位结果能落到具体 runtime 模块。
- worker 任务书能以“单模块 + 单 smoke”派发，不再要求 worker 搜索整个 `layout.rs` / `web_shell.rs`。

## 12. 风险与控制

- 风险：模块加载路径错误导致主壳首屏无交互。
  - 控制：第一阶段沿用现有 no-store asset route；每次切片保留 smoke。
- 风险：外置模块改变执行时机。
  - 控制：保留 `DOMContentLoaded` / immediate start 逻辑；迁移前后记录启动 DOM 标记。
- 风险：debug shell 与主壳复用时把 debug 行为带进主路径。
  - 控制：host bridge 分为 `debugTreeShellHost` 与 `workspaceSidebarHost`，共享纯 runtime，不共享 debug UI。
- 风险：前端模块继续膨胀成新的巨文件。
  - 控制：单文件超过约 800 行时必须拆职责；DND、selection、transport、resource open、context menu 分文件。
- 风险：外置 JS 无类型约束导致合同漂移。
  - 控制：第一阶段用 Rust JSON contract 测试 + browser smoke；稳定后再评估 TypeScript 或 JSDoc typedef。
- 风险：resource open adapter 迁出时顺手改变 open target 行为。
  - 控制：先建立 open intent 表，迁移只做同构搬迁；secondary pane、active tab、new window fallback 必须由 smoke 覆盖。
- 风险：`03-rust-web` 与 `05-editor-mainline` 边界混淆。
  - 控制：本设计只处理 shell/browser host runtime；Tiptap 命令、history、附件链接增强、编辑器 DOM overlay 由 `05-editor-mainline` 设计处理。

## 13. Done Gate

本设计移动到 `done/` 前必须满足：

- [x] `TREE_LIVE_CONTROLLER_JS` 已从 `layout.rs` 迁出。
- [x] debug `/tree` shell runtime 大块 JS 已从 `tree.rs` 迁出，或已有明确豁免说明。
- [x] 主壳 filetree runtime 的核心 selection / DND / context menu 至少完成一个模块外置切片。
- [x] resource open / local upload adapter 至少完成一个高频 bug 模块外置切片。
- [x] 新增 JS runtime 文件能被 CodeGraph 索引。
- [x] 至少一组 tree realtime smoke 和一组 local folder filetree/resource smoke 通过。
- [x] `layout.rs` / `web_shell.rs` 不再继续新增大型 inline browser runtime。

## 14. 后续问题

- 是否引入 TypeScript：建议等 P1-P3 完成后再决定，不作为本设计前置。
- 是否引入 bundler：当前不需要；plain ES module 足够完成第一阶段。
- 是否把 Page AI panel 一并迁出：不建议混在本设计中。Page AI panel 应另起 `07-ai` 或 `05-editor-mainline` 设计，避免 tree runtime 重构扩大范围。
- 是否保留内嵌 JS 提取脚本：如果 P1-P3 推进顺利，可只作为临时诊断工具；如果短期没有资源迁出大块 runtime，则保留脚本辅助 CodeGraph。
