chore: land tree view-state, vault, Pi module split, and repo hygiene

Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
This commit is contained in:
Agent Board
2026-07-21 05:13:05 +08:00
parent 6f9c7d3b58
commit b798f628ee
264 changed files with 17480 additions and 17314 deletions
@@ -1,330 +0,0 @@
# 4-11 [done] 树域 Rust 家族 final renderer 与宿主收薄清单 v1
> 更新时间:2026-04-26
>
> 前置文档:
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
>
> 口径覆盖(2026-04-28):此前 `rust_runtime_artifact_host`、Rust/WASM reducer runtime、Rust initial DOM 与 `task112/task113` smoke 通过,只能证明 reducer/runtime 阶段完成,不能证明 final Rust DOM renderer 完成。真正 final renderer 的硬门禁以 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准:默认真实流量必须不再依赖 `TreeShellIframeHost` 的 `iframe_srcdoc` DOM shell。
>
> 收口更新(2026-04-28):`task-010` 至 `task-013` 已完成上述 final DOM shell 硬门禁。`rust_family` 默认真实流量已切到 `rust_wasm_dom_shell_host` + `dom_wasm` bridge。
>
> 最终清理更新(2026-05-16):`TreeShellIframeHost` 与对应测试已移动到 `recycle/``NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST` 入口已删除;active source 不再保留 iframe/srcDoc legacy/debug host。
## 1. 当前已收口基线
- `3000` 仍是唯一浏览器公开入口。
- `page tree / file tree / picker` 默认主路径已进入 `rust_family` 3000 `rust_wasm_dom_shell_host`
- `3104` tree shell proxy 已退到显式 debug/internal 边界;默认主站 iframe 不再请求 `/api/tree/shell -> mnote-web:3104`,避免 `desktop:hot` 下页面树/文件树显示 `{"error":"fetch failed"}`
- `file_tree` 主路径输入已收口到 `kernel file_tree items`
- `buildVisibleRows` 不再从 `pageRows + assets` fallback 重建文件树对象语义。
- `streamDelta` 已可写入 `command_logs``domain_events`
- Rust SSE 与 3000 同源 SSE 都能从单条新 `domain_event.payload.streamDelta` 直接产出 `delta`
- Rust `bridge-runtime` 已具备 `tree.subtree.move` 的可测试 canonical move order plan。
- TS transport 已把 Rust `normalizedMove` 透传到 Convex `documents.move` 的可选校验边界。
- Convex `documents.move` 在收到 `normalizedMove` 时会先确认当前排序 patch plan 与 Rust plan 一致,再执行既有兼容写入。
- Rust SSE 与 3000 同源 SSE 对同一 `command_id` 的 command log + domain event 双写做 delta 去重:delta 一致时发一次 `delta`,不一致继续 `resync`
- `tree.subtree.move` 已从 `replace_documents` 过渡 delta 推进到 `move_document` 细粒度 delta
- Rust `bridge-runtime` 已输出 `streamDeltaHint(kind=move_document)``domainEventHint(tree.subtree.moved)`
- 3000 `/api/tree/commands` 按 Rust plan hint + mutation result 物化并写入 `move_document`
- 3000 SSE 与 Rust SSE 均保留 `documentId / parentId / sortOrder / updatedAt`
- 前端 `tree-delta` reducer 可用该 delta 重建 `sidebar_tree / page_tree / file_tree`
- `tree.node.restore / tree.subtree.copy` 已从 `replace_documents` 过渡 delta 推进到细粒度 upsert
- `restore -> upsert_document`
- `copy -> upsert_documents`
- 前端 `tree-delta` reducer 可用 upsert delta 重建 `sidebar_tree / page_tree / file_tree`
- Rust `bridge-runtime` 已输出正式 tree domain event plan
- `tree.node.created / renamed / archived / restored / purged / embedded`
- `tree.subtree.moved / copied`
- 每条正式 tree command plan 都带 `domainEventPlan(schema=mnote.tree.domain_event, schemaVersion=1, eventType, streamDeltaHint)`
- `recordBridgeCommandArtifacts` 会优先使用 Rust plan 透传的 event type / materialized streamDelta,而不是 `${command}.requested`
- `file_tree` 搜索过滤已接入 Rust projection query 主链:
- `KernelProjectionFilter.query` 已进入协议
- `KernelProjectionFilter.maxResults` 已进入协议
- Rust `kernel.project_view(file_tree)` 已能输出命中项与必要祖先
- Rust 搜索 projection 已按“直接命中项限流,必要祖先不计入上限”执行 `maxResults`
- `mnote-web` `/api/tree/projections/file?...&query=` 与 3000 同源 `/api/tree/projections/file` route 已覆盖
- `mnote-web` 与 3000 同源 route/client 已透传 `maxResults`
- Sidebar 搜索态只消费 Rust 搜索 projection items,旧宿主裁剪 helper 已移除
- tree realtime 主链已补强:
- command log 与 domain event 双写时,即使上一帧 cursor 来自另一条流,也会按时间排除旧 artifact 后再做同 `command_id` delta 去重
- 未识别 domain event payload 继续保守 `resync`
- TS artifact 写入边界已固定 `mnote.tree.domain_event` payload schema v1,并保留历史 snake_case 字段兼容 SSE
- resource command 主链已补强:
- `tree.resource.copy / tree.resource.move` 已由 Rust `bridge-runtime` 生成 `resourceTransferPlan`
- `tree.resource.upload` 已由 Rust `bridge-runtime` 生成 `resourceUploadPlan`
- Convex `mediaAssets.batchCopy / batchMove / createWithStorage` 已按 Rust plan 做可选一致性校验
- `tree.resource.copied / moved / uploaded` 已可生成 `asset_result -> upsert_assets` delta / domain event plan
- 3000 media batch/upload route 已改为调用 Rust artifact writer,按 Rust artifact plan 持久化 `command_logs / domain_events`
- Sidebar 对 copy / move / rename / delete / restore / upload 的资源请求已抽成 `file-tree/resource-command-client`
- file tree 内部 drop 已新增 Rust command preflight
- `tree.filetree.drop.preflight` 由 Rust `bridge-runtime` 归一化 target、row 去重、doc/asset 分类、mindmap subPath、copy/move action
- Rust preflight 会拒绝页面移动到自身/后代
- preflight plan 已输出 `documentTransferPlan``resourceTransferPlan`Sidebar 内部 drop 主路径改为先消费 Rust plan 再触发既有 transport
- 宿主仍负责实际命令触发、乐观 UI、副作用通知和错误提示
- file tree 删除目标已新增 Rust command preflight
- `tree.filetree.delete.preflight` 由 Rust `bridge-runtime` 归一化 row 去重、父页面覆盖过滤、doc/asset 删除目标
- Rust delete plan 会在“选中父页面”时过滤其子页面附件,避免宿主重复构造 asset 删除列表
- Sidebar 删除确认与执行主路径改为先消费 Rust delete plan,再触发既有 document/media transport
- file tree 粘贴目标与复制分类已新增 Rust command preflight
- `tree.filetree.paste.preflight` 由 Rust `bridge-runtime` 归一化剪贴板 row、目标页面、focused row、`doc/index` 复制递归语义、真实 asset 过滤和 mindmap `targetSubPath`
- 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
- `Sidebar``rust_family` 粘贴主路径先消费 Rust paste plan,再触发既有 `copyTreeCommand` / `copyFileTreeResourceAssets`
- file tree 外部上传目标已新增 Rust command preflight
- `tree.filetree.upload-target.preflight` 由 Rust `bridge-runtime` 归一化 drop target、focused row、active doc、目标 workspace 与 mindmap `targetSubPath`
- 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
- `Sidebar` 外部文件 drop 主路径先消费 Rust upload target plan,再执行既有文件字节读取与 upload transport
- Rust tree shell 已新增可测试 renderer/state family 基础合同:
- `renderer_input` 覆盖 `page / filetree / picker` 的 projection items、expanded ids、focused id、selected row ids、active picker item、excluded ids、command dispatcher
- `mnote-web` tree shell HTML 已嵌入 Rust 构建的 `rendererInput` JSON contract,供 compat host 与后续 final renderer 共用;compat shell 初始 expanded / selection / picker active / exclude 已优先读取该合同
- 3000 默认 `TreeShellRustDomShellHost` 已对齐同一 projection model`page / filetree / picker` 的 projection items 进入 DOM host model`TreeShellIframeHost` 的 appState JSON 只保留在 legacy/debug host。
- `rendererInput` 已继续暴露三类 reducer contract`rust_page_focus_keyboard_reducer_v1``rust_filetree_selection_reducer_v1``rust_picker_state_reducer_v1`
- `mnote-web /tree` debug shell 与 3000 legacy inline compat host 均已消费同名 reducer contract;当前默认 DOM host 已切到 `TreeShellRustDomShellHost`,并通过 WASM artifact 或同源 Rust reduce seam 执行 runtime。
- 前端 `TreeShellHost` 已暴露 `data-tree-renderer-contract=rust_renderer_input_v1`,主路径 `data-tree-host-implementation=rust_wasm_dom_shell_host``mnote_web_iframe_proxy``rust_inline_compat_host``rust_runtime_artifact_host` 不再代表默认主执行面
- `filetree_selection` 覆盖单选、多选、Shift 范围选、右键选中、清空、visible rows 归一化、drag row ids
- `picker_state` 覆盖高亮归一化、上下/Home/End、排除项、Enter pick
- `focus_state` 覆盖 focused id 归一化与 next/previous/home/end 行走
- `expansion_state` 覆盖默认展开、toggle、活动节点祖先展开
- `keyboard_state` 覆盖导航、打开、展开/折叠、上下文菜单 intent
- `drag_drop_state` 覆盖拖拽 payload 去重归一化与 copy/move effect
- `action_registry` 覆盖 page/filetree/picker 的可用动作集合
- `page_renderer` 已输出 `data-rust-page-renderer=initial_v1` 的 page tree 首屏嵌套 HTML,并由 compat runtime 优先 hydrate 该 Rust DOM;后续状态变化仍保留 JS 重绘路径
- `filetree_renderer` 已输出 `data-rust-filetree-renderer=initial_v1` 的 file tree 首屏嵌套 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate 该 DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
- `picker_renderer` 已输出 `data-rust-picker-renderer=initial_v1` 的 picker 首屏 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate root/item DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
- Rust artifact 写路径已开始落地:
- `bridge-runtime` 已新增 `RuntimeCommandArtifactPlan`,可从 Rust command plan + mutation result 物化 `commandLog``mnote.tree.domain_event` payload
- `mnote-web /api/tree/commands` 已在 Rust transport 内持久化 `bridgeLogs.recordCommandLog / recordDomainEvent`
- artifact 写入失败不会回滚已成功 mutation,响应会暴露 `artifactError` 供观测
- 3000 `/api/tree/commands``/api/media/batch``/api/media/upload`、文档类 `page/save/lifecycle/metadata/page-write/documents-block` Rust transport adapter 已改为调用 Rust artifact writer
- `documents.duplicate` 已补正式 `tree.node.duplicated` domain event plan3000 duplicate 成功链不再回退 TS artifact helper
- 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
- file tree selection 的宿主职责已继续收薄:
- `rust_family``Sidebar` 不再把 legacy React file tree selection reducer 作为选择真相
- `Sidebar` 只从 `tree.filetree.selection.changed` 事件物化 renderer selection snapshot,删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费该 snapshot
- `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop,避免宿主反向控制 renderer selection
- compat runtime 在 file tree 重绘与可见行归一化后会继续回发 selection snapshot,避免宿主业务消费旧镜像
- compat runtime 的 Shift 范围选择与右键选中语义已对齐 Rust `filetree_selection` 合同:Shift 默认覆盖旧选择,Ctrl/Cmd+Shift 才叠加;右键已选中行保留多选集合
## 2. 已完成的 final DOM shell 硬边界与仍未完成项
- 2026-04-28 final DOM shell 硬门禁已完成:`TreeShellIframeHost``iframe_srcdoc` browser bridge 不再是默认 page tree / file tree / picker DOM shell。
- `page tree / file tree / picker` 3000 默认主路径已标识为 `rust_wasm_dom_shell_host`,默认浏览器 bridge 为 `data-tree-browser-bridge="dom_wasm"`
- `TreeShellIframeHost` 已从 active source 移到 `recycle/`;旧 `LOCAL_TREE_SHELL_TEMPLATE``buildInline*Html` 不再参与 active 路径。
- 默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 WASM artifact 或同源 Rust HTTP seam 返回的 state、hostEvents、commandEvents;本地 JS fallback reducer 不再参与默认真实流量。
- 仍未完成的是非 DOM shell 范围:`tree.subtree.move``normalizedMove` compat fallback 删除、`document.snapshot.saved` 独立事件、`tree.node.embed` Page Aggregate/Rust artifact 深层收口等历史条目的后续核验;legacy iframe host 清理已完成。
- 宿主仍承担文件字节读取、外部上传 transport、菜单状态、实际命令调度、乐观 UI 和副作用通知;资源 upload/copy/move、内部 drop preflight、粘贴 preflight、外部上传目标 preflight 的目标与 metadata plan 已进入 Rust command contract。
- `tree.subtree.move` 的 canonical sort plan 已在 Rust 产出并进入 Convex 可选校验,但实际排序写入仍由 Convex `documents.move` 执行。
- `file_tree` 搜索过滤主链已改为 Rust projection query`maxResults / index.md / 附件 / 导图子附件 / book/pdf` 扩展 fixture 已补齐,后续仅保留更丰富搜索语义 hardening。
- `replace_documents` 已退为 restore/copy 的 fallback;主路径 restore/copy 已是 `upsert_document(s)`
- Rust command 已生成 tree/resource delta / domain event plan`mnote-web /api/tree/commands`、3000 tree/media 主写入口与文档类 Rust transport adapter 已可由 Rust artifact writer 落 `command_logs / domain_events`block/save 复合命令已形成 `page.body.saved``block.patched``block.moved``block.embedded` formal domain-event contract。
- block/save 复合命令已并入正式 Rust artifact 主链;OnlyOffice/media writeback 已切到 Rust artifact writer。`document.snapshot.saved` 是否作为独立事件继续留在 `4-16`,不阻塞本文件 final renderer / host thinning 收口。
## 3. Phase Ffinal Rust renderer 接入
### 目标
`page tree / file tree / picker``TreeShellIframeHost` 内联 DOM compat shell,迁到正式 Rust family renderer。
状态:默认主路径已完成迁移,legacy iframe host 已移入 `recycle/`,不再作为显式排障开关保留。
### checklist
- [x] 固定 Rust renderer 输入合同:
- `projection items`
- `expanded ids`
- `focused id`
- `selected row ids`
- `active picker item`
- `command dispatcher`
- 当前已作为 `rendererInput` 嵌入 `mnote-web` tree shell HTMLruntime DOM shell 已优先读该合同初始化局部状态,但尚未完全改为只消费该合同。
- 历史 3000 inline host 已把 `rendererInput``items` 注入同一个 appState;当前默认 DOM host 直接消费 projection model,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 主路径注入第二份 items。
- 默认 DOM shell 已固定 `family=rust_family / version=1 / executionStrategy=dom_wasm`,并优先使用 3000 同源 `tree-shell-runtime` artifactactive source 不再保留 `browserBridge=iframe_srcdoc` host。
- runtime artifact manifest 已新增 `runtimeApi`,显式暴露 `TreeShellRuntimeRequest / TreeShellRuntimeResult`、state snapshot、DOM patch、host event、command event kind。
- runtime artifact manifest 已继续暴露 `reduceEndpoint=/api/tree/runtime/reduce`3000 同源 thin proxy 与 mnote-web endpoint 保留为 fallback/debug。
- 正式 Rust/WASM reducer runtime 已落地:`tree-shell-runtime-wasm` 导出 `reduceTreeShellRuntime`3000 主路径默认优先加载 `mnote-tree-shell-runtime.js``mnote-tree-shell-runtime_bg.wasm` 执行 reduce。
- [x] 拆出 renderer 内部模块:
- [x] row model 雏形(page/filetree/picker renderer test id contract
- [x] page initial DOM renderer(首屏 page tree Rust HTML + hydrate contract
- [x] filetree initial DOM renderer(首屏 file tree Rust HTML contract
- [x] picker initial DOM renderer(首屏 picker Rust HTML contract
- [x] expansion model(默认展开 / toggle / 祖先展开 state family
- [x] focus modelfocused id normalize / next / previous / home / end
- [x] selection modelfiletree selection state family
- [x] picker state model(高亮 / Enter / exclude
- [x] keyboard model(导航 / 打开 / expand-collapse / context menu intent
- [x] drag/drop modelpayload normalize / copy-move effect
- [x] action registrypage / filetree / picker action set
- [x] runtime facade`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API,输出统一 DOM patch / host event / command event
- [x] `page tree` 先切 final renderer。
- 当前状态:默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOMfocus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。
- [x] `picker` 以轻量模式复用同一 renderer state family。
- 当前状态:默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOMkeyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动,测试已禁止默认 iframe postMessage 成功路径。
- [x] `file tree` 后切 final renderer,并保留 `doc / index / asset-folder / asset` 能力。
- 当前状态:默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOMselection、context menu、open、internal/external drop 与 drop target 通过 runtime result/hostEvents 驱动,并保留 doc/index/asset-folder/asset row 口径。
- [x] compat host 退为 debug/internal fallback(本批可验证范围)。
- 3000 默认主路径不再请求 `/api/tree/shell -> mnote-web:3104`,也不再回退旧 React rendererhost implementation 已切到 `rust_wasm_dom_shell_host``mnote-web /tree` 仅保留显式 debug/internal 验证边界,`TreeShellIframeHost` 已进入 `recycle/`
### 验收
- 默认真实流量不再以 `rust_inline_compat_host``rust_runtime_artifact_host``TreeShellIframeHost` 作为主路径标识;page / picker / filetree 默认路径已不再依赖 `iframe_srcdoc` 内联 DOM renderer。
- `data-tree-host-implementation` 不再以 `mnote_web_iframe_proxy` 代表主执行面。
- `page tree / file tree / picker` smoke 仍覆盖打开、键盘、选择、拖放、菜单。
## 4. Phase G:宿主状态机收薄
### 目标
宿主只保留挂载、认证、文件读取、transport bridge,不再解释树域交互合法性。
### checklist
- [x] 上传执行从 sidebar 宿主抽成正式 resource command。
- `tree.resource.upload` 已进入 Rust plan / domain event hint / `asset_result` delta
- 浏览器文件字节读取与 Convex upload URL 仍由 3000 upload route 执行
- [x] 内部 file tree drop payload 归一化进入 Rust command preflight。
- `tree.filetree.drop.preflight` 已接 3000 同源 route `/api/tree/filetree/drop-preflight`
- 前端 `file-tree/shell` 只负责从 projection row / parent snapshot 构造 preflight payload
- Rust plan 输出 target、mindmap subPath、doc/asset 分类、top-level doc、source asset documents、`documentTransferPlan``resourceTransferPlan`
- [x] copy/move 区分与非法投放校验进入 Rust command/projection contract。
- `tree.filetree.drop.preflight` 已按 `copy` 输出文档/资源 transfer plan
- 非 copy 的页面移动已在 Rust preflight 拒绝自身/后代投放
- 更完整的跨工作空间/混合对象权限校验仍随正式 Rust 写路径继续下沉
- [x] file tree 删除目标归一化进入 Rust command contract。
- `tree.filetree.delete.preflight` 已接 3000 同源 route `/api/tree/filetree/delete-preflight`
- `Sidebar` 删除链已先消费 Rust delete plan,再执行既有 `deleteDocumentCommand` / `deleteFileTreeResourceAssets`
- legacy `computeFileTreeShellDeleteTargets` 已退出生产代码,只保留 Rust preflight payload builder
- [x] file tree 粘贴目标推导与复制对象分类进入 Rust command contract。
- `tree.filetree.paste.preflight` 已接 3000 同源 route `/api/tree/filetree/paste-preflight`
- Rust paste plan 已输出 `docItems``resourceTransferPlan`,覆盖 `doc -> recursive true``index -> recursive false`、真实 asset 过滤和 mindmap `targetSubPath`
- `Sidebar``rust_family` 粘贴链已不再本地推导 `docItemsMap / copyableAssetIds`,只消费 Rust paste plan 后触发既有 transport
- [x] file tree 外部上传目标推导进入 Rust command contract。
- `tree.filetree.upload-target.preflight` 已接 3000 同源 route `/api/tree/filetree/upload-target-preflight`
- Rust upload target plan 已输出 `workspaceId / targetDocumentId / targetMindmapId / targetSubPath`
- `Sidebar` 的外部文件 drop 链已不再本地推导 target row / focused row / workspace fallback,只消费 Rust upload target plan 后执行既有 upload transport
- [x] 多选、范围选、右键选中状态进入 renderer state family。
- Rust `filetree_selection` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 均已通过 `rust_filetree_selection_reducer_v1` 合同入口执行选择、右键、可见行归一化与 drag rows 解析;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] picker 高亮、Enter 选中、排除项规则进入 renderer state family。
- Rust `picker_state` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 已通过 `rust_picker_state_reducer_v1` 合同入口执行 next/previous/home/end/pick/focus;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
- [x] 宿主不再持有 file tree 选择真相,只订阅 renderer state event。
- `Sidebar` 已拆分 legacy selection 与 renderer selection snapshot`rust_family` 下只有 `handleFileTreeShellSelectionChange` 写入 renderer snapshot。
- `SidebarTreeSurface` 已移除 `selectedRowIds` 控制输入,file tree 选择只能由 renderer event 回报给宿主业务。
- 仍未完成:compat runtime 内部选择算法虽已统一为 `rust_filetree_selection_reducer_v1` 合同入口,但尚未替换为 Rust runtime/wasm 直接执行,继续归入 Phase F final renderer / runtime 收口。
### 验收
- sidebar 宿主不再包含主要 file tree selection truth 分支;drop legality 仍通过 Rust preflight + 宿主 transport 过渡执行。
- 上传和资源移动失败能返回稳定 command error,不依赖前端临时判断。
- renderer state 可独立测试,不需要挂载整个 sidebar。
## 5. Phase HRust 搜索 projection
### 目标
`file_tree` 搜索过滤不再由宿主裁剪 `kernel file_tree items`,而是由 Rust 输出搜索 projection。
### checklist
- [x] 定义 `file_tree.search` 基础输入:
- query
- root node
- include ancestors
- include assets
- [x] 补齐 `file_tree.search` 扩展输入:
- `index.md` 显式作为可搜索资源;仅自身命中时进入结果,祖先仅用于维持路径
- `maxResults` 限制直接命中项,必要祖先不计入上限
- [x] 定义搜索结果展开策略:
- 命中页祖先展开
- 命中 asset-folder 展开
- 空结果稳定空态
- [x] Rust projection fixture 覆盖:
- mindmap folder
- table
- 命中项必要祖先
- [x] 补齐搜索 fixture 扩展覆盖:
- `index.md`
- 附件
- 导图子附件
- book/pdf
- [x] 前端只消费搜索 projection items,不再计算 visible document ids。
### 验收
- 搜索态、空态、正常态都由同一 Rust projection contract 驱动。
- 前端不再持有搜索过滤的对象语义。
- Phase H 当前验收已满足;后续 richer search semantics 作为 hardening,不再阻塞主链。
## 6. Phase Imove 排序执行下沉
### 目标
`tree.subtree.move` 的排序执行从 Convex compat mutation 迁到 Rust kernel command 主链。
### checklist
- [x] Rust 已有 canonical move order plan 纯函数。
- [x] Rust command plan 已可输出 `normalizedMove`
- [x] Convex `documents.move` 增加可选校验:若传入 Rust `normalizedMove`,执行前确认 patch plan 一致。
- [x] TS transport 对 `documents:move` 透传 `normalizedMove`,避免 Rust plan 停在不可观测状态。
- [x] Rust command plan 已产出正式 `tree.subtree.moved` domain event hint。
- [x] `mnote-web /api/tree/commands` Rust 写路径已可直接持久化正式 `tree.subtree.moved` domain event。
- [x] 前端/stream 不再由 Next route 写 `replace_documents` 作为 move 主 delta。
- [x] 细粒度 move delta reducer 同时更新 `sidebar_tree / page_tree / file_tree`
- [x] `move_document` 不再由 Next route 按 action 手写;当前由 Rust `streamDeltaHint` + mutation result 物化。
- [x] `mnote-web /api/tree/commands``move_document` artifact 已由 Rust transport 直接生成/落库。
- [x] 3000 Next 主写入口已切到 Rust artifact writer。
### 验收
- sort order clamp、同父移动、跨父移动、重复 sort_order、null sort_order、created_at tie-break 均由 Rust 测试锁定。
- Convex 只作为存储执行层,不再持有唯一排序规则。
## 7. Phase J:正式 delta 主链
### 目标
树域 realtime 从 `replace_documents` 过渡 delta,推进到 Rust domain event 驱动的细粒度 delta。
### checklist
- [x] domain event 可携带并被 SSE 解释 `streamDelta`
- [x] command log 与 domain event 同一 `command_id``streamDelta` 一致时,SSE 发一次 `delta` 而不是退回 `resync`
- [x] 定义正式 domain event type hint
- `tree.node.created`
- `tree.node.renamed`
- `tree.node.archived`
- `tree.node.restored`
- `tree.node.purged`
- `tree.node.duplicated`
- `tree.subtree.moved`
- `tree.subtree.copied`
- `tree.resource.copied`
- `tree.resource.moved`
- `tree.resource.uploaded`
- [x] 定义正式 domain event payload schemaTS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1
- [x] Rust 侧由 command plan 生成 delta / event hintNext route 不再按 action 拼主 delta / event type。
- [x] `mnote-web /api/tree/commands` Rust 写路径已由 command result 直接生成并持久化 domain event。
- [x] 显式 compat adapter 写入口已迁出 TS artifact transport。
- [x] block/save-snapshot 复合命令已补齐正式 Rust artifact / domain-event contract。
- 当前完成口径:`page.body.saved` payload 已携带 snapshot 摘要,`block.patched / block.moved / block.embedded` 已有 formal domain-event contract。
- `document.snapshot.saved` 是否拆成独立事件继续留在 `4-16`,不阻塞本阶段收口。
- [x] 前端 reducer 支持细粒度 move。
- [x] 前端 reducer 支持细粒度 restore/copy
- `upsert_document`
- `upsert_documents`
- [x] 前端 reducer 支持资源 upsert delta
- `upsert_assets`
- [x] 未识别事件继续保守 `resync`
### 验收
- 新树命令默认有 domain event delta。
- `replace_documents` 仅作为 fallback,不再是 move / restore / copy 主路径。
## 8. 不做事项
- 不为了“看起来 Rust 化”把 React/DOM compat shell 原样翻译成另一层大壳。
- 不在 sidebar 继续新增树对象真相。
- 不把 `3104` 恢复成默认浏览器入口。
- 不在 Next route 继续扩写长期树命令语义。
@@ -1,34 +0,0 @@
# 4-12 [done] task-003 page tree final renderer 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 page tree 的 Rust initial DOM、hydration patch 与 state-family 阶段完成;不表示 `4-11` 所定义的 final Rust DOM shell 已完成。默认主路径仍依赖 `TreeShellIframeHost` / `iframe_srcdoc` 时,不得引用本文宣称 page tree final renderer 已最终收口。
>
> 最终清理更新(2026-05-16):`TreeShellIframeHost` 与 `iframe_srcdoc` 已退出 active source 并移动到 `recycle/`;本文保留为历史 state-family 阶段记录,当前口径以 `4-18` / `4-19` 为准。
## 目标
`page tree` 默认运行时从 inline compat JS 整树重绘路径继续收口到 Rust family renderer contract
- 首屏 DOM 继续由 `data-rust-page-renderer="initial_v1"` 输出。
- 焦点、active 高亮与键盘移动不再触发 `renderTree()` 整树重绘,只 patch 已 hydrate 的 Rust DOM row 状态。
- 展开/折叠不再把 Rust 初始 DOM 替换成 compat DOM;在已 hydrate Rust DOM 内 patch `aria-expanded`、toggle 文案与子树 fragment。
- `mnote-web /tree` debug shell 与 3000 inline host 保持同一可测试合同,避免默认路径和 debug 路径继续漂移。
## 执行项
- [x] Rust `page_renderer` 初始 HTML 为有子节点的 page row 输出 `tree-node-toggle`,使 Rust DOM 自身具备展开/折叠交互锚点。
- [x] 3000 inline `buildInlinePageTreeHtml` 对齐 Rust 初始 HTML,避免 `TreeShellIframeHost` 继续生成缺少 toggle 的第二份 page DOM 合同。
- [x] 3000 inline runtime 增加 hydrated page DOM patch
- [x] `focusNode``usedRustInitialRenderer` 下只更新 `data-active / data-focused / tabIndex`
- [x] `toggleExpand``usedRustInitialRenderer` 下只 patch 当前节点 `aria-expanded`、toggle 文案与子树显示/插入。
- [x] 新插入的 page 子树 row 复用同一 `bindPageRowEvents`,保留 open/create/rename/menu 事件。
- [x] `mnote-web /tree` debug runtime 对齐同一 page DOM patch 合同,保留 direct debug 验证边界。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web page_renderer`
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
- [x] 本地 Convex 函数已通过 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 同步,避免 `documents.move` 运行旧 validator 拒绝 `normalizedMove`
## 非目标
- 本清单不把 file tree 与 picker 一并切 final renderer;它们对应 `task-004``task-005`
- 本清单不恢复 `3104` 默认入口;`/tree` 仍只作为显式 debug/internal 验证边界。
@@ -1,39 +0,0 @@
# 4-13 [done] task-004 picker final renderer state family 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 picker state-family / runtime reducer / hydrated DOM patch 阶段完成;不表示 picker 已脱离 `TreeShellIframeHost` 的 inline JS DOM shell。final DOM shell 收口以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
>
> 最终清理更新(2026-05-16):`TreeShellIframeHost` 与 `iframe_srcdoc` 已退出 active source 并移动到 `recycle/`;本文保留为历史 state-family 阶段记录,当前口径以 `4-18` / `4-19` 为准。
## 目标
`picker``rust_family` 默认路径下继续退出 compat JS 高亮与 pick 执行路径,收口到轻量 final renderer state family
- 首屏 DOM 继续由 `data-rust-picker-renderer="initial_v1"` 输出。
- 空查询态、搜索结果态、根目录、排除项使用同一 picker state family 口径。
- 键盘高亮与 Enter pick 只走 `rust_picker_state_reducer_v1` 合同入口,并在 hydrated Rust DOM 上 patch `data-focused / tabIndex`
- 鼠标点击 picker row/root 不再绕过 state family;应先归一化当前 active item,再按同一 pick 结果回传宿主。
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
## 执行项
- [x] 补齐 3000 inline picker state family 口径:
- [x] `getPickablePickerEntries` 区分 root 与 doc,并应用 `excludeIds` 后只返回可选项。
- [x] `normalize``focus``next/previous/home/end``pick` 使用同一 pickable 列表。
- [x] 鼠标点击 row/root 通过 state action + `postPickerPickResultToHost`,不直接调用 `handleNavigate` / `tree.pick.root`
- [x] 对齐 `mnote-web /tree` debug runtime
- [x] row/root 点击同样经 state action + pick result helper。
- [x] hydrated Rust DOM 下高亮变化只 patch,不整树重绘;键盘 command 不把焦点抢入 iframe,搜索框焦点保持在宿主输入框。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web picker_renderer && cargo test -p mnote-web picker_state`
- [x] `pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx`
- [x] `node scripts/task113-picker-keyboard-regression-smoke.js`
## 完成记录
- 2026-04-26`task113` 的根因是键盘 command 后 hydrated picker row 主动 `focus()`,导致父级搜索输入框失焦;已改为键盘/state patch 只更新 `data-focused / tabIndex`,点击路径才允许 `focusDom: true`
- 2026-04-263000 inline host 与 `mnote-web /tree` debug shell 已同步 `postPickerPickResultToHost`、pickable state family、row/root click state action 路径。
## 非目标
- 本清单不处理 file tree 的 final renderer;对应 `task-005`
- 本清单不拆除 compat host;对应 `task-006`
@@ -1,39 +0,0 @@
# 4-14 [done] task-005 file tree final renderer 能力保留执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 file tree 的 Rust initial DOM、row contract、selection state family 与能力保留阶段完成;不表示 file tree 已完成 final Rust DOM shell 切换。默认主路径仍依赖 `iframe_srcdoc` 时,不得用本文作为最终完成依据。
>
> 最终清理更新(2026-05-16):`TreeShellIframeHost` 与 `iframe_srcdoc` 已退出 active source 并移动到 `recycle/`;本文保留为历史 state-family 阶段记录,当前口径以 `4-18` / `4-19` 为准。
## 目标
`file tree``rust_family` 默认路径下继续从 3000 inline compat host 收口到 final Rust renderer state family,同时保留文件树关键能力:
- 首屏 DOM 由 `data-rust-filetree-renderer="initial_v1"` 输出,并覆盖 `doc / index / asset-folder / asset` 行语义。
- 选择状态只通过 `rust_filetree_selection_reducer_v1` 合同入口执行与回报,不再由宿主反向控制。
- 内部拖放、外部文件拖入、右键菜单、双击打开、空白区清空选择与 drop target dataset 不退化。
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
## 执行项
- [x] 补齐 3000 inline file tree hydrated runtime
- [x] Rust initial DOM 包含 `doc / index / asset-folder / asset` 行 test id、row kind、document id、asset id、icon hint 与 selected/active dataset。
- [x] `asset-folder``asset` 行保留打开、右键、拖放目标与资源打开 bridge。
- [x] 选择、右键选择、Shift/Ctrl/Cmd 多选、可见行归一化与 drag rows 解析统一走 `rust_filetree_selection_reducer_v1` 镜像入口。
- [x] active/selection/drop feedback 在 hydrated Rust DOM 上 patch dataset,不因宿主 patch 触发整树重绘。
- [x] 对齐 `mnote-web /tree` debug runtime
- [x] debug shell 的 file tree row 语义、selection contract、drag/drop bridge 与 3000 inline host 同步。
- [x] fallback render path 仍可作为 debug/internal 边界,但主路径首屏不再清空 Rust initial DOM。
- [x] 测试覆盖:
- [x] `cargo test -p mnote-web filetree_renderer && cargo test -p mnote-web filetree_selection`
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/lib/file-tree/shell.test.ts src/lib/tree-stream/tree-delta.test.ts`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
## 完成记录
- 2026-04-26:代码对照确认 3000 inline host 与 `mnote-web /tree` debug shell 均已 hydrate `data-rust-filetree-renderer="initial_v1"`,并保留 `doc / index / asset-folder / asset` row contract、selection reducer contract、drag/drop bridge 与资源打开 bridge。
- 2026-04-26task-005 完整验证通过,未发现需要新增生产改动的缺口;本清单作为能力门槛记录。
## 非目标
- 本清单不拆除 compat host 的所有 debug fallback;对应 `task-006`
- 本清单不新增业务命令语义;file tree drop/delete/paste/upload preflight 已在前置任务中下沉 Rust。
@@ -1,33 +0,0 @@
# 4-15 [done] task-006 compat host thinning 执行清单 v1
> 口径修正(2026-04-28):本文件的 `done` 状态只表示本批可验证范围内的 compat host 收薄完成;不表示 compat host 已退为纯 debug/internal,也不表示 `TreeShellIframeHost` 的 JS DOM/state machine 已从默认真实流量删除。最终退场门禁以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
>
> 最终清理更新(2026-05-16):本文以下关于 `TreeShellIframeHost` 仍在 3000 主路径或作为 legacy/debug 暂留的表述均为历史阶段记录;当前 active source 已删除 legacy iframe 入口,旧源码与测试已移动到 `recycle/`。
## 目标
把本批可验证范围内的 compat host 继续收薄:默认 3000 主路径不再请求 `mnote-web:3104` proxy,不回退旧 React renderer,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份主路径 itemspage/filetree/picker 的首屏 Rust initial DOM 与 rendererInput/state family 成为主路径合同。
## 当前边界
- 本任务不声称已经完成完整 Rust runtime / wasm 替换。
- 历史阶段中 `TreeShellIframeHost` 曾是 3000 主路径的 same-origin inline host;当前已退出 active source,不能再作为当前主路径或 debug/internal fallback。
- 3104 `/tree` 只保留显式 debug/internal 验证边界,`task112` 中 direct tree shell debug 已默认跳过。
## 执行项
- [x] 主路径宿主边界:
- [x] page/filetree/picker 在 `rust_family` 下继续进入同源 iframe host,不回退旧 React renderer。
- [x] 缺少 `workspaceId` 时显示 renderer removed 占位,不恢复旧 React renderer。
- [x] 3000 inline `srcDoc` 主路径使用 `state.items + rendererInput`,不再依赖 `__MNOTE_TREE_SHELL_OVERRIDE__` 作为第二输入真相。
- [x] 状态与事件收薄:
- [x] sidebar refresh/sync 保持事件与语义 key,而不是引入新的树结构重建真相。
- [x] file tree selection 只通过 renderer event snapshot 回宿主业务。
- [x] page/filetree/picker smoke 继续覆盖主路径 iframe host 的导航、右键、双击打开、picker 搜索/选中与 stream fallback。
- [x] 验证覆盖:
- [x] `pnpm vitest run src/components/sidebar/tree-shell-surface.test.tsx src/components/sidebar/sidebar-events.test.ts src/components/sidebar/sidebar-sync.test.ts`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
## 后续剩余
- 完整“compat host 退为 debug/internal fallback”仍需要真正 Rust runtime/wasm 或等价正式 renderer 承接运行时 DOM 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。
@@ -1,281 +0,0 @@
# 4-16 [done] 树域 Rust 家族剩余 final runtime checklist v1
> 创建时间:2026-04-27
>
> 来源:`4-9 / 4-10 / 4-11` 中仍未完成的后续目标,以及 `4-12` 到 `4-15` 完成后的剩余边界。
>
> 本文只规划后续目标,不回滚已完成的 3000 legacy inline host、Rust initial DOM、rendererInput/state family 与现有 smoke 覆盖。
>
> 口径修正(2026-04-28):本文记录的 `harness task-001` 至 `task-009` 完成,只代表 Rust/WASM reducer runtime、artifact、route thinning、projection hardening 等阶段性目标完成;不代表 page tree / file tree / picker 已完成 final Rust DOM shell 切换。真正 final DOM shell 的硬门禁以 `../done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准。
>
> 收口更新(2026-04-28):`harness task-010` 至 `task-013` 已完成 final DOM shell 默认路径切换。`rust_family` 默认 host 已改为 `rust_wasm_dom_shell_host`,默认 bridge 为 `dom_wasm`。
>
> 最终清理更新(2026-05-16):`TreeShellIframeHost` 与对应测试已移动到 `recycle/``NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST` 入口已删除;active source 不再保留 iframe/srcDoc legacy/debug host。
>
> 最终 runtime 剩余项收口(2026-04-28):`harness task-014` 至 `task-018` 已完成。`normalizedMove` fallback 已删除;`page.body.saved` 与 `document.snapshot.saved` 已拆分为双 domain event plan`tree.node.embed` 的 `pageReference` 结构与插入位置已迁入 Rust `pageAggregateEmbedPlan``file_tree` 搜索索引可见性已成为 projection result `meta.search.indexingVisibility`。
## 0. 当前执行状态(2026-04-27
`harness` 本轮从本文拆出的 `task-001``task-009` 已全部执行完成,`harness-progress.txt` 记录为 `completed=9 / pending=0 / in_progress=0 / failed=0`
### 已完成
- Phase K 已完成 `rust_tree_shell_runtime_artifact_v1` artifact 边界,以及 page / filetree / picker 的 Rust-side runtime reducer contract。
- Phase K 历史阶段曾固定 3000 inline host 与 `mnote-web /tree` debug shell 都输出同名 runtime artifact;当前默认主路径已继续收口为 `rust_wasm_dom_shell_host``/api/tree/shell -> mnote-web:3104` debug 直连默认关闭。
- Phase K 历史阶段曾把 3000 host implementation 从 `rust_inline_compat_host` 收口为 `rust_runtime_artifact_host`,并在 legacy runtime artifact manifest 中显式标注 `family=rust_family / version=1 / executionStrategy=browser_bridge / browserBridge=iframe_srcdoc`;当前该 active host 已删除,历史源码只在 `recycle/` 中保留。
- Phase K 已新增 Rust `TreeShellRuntimeRequest / TreeShellRuntimeResult` wire-safe facadepage / filetree / picker 三类 request 可 `serde_json` roundtrip,输出统一分为 `TreeShellDomPatch``TreeShellHostEvent``TreeShellCommandEvent`,并已在 runtime artifact manifest 中暴露 request/result contract、state snapshot、patch/event kind。
- Phase K 已新增 `mnote-web POST /api/tree/runtime/reduce`,并新增 3000 同源 `POST /api/tree/runtime/reduce` thin proxy;这是当前 browser bridge 调用 Rust runtime facade 的首个正式 HTTP seam。
- Phase K 已把 3000 `TreeShellIframeHost` 的 filetree selection / normalize visible rows / drag row ids 路径接到 runtime reduce seam,保留本地 reducer fallback,避免 Rust runtime endpoint 不可用时破坏现有真实交互。
- Phase K 已把 3000 `TreeShellIframeHost` 的 page focus / keyboard / expand / collapse / toggle / open / context menu 路径接到 runtime reduce seam,保留本地即时 fallback 与 runtime 对账。
- Phase K 已把 3000 `TreeShellIframeHost` 的 picker focus / keyboard / pick 路径接到 runtime reduce seam,保留本地即时 fallback 与 runtime 对账。
- Phase K 已把 3000 `TreeShellIframeHost` 的 page / filetree open-context 与 picker pick 路径接到 runtime hostEvent 回放器:主路径优先消费 `TreeShellRuntimeResult.hostEvents`endpoint 不可用或无 hostEvent 时才回退本地宿主事件。
- Phase K 已把 3000 `TreeShellIframeHost` 的 filetree open / context menu 路径接到 runtime reduce seam,并已改为优先消费 runtime hostEvent。
- Phase K 已把 3000 `TreeShellIframeHost` 的 filetree drop-target highlight 路径接到 runtime reduce seamhydrated 与 fallback 两条 dragover/dragleave 链都会构造 `updateDropTarget` 请求,并把 runtime snapshot 回填到 `data-drop-target` DOM patch。
- Phase K 已落地正式 Rust/WASM reducer runtime artifact:新增 `rust/crates/tree-shell-runtime-wasm`,通过 wasm-bindgen 导出 `reduceTreeShellRuntime`,固定产物为 `mnote-tree-shell-runtime.js``mnote-tree-shell-runtime_bg.wasm`
- Phase K 已新增 3000 同源 `/api/tree-shell-runtime/*` 资源路由与 `build-tree-shell-runtime.js` 构建脚本,`dev-server` 启动前会生成正式 tree shell wasm artifact。
- Phase K 已将 3000 `TreeShellIframeHost` 的 page / filetree / picker reduce 执行器切为 wasm-first:默认优先调用 wasm artifactHTTP `/api/tree/runtime/reduce` 只作为 fallback/debug seam。
- Phase L 已引入 Rust `treeWriteOperation`Convex `documents.move` 只接受 Rust write operation`normalizedMove` fallback 已删除。
- Phase M 已将 `page.body.saved``document.snapshot.saved``block.patched``block.moved``block.embedded` 切到 formal `domainEventPlan/domainEventPlans` payload schema,复合命令 stream delta 使用 `resync_required` 保守策略。
- Phase N 已新增 route thinning manifest 与 boundary 测试,明确 `/api/tree/commands``/api/tree/stream``/api/tree/shell` 的 thin proxy / debug / compat 边界。
- Phase O 已完成 `file_tree` 搜索 hardening`maxResults=80`、祖先补全/排序、projection result `meta.search.indexingVisibility`、空结果文案,以及 `index / asset-folder / asset / mindmap child / book / pdf` fixture 覆盖。
### 后续阶段已收口
- final DOM shell 默认路径已切到 `rust_wasm_dom_shell_host`page / file tree / picker 不再通过 `TreeShellIframeHost``srcDoc` 内联模板承载默认 DOM shell。
- 默认 DOM host 通过 `reduceTreeShellRuntime` WASM artifact 或同源 `/api/tree/runtime/reduce` Rust seam 消费 `state / hostEvents / commandEvents`;旧本地 JS fallback reducer 不再保留 active 入口。
- 默认真实流量已禁止 `iframe_srcdoc` 作为 tree DOM shell 成功路径;对应负向门禁与 smoke 已覆盖。
- `normalizedMove` fallback 已删除,无 `treeWriteOperation` 的 move 不再进入主写路径。
- `document.snapshot.saved` 已作为独立 event type 固定;`page.body.saved` payload 只保留 page 与 blockssnapshot 版本/hash/updatedAt 进入独立事件。
- `tree.node.embed``pageReference` block 组装已迁入 Rust `pageAggregateEmbedPlan`3000 route 只负责读取源/目标 substrate 快照并作为 preflight 传入。
- Phase O 的索引可见性已从客户端 request meta 提升为 file_tree projection result meta;完整外部索引平台仍不是本树域 runtime 收口阶段目标。
## 1. 当前基线
- `harness` task-001 至 task-009 已完成。
- `page tree / file tree / picker` 默认主路径已使用 3000 same-origin `rust_wasm_dom_shell_host`,不再请求 `/api/tree/shell -> mnote-web:3104`,也不再依赖 `iframe_srcdoc` inline DOM shell。
- 首屏 DOM 已由 Rust initial renderer 输出:
- `data-rust-page-renderer="initial_v1"`
- `data-rust-filetree-renderer="initial_v1"`
- `data-rust-picker-renderer="initial_v1"`
- 主路径已统一为 `state.items + rendererInput`,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份 items。
- page focus、filetree selection、picker state 已暴露并消费 reducer contract;默认 DOM host 已通过 Rust/WASM runtime facade 驱动 page / picker / filetree 的 focus、expand、selection、open/context、drop 与 picker pickHTTP reduce seam 保留为同源 Rust fallback/debug。
- 3000 legacy inline host 与 `mnote-web /tree` debug shell 已同步输出 `rust_tree_shell_runtime_artifact_v1` manifest;默认 DOM host 已使用同源 tree-shell-runtime WASM artifact 或 HTTP reduce seam 执行 runtime。
- tree command move 写路径已优先使用 Rust `treeWriteOperation`Convex 继续作为 substrate 与一致性校验层。
- block/save 复合命令已具备 formal `domainEventPlan``resync_required` 保守 delta 合同。
## 2. 后续总目标
把树域从“Rust 持有语义 + compat JS 持有运行时 DOM/state machine”的过渡态,推进到:
- [x] Rust family 持有 renderer runtime / state machine contract。
- [x] 3000 主路径 host implementation 已从 compat 命名收口为 `rust_wasm_dom_shell_host`
- [x] Rust family 暴露可序列化的 runtime request/result facade,供后续 wasm/browser bridge 调用。
- [x] Rust family 已通过 wasm artifact 持有默认 reducer runtime 执行层,不再默认依赖 HTTP bridge 执行 reduce。
- [x] Rust family 默认持有 renderer DOM 执行层,不再依赖 `TreeShellIframeHost` browser bridge JS adapter 维护默认 DOM/state shell。
- [x] 3000 host 收薄为挂载、认证、transport bridge、artifact writer 与浏览器能力边界。
- [x] Convex compat mutation 不再优先承担 tree command 的核心 legality / normalize / write truth。
- [x] block/save 复合命令进入正式 Rust artifact / domain-event contract。
- [x] snapshot 独立事件与 Page Aggregate 深层收口继续后续推进。
### 最高优先级硬门禁
- [x] 默认 page tree / file tree / picker 主路径不得再依赖 `TreeShellIframeHost``srcDoc` inline DOM shell。
- [x] 默认主路径不得再标注 `data-tree-browser-bridge="iframe_srcdoc"`
- [x] `TreeShellIframeHost` 必须降级为显式 legacy/debug host,不能作为 `rust_family` 默认执行面。
- [x] `task112/task113` 必须改为验证新 Rust/WASM DOM shell 主路径,而不是验证旧 inline iframe host 继续可用。
## 3. Phase K:正式 Rust runtime / wasm 承接 tree shell 运行时
### 目标
完整替换 `TreeShellIframeHost` 内联模板中的主要 compat JS runtime,至少先把 page/filetree/picker 的核心状态机从字符串模板 JS 迁到 Rust runtime/wasm 或等价正式 Rust family runtime。
### checklist
- [x] 定义 runtime artifact 边界:
- [x] 输入:`rendererInput`、projection items、expanded ids、selected row ids、active picker item、focused id。
- [x] 输出:DOM patch / intent event / command dispatch event。
- [x] 事件:focus、keyboard、expand/collapse、selection、context menu、drag/drop、pick。
- [x] 为 page tree runtime 建立 Rust-side 状态机:
- [x] focus normalize / next / previous / home / end。
- [x] expand / collapse / open / context-menu intent。
- [x] drag move intent 与 drop feedback state。
- [x] 为 file tree runtime 建立 Rust-side 状态机:
- [x] selection / range / context selection / normalize visible rows。
- [x] drag row ids / copy-move effect / drop target feedback。
- [x] asset-folder / asset / index / doc 的打开与菜单 intent。
- [x] 为 picker runtime 建立 Rust-side 状态机:
- [x] next / previous / home / end / focus / pick。
- [x] excluded ids 与 root pick。
- [x] 搜索态输入框焦点不被 iframe/runtime 抢走。
- [x] 3000 legacy inline host 与 `mnote-web /tree` debug shell 输出并消费正式 runtime artifact contract。
- [x] 历史 Phase K 主路径标识曾切到 `rust_runtime_artifact_host`iframe 上显式标注 `data-tree-runtime-artifact-host=rust_tree_shell_runtime_artifact_v1``data-tree-browser-bridge=iframe_srcdoc`;当前默认主路径已继续切到 `rust_wasm_dom_shell_host``data-tree-browser-bridge=dom_wasm`
- [x] 定义 wire-safe runtime facade
- [x] `TreeShellRuntimeRequest` 统一承载 page / filetree / picker 的 environment、state、action。
- [x] `TreeShellRuntimeResult` 统一输出 state snapshot、`domPatches``hostEvents``commandEvents`
- [x] `TreeShellDomPatch` 固定 `pageState / fileTreeState / pickerState`
- [x] `TreeShellHostEvent` 固定 page open/context menu、filetree open/context menu、picker pick root/document。
- [x] `TreeShellCommandEvent` 固定 `createNode / renameNode / moveSubtree / copyResource / moveResource / uploadResource`
- [x] runtime artifact manifest 暴露 `runtimeApi`,让 3000 host 可识别 request/result 与 patch/event kind。
- [x] `mnote-web` 暴露 `POST /api/tree/runtime/reduce`,让 browser bridge 可调用 Rust `reduce_tree_shell_runtime`
- [x] 3000 暴露同源 `POST /api/tree/runtime/reduce` thin proxy,保留 cookie/auth/header 转发与 `no-store` 边界。
- [x] 3000 `TreeShellIframeHost` 的 filetree selection / visible row normalize / drag row ids 先接入 runtime reduce seam,并保留本地 fallback。
- [x] 3000 `TreeShellIframeHost` 的 page focus / keyboard / expand / collapse / toggle / open / context menu 已接入 runtime reduce seam,并保留本地 fallback。
- [x] 3000 `TreeShellIframeHost` 的 picker focus / keyboard / pick 已接入 runtime reduce seam,并保留本地 fallback。
- [x] 3000 `TreeShellIframeHost` 的 page / filetree open-context 与 picker pick 已开始消费 runtime hostEvent,并保留本地 fallback。
- [x] 3000 `TreeShellIframeHost` 的 filetree open / context menu 已接入 runtime reduce seam,并优先消费 runtime hostEvent。
- [x] 3000 `TreeShellIframeHost` 的 filetree drop-target highlight 已接入 runtime reduce seam,并保留本地 DOM fallback。
- [x] 新增正式 wasm artifact
- [x] `rust/crates/tree-shell-runtime-wasm` 复用 `mnote-web/src/tree_shell/*` runtime API 与 reducer。
- [x] `reduceTreeShellRuntime(request)` wasm-bindgen API 已可在浏览器/Node 中执行 `TreeShellRuntimeRequest -> TreeShellRuntimeResult`
- [x] `build-tree-shell-runtime.js` 生成固定 `mnote-tree-shell-runtime.js``mnote-tree-shell-runtime_bg.wasm`
- [x] 3000 同源 `/api/tree-shell-runtime/manifest.json``/api/tree-shell-runtime/mnote-tree-shell-runtime.js``/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm` 已覆盖。
- [x] `TreeShellIframeHost` 已通过 `loadTreeShellWasmRuntime / reduceTreeShellRuntimeViaWasm / reduceTreeShellRuntimeWithArtifact` 默认优先使用 wasm artifact。
- [x] 3000 默认 host 不再维护主路径 JS adapter 状态机;`TreeShellIframeHost` 内联模板 browser bridge JS DOM shell 已移入 `recycle/`
- [x] `mnote-web /tree` debug shell 只保留 debug/internal runtime 验证入口。
### 验收
- [x] 历史验收曾覆盖 `tree-shell-iframe-host.test.tsx`2026-05-16 后该测试已随旧 host 移入 `recycle/`active 验收改为 `tree-shell-host.test.tsx` / `tree-shell-surface.test.tsx` 的负向门禁。
- [x] `pnpm vitest run src/app/api/tree/runtime/reduce/route.test.ts src/app/api/tree/shell/route.test.ts src/components/sidebar/tree-shell-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx src/components/documents/move-embed-picker-dialog.test.tsx`
- [x] `pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx`
- [x] `cargo test -p mnote-web page_renderer filetree_renderer picker_renderer`
- [x] `cargo test -p mnote-web tree_runtime_reduce_endpoint_returns_filetree_runtime_result`
- [x] `cargo test -p mnote-web tree_runtime_reduce_endpoint_returns_page_and_picker_runtime_results`
- [x] `cargo test -p mnote-web tree_shell_embeds_renderer_input_contract`
- [x] `cargo test -p mnote-web tree_shell_renderer_input_exposes_runtime_artifact_boundary`
- [x] `cargo test -p tree-shell-runtime-wasm -- --nocapture`
- [x] `node scripts/build-tree-shell-runtime.js`
- [x] `pnpm vitest run src/app/api/tree-shell-runtime/[...asset]/route.test.ts src/components/sidebar/tree-shell-host.test.tsx`
- [x] Node smoke:加载 `generated/mnote-tree-shell-runtime.js` + `.wasm` 后调用 `reduceTreeShellRuntime(mode=page, action=moveNext)`,返回 `focusedId=doc:child`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
- [x] `node scripts/task113-picker-keyboard-regression-smoke.js`
- [x] smoke 中 `direct_tree_shell_debug_disabled` 仍成立,3000 主路径不请求 3104。
### 当前保留边界
- 这里的历史完成口径包含正式 Rust/WASM reducer runtime artifact 与默认 wasm-first reduce 执行;真正 final DOM shell 完成以 `task-010``task-013``4-18` 硬门禁为准。
- 当前 DOM runtime 阶段已把 page/picker/filetree 默认 host 切到 `TreeShellRustDomShellHost`;后续已继续删除 `normalizedMove` fallback、补 `document.snapshot.saved` 独立事件,并把 `tree.node.embed` 的 Page Aggregate / Rust artifact 边界收口。
- 后续阶段不得继续把“新增 runtime reducer action”或 legacy iframe 能力增强误写成 final renderer 主任务;默认路径必须保持 `rust_wasm_dom_shell_host + dom_wasm`
## 4. Phase Ltree.subtree.move 最终写路径下沉 Rust kernel
### 目标
`tree.subtree.move` 的 legality、target parent、position normalize、canonical sort plan 与最终写入从 Convex compat mutation 继续回收到 Rust kernel / Rust write path。
### checklist
- [x] 梳理当前 `normalizedMove` 写入链:
- [x] `/api/tree/commands` preflightData。
- [x] `bridge-runtime` canonical order plan。
- [x] Convex `documents.move` optional validation。
- [x] Rust kernel 输出最终 write operation
- [x] parent_id。
- [x] sort_order patch set。
- [x] workspace_id / updated_at。
- [x] self / descendant / missing-parent legality error。
- [x] Convex mutation 优先执行 Rust `treeWriteOperation`,退为 substrate 写入执行器与一致性断言层。
- [x] 前端 Rust runtime / command adapter 已透传 `treeWriteOperation`,不再以 documents move compat 语义作为主路径。
- [x] domain event 与 streamDelta 使用 Rust move result,而不是 TS/Convex 再拼。
- [x] 删除 `normalizedMove` fallback,并把无 `treeWriteOperation` 的旧 compat move 降级为显式 legacy/debug 路径。
### 验收
- [x] `cd rust && cargo test -p bridge-runtime tree_subtree_move_command`
- [x] `cd wolai-frontend && pnpm vitest run src/app/api/tree/commands/route.test.ts src/lib/documents/tree-command-client.test.ts src/lib/tree-stream/tree-delta.test.ts`
- [x] 增加或复用 Convex 层测试,证明 Convex 优先接受 Rust `treeWriteOperation` 并校验当前排序状态。
### 当前保留边界
- `normalizedMove` 不再作为旧调用链 fallback 保留;move 主写路径强制使用 Rust `treeWriteOperation`
## 5. Phase Mblock/save-snapshot 复合命令 formal artifact 与 domain-event contract
### 目标
`page.body.save / documents.save / blocks.patch|move|embed / snapshot save` 等复合命令补齐最终 formal Rust artifact / domain-event contract,避免停留在 noop 或过渡 hint。
### checklist
- [x] 固定复合命令 event type
- [x] `page.body.saved`
- [x] `document.snapshot.saved`
- [x] `block.patched`
- [x] `block.moved`
- [x] `block.embedded`
- [x] 固定 payload schema
- [x] document id / page id。
- [x] block ids / patch summary / move source-target。
- [x] snapshot version / content hash / updated_at 作为 `document.snapshot.saved` payload 的一部分。
- [x] Rust command plan 输出正式 `domainEventPlan``streamDeltaHint`,不再只用 noop 占位。
- [x] 3000 adapters 与 Rust artifact writer 按同一 schema 落 `command_logs / domain_events`
- [x] SSE reducer 对这些事件有明确 delta 或保守 resync 策略。
- [x] 将 snapshot save 是否需要独立 `document.snapshot.saved` 事件定稿,并避免与 `page.body.saved` payload 重叠。
### 验收
- [x] `cd rust && cargo test -p bridge-runtime documents_save_command_plans_include_formal_domain_event_contract block_commands_include_formal_domain_event_contract`
- [x] `cd wolai-frontend && pnpm vitest run src/lib/documents/rust-runtime.test.ts src/lib/documents/page-write-command-adapter.test.ts src/lib/blocks/block-command-adapter.test.ts`
- [x] 至少一个非 noop 复合命令事件可在 command log 与 domain event 中稳定关联同一 `command_id`
### 当前保留边界
- `document.snapshot.saved` 已独立落地;`page.body.saved` formal payload 不再包含 snapshot 摘要。
## 6. Phase N3000 Next route 过渡职责继续收薄
### 目标
`3000` 保持唯一浏览器公开入口,但 Next route 只做薄代理、认证/浏览器能力桥和兼容边界,不继续承担树域主语义、主 delta 或主 renderer runtime。
### checklist
- [x] 盘点 3000 route 中仍在拼树域主语义的入口。
- [x] 将 route 内 action switch 收敛到 Rust command result / artifact writer。
- [x] 对 polling / SSE / snapshot route 标注 Rust 主体与 Next 薄代理边界。
- [x] 隔离只服务旧 compat host 的 runtime 分支,`/api/tree/shell` 默认关闭 debug 直连。
- [x] 保留文件字节读取、upload URL、cookie/auth 等浏览器入口必须职责。
- [x]`tree.node.embed``pageReference` block 组装从 3000 compat 边界迁到 Page Aggregate / Rust artifact。
### 验收
- [x] `cd wolai-frontend && pnpm vitest run src/app/api/tree/commands/route.test.ts src/lib/tree-stream/tree-delta.test.ts`
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
- [x] 新增文档说明哪些 route 是 thin proxy,哪些仍是待迁移 compat`design/04-tree-domain/done/4-17-tree-3000-route-thin-proxy-boundary-v1.md`
### 当前保留边界
- 3000 仍是唯一浏览器公开入口;thin proxy 不等于删除浏览器能力桥。
- `tree.node.embed` 的目标读取仍是 3000 浏览器公开入口的 substrate 职责;`pageReference` block 结构与插入位置语义已由 Rust `pageAggregateEmbedPlan` 产出。
## 7. Phase O:搜索语义 hardening
### 目标
`file_tree` 搜索过滤主链已在 Rust projection query;后续只补更丰富搜索语义和稳定性,不作为当前 renderer/runtime gate。
### checklist
- [x]`index.md / asset / asset-folder / mindmap child / book / pdf` 搜索命中补更多 fixture。
- [x] 明确 `maxResults` 与祖先补全的排序/截断规则。
- [x] 增加异步索引可见性元数据,特别是 move/embed picker 搜索结果路径。
- [x] 空结果与 fallback 文案保持稳定。
- [x] 如后续需要完整索引平台,再补真实异步索引延迟、刷新和可见性指标,而不是只依赖 projection request meta。
### 验收
- [x] `cd wolai-frontend && pnpm vitest run src/lib/tree-stream/tree-delta.test.ts`
- [x] 搜索 route / projection fixture 覆盖新增资源类型与边界。
### 当前保留边界
- 本阶段完成的是 file tree projection 搜索契约 hardening 与 projection result 可见性指标,不是独立搜索服务或外部索引平台。
## 8. 非目标
- 不拆除 Convex substrate。
- 不把 OnlyOffice 嵌入 BlockNote 画布。
- 不恢复旧 React PageTree/FileTree 作为 rust_family 主路径 fallback。
- 不把 3104 重新暴露为默认前端入口。
- 不在前端新增第二套树结构真相或排序真相。
@@ -1,72 +0,0 @@
# 4-17 [done] tree 3000 route thin proxy boundary v1
> 创建时间:2026-04-27
>
> 来源:`4-16` Phase N。
## 1. 结论
`3000` 继续是唯一浏览器公开入口,但 tree route 不再承载长期树域主语义。
- Rust runtime 负责 tree command legality、canonical plan、write operation、domain event plan 与稳定 stream delta 语义。
- Rust `mnote-web` route 负责 session/actor、source-specific client、browser/substrate 能力桥、CommandEnvelope、transport、artifact writer 调用与用户可见回包;本文中的 Next route 是历史 thin-proxy 阶段记录。
- Convex 仅在显式 cloud / compat source 下作为 substrate 执行器,不重新生成 tree truth;默认 local-first 路径走 LocalFS + Rust SQLite control-plane。
## 2. Thin Proxy Route
### `/api/tree/commands`
保留职责:
- 读取 auth / cookie 并获取 Convex client。
- 构造 `CommandEnvelope`
- 调用 `resolveRustBridgeCommandPlan`
- 调用 `executeRustBridgeMutationTransport`
- 调用 `recordRustBridgeCommandArtifacts` / failure artifact writer。
- 对浏览器返回 `requestId / traceId / result`
不得新增:
- 树合法性判断。
- 排序 canonical plan。
- 第二套 tree write truth。
- 长期 streamDelta 主语义拼装。
当前仍保留的 substrate / browser 边界:
- create / copy 后创建页面 scaffold。
- copy 后复制 mindmap 文件。
- move 前采集 sidebar snapshot 作为 Rust preflight data。
- `tree.node.embed` 的目标内容读取与 Rust preflight 数据准备。
`tree.node.embed``pageReference` block 结构与插入位置已经迁入 Rust `pageAggregateEmbedPlan`;3000 route 不再定义该语义,只负责源/目标快照读取与 preflight 桥接。
### `/api/tree/stream`
保留职责:
- 作为浏览器 SSE / polling 入口。
- 读取 bridge overview。
- 优先转发 command log / domain event 中的 Rust `streamDelta`
- 缺少稳定 delta 或 command/event 不一致时保守 `resync`
不得新增:
- 在 Next 层重新解释树排序或 projection truth。
- 为复合命令伪造局部树补丁。
## 3. Debug / Compat Route
### `/api/tree/shell`
定位:
- 仅显式 debug/internal runtime 验证入口。
- 3000 主路径不应默认请求 `mnote-web:3104`
- 主路径继续使用 same-origin inline host 与 Rust runtime artifact。
## 4. 代码锚点
- `wolai-frontend/src/lib/tree-route-boundary.ts` 固定 3000 route boundary manifest。
- `wolai-frontend/src/app/api/tree/commands/route.test.ts` 绑定 tree commands route 的 thin proxy 定位。
- `wolai-frontend/src/lib/tree-stream/tree-delta.ts` 明确 `resync_required` 为保守等待 snapshot/resync 的 delta。
@@ -1,174 +0,0 @@
# 4-18 [done] 树域 final DOM shell 切换硬门禁 v1
> 创建时间:2026-04-28
>
> 目的:纠正此前把 `Rust/WASM reducer runtime` 误当成 `final Rust renderer` 的完成口径,重新定义 page tree / file tree / picker 真正收口所需的不可绕过门禁。
>
> 收口更新(2026-05-16):旧 `TreeShellIframeHost` 与对应测试已退出 active source,并移动到 `recycle/``NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST` 回退入口已删除。当前口径不再保留 iframe/srcDoc legacy/debug host,默认与唯一活跃 host 是 `rust_wasm_dom_shell_host + dom_wasm`。
>
> 前置文档:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/1.md`
## 1. 口径纠偏
### 2026-04-28 收口结果
本轮 `task-010``task-013` 已按本文硬门禁完成默认主路径切换:
- `TreeShellHost``rust_family + workspaceId` 下默认选择 `rust_wasm_dom_shell_host`
- 历史 `TreeShellIframeHost` 不再保留 active 入口;相关源码与测试已进入 `recycle/`,不再作为可开启的 legacy/debug 排障 host。
- 默认 page tree / file tree / picker 主路径标注 `data-tree-browser-bridge="dom_wasm"`,负向测试禁止 `iframe_srcdoc` 被当成默认成功路径。
- 默认 DOM host 不再调用 `LOCAL_TREE_SHELL_TEMPLATE``buildInlinePageTreeHtml``buildInlineFileTreeHtml``buildInlinePickerHtml`
- 默认 DOM host 的展开、选择、高亮、打开、右键、拖放、picker pick 由 `reduceTreeShellRuntime` WASM artifact 或同源 `/api/tree/runtime/reduce` Rust seam 返回的 `state / hostEvents / commandEvents` 驱动;不再保留 iframe/srcDoc JS fallback reducer 作为可用路径。
- `task112-tree-rust-family-regression-smoke.js``task113-picker-keyboard-regression-smoke.js` 已支持并验证默认 DOM host;后续不再验收 legacy iframe 路径。
`task-010` 之前,过去几轮已经完成的是:
- Rust renderer input / state family / runtime facade。
- `tree-shell-runtime-wasm` reducer artifact。
- 3000 主路径 wasm-first `reduceTreeShellRuntime`
- page / filetree / picker 的若干 hostEvent / commandEvent / DOM patch 切片。
- `rust_runtime_artifact_host` 主路径标识。
这些都只是 `Rust/WASM reducer runtime-first`,不是 `final Rust DOM renderer`
`task-010` 之前真正没有完成的是:
- 当时 `TreeShellIframeHost` 仍在 3000 主路径生成 `srcDoc`
- 当时 `LOCAL_TREE_SHELL_TEMPLATE` 仍承载 DOM 壳、事件绑定、hydration、fallback renderer 与状态 patch。
- 当时 `data-tree-browser-bridge="iframe_srcdoc"` 仍是默认真实流量的浏览器执行层。
- 当时 page / filetree / picker 的 DOM shell 仍由 JS adapter 维护,Rust/WASM 只主导 reducer result。
因此,后续任何文档、harness、提交说明都不能再把 `task112/task113 通过``rust_runtime_artifact_host``wasmModuleUrl/jsGlueUrl 非空` 作为 final renderer 完成声明;必须同时检查默认 host implementation、浏览器 bridge 标记和 legacy flag 边界。
## 2. 不可绕过的完成定义
只有同时满足以下条件,才允许声明“页面树 / 文件树已切换为 Rust final renderer”:
- 3000 默认真实流量的 page tree / file tree / picker 不再通过 `TreeShellIframeHost``srcDoc` 内联模板承载 DOM shell。
- 默认主路径不再标注 `data-tree-browser-bridge="iframe_srcdoc"`
- `LOCAL_TREE_SHELL_TEMPLATE``buildInlinePageTreeHtml``buildInlineFileTreeHtml``buildInlinePickerHtml` 不再参与默认 page / filetree / picker 渲染路径。
- `TreeShellIframeHost` 不得继续存在于 active source;历史源码只能位于 `recycle/` 且被 gitignore 排除。
- Rust/WASM 或等价 Rust family renderer 持有初始渲染、动态 DOM patch、事件绑定归一化、展开/选择/高亮/拖放反馈的运行时 DOM shell。
- 3000 host 只保留挂载、认证、transport、文件字节读取、postMessage / command bridge、artifact 资源发布等浏览器能力边界。
- page / filetree / picker 的主路径回归测试必须包含负向断言:默认路径不得出现 `iframe_srcdoc` DOM renderer。
- `task112/task113` 仍通过,并且 smoke 需要验证的是新 Rust DOM shell 主路径,而不是旧 inline iframe host。
## 3. 第一优先级:先加负向门禁
下一轮实现前必须先补负向测试,防止继续小步绕开最终目标。
### 前端门禁
- `tree-shell-host.test.tsx`、surface 测试或新测试必须断言默认主路径不再输出:
- `srcDoc={renderedInlineSrcDoc}`
- `data-tree-browser-bridge="iframe_srcdoc"`
- `LOCAL_TREE_SHELL_TEMPLATE`
- `buildInlineInitialTreeHtml`
- `buildInlinePageTreeHtml`
- `buildInlineFileTreeHtml`
- `buildInlinePickerHtml`
- 允许这些字符串只出现在 `recycle/` 下的历史文件中;active source 与默认 host implementation 测试不能再把它们作为成功条件。
### smoke 门禁
- `task112-tree-rust-family-regression-smoke.js` 需要确认 page/filetree 默认 renderer host 不是 inline srcdoc host。
- `task113-picker-keyboard-regression-smoke.js` 需要确认 picker 默认 renderer host 不是 inline srcdoc host。
- direct `/api/tree/shell -> 3104` debug disabled 仍应保留,但它不能替代 final DOM shell 验收。
### 文档门禁
- `harness` 任务完成条件必须包含“不依赖 iframe srcdoc DOM shell”。
- 任一阶段文档若只完成 reducer/state family,标题和完成记录必须写成 `runtime reducer/state-family stage`,不得写成 `final renderer completed`
## 4. 实现路线
### Phase P:冻结旧 inline host 主路径
状态:已完成。
目标:把旧 `TreeShellIframeHost` 从 active source 中移出,彻底关闭 legacy/debug 候选入口。
执行项:
- 新增主路径 host 名称,例如 `rust_dom_shell_host``rust_wasm_dom_shell_host`
-`TreeShellIframeHost` 与测试移动到 `recycle/`,不再提供 debug/legacy flag 入口。
- `TreeShellHost` 默认选择新 host;旧 host 不再作为 `rust_family` 默认实现。
- 所有旧 `srcDoc` 合同测试迁到 legacy/debug 测试组。
验收:
- 默认 `TreeShellHost` 测试不再匹配 `iframe_srcdoc`
- active 测试不再证明旧 host 可用于回退排障,只保留默认 DOM/WASM host 的负向门禁。
### Phase Q:建立 Rust/WASM DOM shell host
状态:已完成默认主路径切换。
目标:给 page / filetree / picker 建立一个不依赖 inline JS template 的 DOM shell 承载。
执行项:
- 复用现有 `rendererInput``TreeShellRuntimeRequest/Result`
- WASM artifact 输出或驱动 DOM shell 初始化,不再由 `buildInline*Html` 生成主路径 HTML。
- DOM patch、hostEvent、commandEvent 的应用入口统一放在新 host adapter,adapter 只负责浏览器边界,不维护树状态机。
- page tree 先切,再切 picker,最后切 filetree。
验收:
- page / picker / filetree 默认主路径首屏可见。
- 键盘、展开、选择、右键、打开、拖放反馈继续工作。
- `task112/task113` 使用新 host 通过。
### Phase R:删除默认路径 JS renderer 状态机
状态:已完成默认路径隔离;旧 JS renderer/state machine 已从 active source 移到 `recycle/`
目标:移除默认主路径对旧 JS DOM renderer/state machine 的依赖。
执行项:
- 默认路径删除对 `renderTree()``renderFileTree()``hydrateInitialPageTree()``hydrateInitialPickerTree()` 等旧 DOM shell 的调用。
- 旧本地 fallback reducer 不再保留 active 入口。
- 删除或隔离 `buildInlinePageTreeHtml` / `buildInlineFileTreeHtml` / `buildInlinePickerHtml` 的默认路径调用。
验收:
- 默认 host implementation 中没有 `srcDoc` 主渲染链。
- 默认主路径没有第二套 JS 展开、选择、高亮、拖放接受规则。
- `TreeShellIframeHost` 已移动到 `recycle/`,不能再作为 debug 目录或 active 回退入口。
### Phase S:最终文档收口
状态:已更新 4-11、4-16、4-18 的真实代码状态。
目标:只在真实代码满足门禁后移动文档状态。
执行项:
- `4-11``4-16``4-18` 中所有 final DOM shell gate 勾选。
- 将真正完成后的最终清单移动到 `done/`
-`4-12``4-15` 的旧标题继续误导,则移动到 `design/old/done` 或在标题中明确标记 `state-family stage`
验收:
- 文档状态与默认真实流量一致。
- 没有任何 `done` 文档暗示 `iframe_srcdoc` 主路径等于 final Rust renderer。
## 5. 非目标
- 不拆除 Convex substrate。
- 不恢复 `3104` 为默认浏览器入口。
- 不把文件字节读取、浏览器 DnD `File[]` 对象直接塞入 Rust/WASM。
- 不因为切 DOM shell 而重写 tree command / projection / artifact contract。
- 不把 `task112/task113` 的旧 inline host smoke 当作最终通过标准。
## 6. 当前下一步
本文定义的 final DOM shell 硬门禁已收口。后续继续推进时,不应再回到 `iframe_srcdoc` 默认路径,而应沿以下方向继续减少过渡面:
1. 继续把剩余 tree command、Page Aggregate、snapshot 独立事件等非 DOM shell 收口项按各自设计稿推进。
2. 若新增 page / filetree / picker 交互,默认必须接入 `TreeShellRuntimeRequest/Result`,不得在 DOM host 增加第二套 JS reducer。
3. 若需要排障,只能在 Rust DOM/WASM host 或 Rust Web debug route 上加临时观测,不得恢复 iframe/srcDoc host。
@@ -1,33 +0,0 @@
# 4-19 [done] 树域 final status 与 Rust DOM shell 切换记录 v1
> 更新时间:2026-04-28
>
> 本文由 `process/1.md` 归档而来,用于记录 `design/04-tree-domain` 当前代码已完成的最终树域收口状态。
>
> 收口更新(2026-05-16):旧 `TreeShellIframeHost` 与对应测试已移动到 `recycle/``NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST` 入口已删除。当前 active source 不再保留 iframe/srcDoc legacy/debug host。
## 执行状态(2026-04-28
- [x] 口径收口:`task-001``task-009` 完成的是 Rust/WASM reducer runtime、runtime artifact、route thinning 与 projection hardening`task-010``task-013` 才是 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 定义的 final DOM shell 默认路径切换。
- [x] 默认 host 已从历史 `rust_runtime_artifact_host + iframe_srcdoc` 切到 `rust_wasm_dom_shell_host + dom_wasm``rust_runtime_artifact_host` 与 active `data-tree-browser-bridge="iframe_srcdoc"` 已退出 active source。
- [x] `TreeShellHost``rust_family + workspaceId` 下默认且唯一挂载 `TreeShellRustDomShellHost`;旧 `TreeShellIframeHost` 已移动到 `recycle/`
- [x] 默认 DOM host 不再调用 `LOCAL_TREE_SHELL_TEMPLATE``buildInlinePageTreeHtml``buildInlineFileTreeHtml``buildInlinePickerHtml`,也不再把 `srcDoc` inline DOM shell 当作成功路径。
- [x] 默认 page / filetree / picker DOM host 标注 `data-tree-runtime-artifact-host="rust_tree_shell_runtime_artifact_v1"``data-tree-browser-bridge="dom_wasm"`
- [x] 默认 DOM host 的 page focus / keyboard / expand / collapse / open / context / create / rename / dropfiletree selection / open / context / internal drop / external drop / drop target,以及 picker keyboard / focus / pick,均通过 `TreeShellRuntimeRequest/Result` 消费 WASM artifact 或同源 Rust HTTP seam 返回的 state、hostEvents、commandEvents。
- [x] 旧 JS DOM renderer、inline template、fallback reducer/state machine 已从 active source 移到 `recycle/`,不再作为短期排障面。
- [x] 默认主路径负向测试已覆盖:`tree-shell-host.test.tsx``tree-shell-surface.test.tsx``move-embed-picker-dialog.test.tsx` 均断言默认路径不得出现 `iframe_srcdoc` / `rust-iframe`
- [x] `task112-tree-rust-family-regression-smoke.js``task113-picker-keyboard-regression-smoke.js` 默认验证 `dom_wasm` DOM hostlegacy iframe 不再作为兼容验收路径。
## 历史 runtime 阶段保留记录
- Rust/WASM reducer runtime artifact 已落地:`rust/crates/tree-shell-runtime-wasm` 通过 wasm-bindgen 导出 `reduceTreeShellRuntime`3000 同源 `/api/tree-shell-runtime/*` 发布 `mnote-tree-shell-runtime.js``mnote-tree-shell-runtime_bg.wasm`
- `mnote-web` 与 3000 同源均保留 `POST /api/tree/runtime/reduce` seam;默认 DOM host 优先调用 WASM artifactHTTP seam 作为同源 Rust fallback/debug。
- 历史 `TreeShellIframeHost` 已完成过 page/filetree/picker reducer result 回放、hostEvent/commandEvent 回放、page drop target runtime 收口、picker runtime-first、filetree drop hardening 等切片;这些能力只作为历史记录保留,不再对应 active legacy/debug host。
## 当前保留边界
- `normalizedMove` compat fallback 已删除;无 `treeWriteOperation` 的 move 不再进入主写路径。
- `document.snapshot.saved` 已作为独立 event type 固定;`page.body.saved` 不再携带 snapshot payload。
- `tree.node.embed``pageReference` block 结构与插入位置已迁入 Rust `pageAggregateEmbedPlan`3000 route 只负责读取源/目标 substrate 快照并作为 preflight 传入。
- `TreeShellIframeHost` 与对应旧测试已移入 `recycle/`active source 不再保留 legacy iframe host。
- 3000 仍保留挂载、认证、transport、文件字节读取、浏览器 DnD `File[]`、菜单状态、实际命令调度、乐观 UI 与副作用通知等浏览器能力边界;这是浏览器公开入口职责,不再代表树域主语义滞留在 Next route。
@@ -1,588 +0,0 @@
# 4-21 [done] Local Folder / Convex 统一树源架构 v1
> 更新时间:2026-05-06
>
> 关联:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
>
> 状态说明:当前 `3000` 主入口、本地文件夹打开、统一 `file_tree / page_tree / page_aggregate` 消费链已进入已完成基线;剩余收口继续由 `3-13`、`3-3` 等后续稿跟踪,本稿不再承载新的 process 项。
## 1. 结论
支持引入 `Local Folder Workspace`,但不能因此维护两套文件树操作。
新的长期结构应固定为:
```text
LocalFolderSource -> Tree Kernel -> file_tree / page_tree / page_aggregate -> UI
ConvexWorkspaceSource -> Tree Kernel -> file_tree / page_tree / page_aggregate -> UI
```
也就是说:
- `Convex` 不再是所有 workspace 的必经中介。
- `Local Folder` 不经过 Convex,直接由 Rust source adapter 读取和写入本地文件系统。
- `Convex` 仅作为显式 cloud / compat workspace 的 source adapter 或 sync replica;同步、协作、权限、分享默认控制面已由 Rust SQLite control-plane 承接。
- 两者共用同一套 projection、command、preflight、runtime state 和 UI。
- 差异只允许出现在 `WorkspaceSource` 的 capability、preflight plan 和 executor 中。
核心原则:
> **维护一套树操作语义,不维护两套文件树产品。**
## 2. 为什么需要这个方案
当前 mnote 的 Explorer 视图仍然基于 Convex 虚拟用户空间,这让 VSCode Explorer 对标天然别扭:
- 没有真实文件夹 root。
- `New File / New Folder / Rename / Move / Cut / Paste / Drop` 都需要模拟文件系统语义。
- `index.md`、页面、附件、mindmap asset-folder 混成复合投影后,无法完全照搬 VSCode 文件夹操作。
如果允许用户打开一个本地文件夹,就能获得真实文件系统语义:
- 文件夹就是文件夹。
- `.md` 文件就是页面内容的本体。
- 普通文件就是资源。
- 拖入、拖动、剪切、复制、粘贴、重命名、删除都可以映射到文件系统操作。
- Git、外部编辑器、脚本、ripgrep 等外部工具自然成立。
但这不表示 Convex 失去意义。Convex 的定位应从“所有树的底座”调整为:
> **云工作区 source:负责多端、协作、权限、分享、发布、异步处理和远程资源。**
## 3. 术语
### 3.1 `WorkspaceSource`
`WorkspaceSource` 是树域的存储源适配层。
最低接口:
- `sourceKind`
- `rootUri`
- `workspaceId`
- `capabilities`
- `loadSnapshot`
- `watch`
- `preflightCommand`
- `executeCommand`
- `resolvePageAggregate`
当前建议先支持两类:
- `local_folder`
- `convex_workspace`
### 3.2 `SpaceRoot`
`SpaceRoot` 是用户在 UI 里选择的工作区入口。
示例:
- `file:///home/lix/Documents/mnote-vault`
- `convex://workspace/ws_123`
`SpaceRoot` 可以出现在同一个 Explorer 列表里,但每个 root 只能由一个 source adapter 持有。
### 3.3 `Source Capability`
capability 描述当前 source 对某项操作的真实支持程度。
示例:
- `create-file`
- `create-folder`
- `rename`
- `move`
- `copy`
- `trash`
- `restore`
- `purge`
- `upload`
- `watch`
- `share`
- `publish`
- `collaborate`
capability 不改变 UI 命令名,只控制菜单显隐、禁用原因和 preflight 结果。
## 4. 真源分层
### 4.1 存储真源
存储真源由 source 持有:
| source | 存储真源 |
| --- | --- |
| `local_folder` | 本地文件系统 |
| `convex_workspace` | Convex 数据库 + 对象存储 |
本地模式不得先导入 Convex 再渲染。那会破坏本地文件夹模式的价值。
### 4.2 语义真源
树、页面和资源语义由 Rust kernel 统一生成:
- `KernelNode`
- `KernelEdge`
- `file_tree`
- `page_tree`
- `page_aggregate`
- `tree.*` command result
source adapter 只负责读写存储,不允许 UI 直接从本地路径或 Convex record 拼树语义。
### 4.3 UI 本地态
继续服从 `4-7`
- `expanded`
- `selected`
- `focused`
- `hovered`
- `editingName`
- `clipboard`
- `dragging`
- `dropTarget`
- `contextMenuOpen`
这些不进入 source,也不进入 projection。
## 5. Local Folder Workspace
### 5.1 P0 范围
P0 只支持:
- 打开一个本地文件夹作为 workspace root。
- `file_tree` 显示该文件夹下的目录、`.md` 文件和普通文件。
- `page_tree` 只显示 `.md` 文件。
- 打开 `.md` 文件时进入文档页。
- 普通文件作为 asset 打开或预览。
- 对本地文件和目录执行 VSCode-like 操作。
暂不支持:
- 多用户本地 ACL。
- 云同步。
- 多端协作。
- 非 Markdown 正文格式的页面编辑。
- 本地文件夹自动发布到 Convex。
### 5.2 本地页面身份
不能只用文件路径作为长期 page id。
建议 P0 采用三层策略:
1. `.md` frontmatter 中存在 `mnote_id` 时使用它。
2. 不存在时在 `.mnote/page-ids.json` 记录 `relativePath -> pageId`
3. 临时未初始化文件夹可用 path-derived id,但只作为只读过渡。
原因:
- 重命名文件时页面引用不能断。
- 移动文件时历史、链接、backlink 和 AI 索引需要能追踪。
- 同名文件在不同目录下必须稳定区分。
### 5.3 Markdown 到 Page Aggregate
本地 `.md` 文件映射到 `page_aggregate`
| Page Aggregate 字段 | 本地来源 |
| --- | --- |
| `page_identity` | `mnote_id` / `.mnote/page-ids.json` |
| `page_head.title` | frontmatter `title` / 第一行 H1 / 文件名 |
| `page_body` | Markdown AST / Tiptap document adapter |
| `page_tree` | 文件夹层级派生的 md-only projection |
| `page_layout` | frontmatter / `.mnote/page-options.json` |
写回时:
- 正文保存写回 `.md`
- 标题重命名优先改文件名,必要时同步 H1/frontmatter。
- 页面设置写入 frontmatter 或 `.mnote/page-options.json`,具体由后续编辑器设计冻结。
### 5.4 本地资源
`.md` 文件作为 resource
- image / video / audio
- pdf / book
- table-like 文件
- unknown file
资源链接优先使用相对路径。
附件从页面正文引用时,应解析成 `resourceMeta`,但不能把正文引用本身变成树结构真相。
## 6. Convex Workspace
Convex 继续存在,但定位更明确:
- 云工作区。
- 权限 / 成员 / 分享。
- 评论、协作状态、公开页面。
- 远程附件存储。
- OCR、AI、索引、异步任务。
- 多设备访问。
Convex source adapter 输出同一套 kernel 输入:
- document record -> page/document node
- media asset -> asset/resource node
- mindmap/table/book/pdf -> resource node
- share/public/template 状态 -> resource meta / decoration meta
Convex 不再被要求模拟真实本地文件系统;它只需要实现同一组 tree command 语义。
## 7. 统一 Projection
### 7.1 `file_tree`
`file_tree` 表达 source 的资源管理结构。
本地 source
- root folder
- directory
- markdown file
- normal file
Convex source
- workspace root
- page/document
- `index.md` view
- asset-folder
- asset
共同字段继续使用 `4-4`
- `rowId`
- `nodeId`
- `parentNodeId`
- `nodeType`
- `projectionKind`
- `title`
- `depth`
- `position`
- `childCount`
- `expandable`
- `capabilities`
- `resourceMeta`
- `iconHint`
新增建议:
- `sourceKind`
- `sourceUri`
- `relativePath`
- `storageIdentity`
- `operationProfile`
这些应进入 `resourceMeta.extra` 或后续协议扩展,不能散落在 UI。
### 7.2 `page_tree`
`page_tree` 表达可作为知识页面打开的页面集合。
本地 source
- 只显示 `.md` 文件。
- 默认层级来自目录结构。
- 排序可先使用文件系统排序,后续支持 frontmatter `order`
Convex source
- 继续显示 document/page 层级。
- 不显示普通 asset。
关键判断:
> Wolai 页面树不是文件树的替代,而是 markdown/page resource 的语义视图。
### 7.3 `page_aggregate`
无论 source 是本地还是 Convex,文档页只消费 `page_aggregate`
前端不应该关心正文来自:
- 本地 `.md`
- Convex document content
- 未来同步副本
差异由 page aggregate loader / source adapter 吸收。
## 8. 统一 Command
UI 只发一套命令。
### 8.1 命令路由
```text
UI action
-> tree command envelope
-> Rust command router
-> source adapter preflight
-> source adapter execute
-> kernel event / projection resync
```
命令 envelope 必须携带:
- `sourceKind`
- `workspaceId`
- `rootUri`
- `targetNodeId`
- `targetResourceMeta`
- `selection`
- `operation`
### 8.2 命令语义
| UI 操作 | 统一命令 | Local executor | Convex executor |
| --- | --- | --- | --- |
| 新建页面 | `tree.node.create` | 创建 `.md` | 创建 document |
| 新建文件夹 | `tree.folder.create``tree.resource.createFolder` | 创建目录 | 按 capability 决定是否支持 |
| 重命名页面 | `tree.node.rename` | rename `.md` / 更新 title | rename document |
| 移动页面 | `tree.subtree.move` | move file / folder | move document subtree |
| 复制页面 | `tree.subtree.copy` | copy file / folder | copy document subtree |
| 删除页面 | `tree.node.archive` | 移到 `.mnote/trash` 或系统 trash | 标记 archived |
| 永久删除 | `tree.node.purge` | 删除文件 | purge document |
| 上传 / 拖入文件 | `tree.resource.upload` | copy into folder | upload object storage |
| 移动资源 | `tree.resource.move` | move file | update asset parent/document |
| 复制资源 | `tree.resource.copy` | copy file | copy asset |
| 重命名资源 | `tree.resource.rename` | rename file | rename asset |
| 删除资源 | `tree.resource.archive` | trash file | archive asset |
| 恢复资源 | `tree.resource.restore` | restore from trash | restore asset |
说明:
- `tree.folder.create` 是否新增,需要单独冻结命名。若不新增,也可以把 folder 视作 `tree.resource.create``resourceKind=folder`
- 命名必须在后续 `4-6` 的 command protocol 补稿里冻结。
## 9. 统一操作但不同 capability
用户担心“两套文件树操作”是合理的。解决方式不是把底层做成一样,而是统一 capability 解释。
### 9.1 统一 UI
以下 UI 行为必须完全共用:
- row model
- active / selection / focus
- keyboard
- inline rename
- context menu
- clipboard
- drag/drop
- reveal
- filter/search
- preflight result display
### 9.2 不同 source capability
差异只表现为:
- 菜单项隐藏。
- 菜单项禁用并显示原因。
- preflight plan 返回不同风险。
- execute adapter 调用不同存储 API。
示例:
- 本地文件夹支持 `New Folder`
- Convex 虚拟空间默认不支持真实 `New Folder`,除非引入 folder resource。
- Convex 支持 `Share / Publish`
- 本地文件夹默认不支持 `Share / Publish`,除非执行显式 publish 到 cloud。
## 10. 权限模型
### 10.1 本地权限
本地权限来自:
- 操作系统文件权限。
- 用户选择文件夹的显式授权。
- workspace trust。
- mnote 自己的安全限制,例如禁止越过 root。
如果用户想“本地建立每个用户的文件夹”,可以作为部署策略:
```text
/data/mnote-users/alice/
/data/mnote-users/bob/
```
但这只是本地 source 的 root 管理方式,不等价于 Convex 的协作权限模型。
### 10.2 Convex 权限
Convex 权限来自:
- workspace membership
- document ACL
- public/share state
- group permission
- server-side policy
这类能力本地文件夹不能自动获得。
### 10.3 不混淆
不要让本地文件夹伪装成云协作空间。
正确表达:
- local:你有这个文件夹的本机读写权。
- cloud:你有这个 workspace 的协作权限。
## 11. VSCode 源码参考
本轮已把 VSCode 源码浅克隆到:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
当前 checkout
- commit`b5c5f2f`
- sparse paths
- `src/vs/workbench/contrib/files`
- `src/vs/platform/files`
- `src/vs/base/browser/ui/tree`
- `src/vs/base/browser/ui/list`
- `src/vs/workbench/browser/parts/views`
- command / action / context key 相关基础目录
### 11.1 值得参考的点
VSCode Explorer 有几个重要设计值得直接吸收:
- `IExplorerService` 同时管理 roots、context、editable、copy/cut、refresh、select/reveal 和 bulk edit。
- context key 明确区分 folder、readonly、root、cut、focus、find provider 等状态。
- `ExplorerItem` 是资源模型,持有 `resource/name/isDirectory/isReadonly/parent/root/children`
- renderer 把 cut 状态变成 row class,并把 inline rename 做在 row 内。
- inline rename 默认只选中文件名 stem`F2` 可在 stem/all/extension 三种选择之间切换。
- DnD 会区分 native drop、Explorer 内部 drop、root reorder、readonly、self/descendant invalid、copy vs move、before/after/over indicator 和 auto expand。
- sorter 支持 default/mixed/filesFirst/type/modified/foldersNestsFiles,但排序逻辑仍在模型层,不在 UI 临时判断。
### 11.2 不能照搬的点
不能照搬:
- 真实磁盘路径作为唯一页面身份。
- compact folders 到 page/resource 复合链。
- OS 级菜单动作作为默认能力。
- VSCode 的 editor tab preview/pin 语义。
- 文件 nesting 规则到 mnote 的页面引用或附件关系。
## 12. 推荐实施顺序
### P0:统一 Source 抽象和本地只读投影
- 增加 `WorkspaceSource` 设计与 Rust trait。
- 增加 `LocalFolderSource` 只读扫描。
- 本地 source 输出 `file_tree`
- 本地 source 输出 md-only `page_tree`
- 打开 `.md` 生成只读 `page_aggregate`
- UI 可从入口选择 `convex workspace``local folder`
验收:
- 打开一个本地文件夹后,Explorer 显示真实目录和 `.md`
- 页面树只显示 `.md`
- 点击 `.md` 能进入文档页阅读态。
- Convex 现有 workspace 不受影响。
### P1:本地写操作与统一 command
- 本地新建 `.md`
- 本地新建目录。
- inline rename。
- move / copy / cut / paste。
- delete 到 `.mnote/trash` 或系统 trash。
- drop 外部文件。
- 文件 watcher 刷新 projection。
验收:
- 同一套 UI 对 local 和 Convex 发同一类 command envelope。
- source adapter 分别执行。
- preflight UI 共用。
### P2:本地 Markdown Page Aggregate 写回
- Markdown AST 与 Tiptap document 双向转换。
- 标题编辑写回文件名 / H1 / frontmatter 的规则冻结。
- page id 管理。
- backlink / reference scan。
- 本地全文索引。
### P3Local 与 Cloud 的显式桥接
- Import local folder to cloud。
- Publish local page to cloud。
- Cloud workspace export to folder。
- 可选 sync,不作为 P0/P1。
## 13. 风险
### 13.1 最大风险:双真相
如果本地文件夹先导入 Convex,再从 Convex 渲染,就会变成:
```text
file system -> Convex mirror -> projection -> UI
```
这会制造双真相,也会让 VSCode-like 操作变慢、变怪。
必须避免。
### 13.2 页面身份风险
如果 page id 只依赖路径,移动/重命名会断引用。
P0 可以先只读,P1 写操作前必须完成 page id 方案。
### 13.3 Markdown 往返风险
Tiptap 文档和 Markdown 不是无损双向转换。
P0 可以先阅读态或有限编辑;P2 再处理复杂块、附件、引用、frontmatter。
### 13.4 安全风险
本地 source 必须限制在用户选择的 root 下:
- 禁止 `../` 越界。
- symlink 默认只显示,不默认跟随写入。
- 删除 / 覆盖必须有 preflight。
- watcher 不监听 root 外路径。
## 14. 非目标
- 不让 Convex 成为本地文件夹模式的中介。
- 不维护 local file tree 和 convex file tree 两套 UI。
- 不在前端用路径和 Convex record 临时拼第二套树。
- 不在 P0 做本地/云自动同步。
- 不在 P0 承诺 Markdown 与 Tiptap 完全无损互转。
- 不用本地 per-user 文件夹替代 Convex 协作权限。
## 15. 完成判定
本文进入 done 的条件:
- `WorkspaceSource` 抽象落地。
- local 和 convex 都能通过同一 command envelope 接入 tree runtime。
- `file_tree/page_tree/page_aggregate` 对 UI 的消费路径不分叉。
- 本地文件夹 P0 只读投影和 `.md` 页面打开可验证。
- 至少一组写操作在 local 和 Convex 上共用 UI、共用命令名、只分 source executor。
- 新增能力没有绕开 Rust kernel 或制造第二套树真相。
@@ -1,592 +0,0 @@
# 4-22 [done] Local Folder / Convex 统一树源顺序执行清单 v1
> 更新时间:2026-05-06
>
> 来源:
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-21-local-folder-convex-unified-tree-source-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
>
> 状态说明:本清单对应的 Local Folder / Convex 统一树源主链已进入已完成基线;后续 Markdown parser、Page Aggregate Rust-first、tree live stream 与设计状态漂移由后续稿继续跟踪。
## 0. 执行原则
本清单按顺序执行,不跳阶段。
总目标:
> **只维护一套 tree projection、tree command、preflight、runtime state 和 UILocal Folder 与 Convex 只作为不同 `WorkspaceSource` executor 存在。**
硬约束:
- [x] 不让本地文件夹先导入 Convex 再渲染。
- [x] 不在 UI 层用本地 path 或 Convex record 拼第二套树。
- [x] 不新增 local 专用文件树 UI。
- [x] 不绕过 Rust kernel 写页面树、文件树、排序或权限。
- [x] 不把 `expanded/selected/focus/drag/drop/contextMenu` 写进 projection。
- [x] 所有写操作先 preflight,再 execute,再 projection resync。
**Done 条件:**
- 本清单对应实现中,Local 和 Convex 都通过同一 command envelope 进入 Rust command router。
- `file_tree/page_tree/page_aggregate` 的 UI 消费路径不因 source 分叉。
- 本地文件夹与 Convex 的差异只通过 capability / preflight / executor 表达。
## 1. Phase A:冻结 Source 与协议边界
### A1. 定义 `WorkspaceSource` 合同
- [x] 在 Rust 侧定义 `WorkspaceSource` trait / interface 草案。
- [x] 字段至少覆盖 `sourceKind``rootUri``workspaceId``capabilities`
- [x] 方法至少覆盖 `loadSnapshot``watch``preflightCommand``executeCommand``resolvePageAggregate`
- [x] 明确 `local_folder``convex_workspace` 两个 source kind。
- [x] 写 contract test,验证 source kind 和 root uri 会进入 command envelope。
**Done 条件:**
- Rust 代码中存在统一 `WorkspaceSource` 抽象。
- 至少有 mock source 能输出 `file_tree/page_tree` snapshot。
- 前端没有新增 source-specific tree component。
### A2. 扩展 projection source metadata
- [x] 为 projection item 补 source metadata 方案。
- [x] 字段候选固定为 `sourceKind``sourceUri``relativePath``storageIdentity``operationProfile`
- [x] 优先放入 `resourceMeta.extra`,除非需要正式扩展 `core-protocol`
- [x] 更新 TS 类型映射。
- [x] 补 fixturelocal folder item、convex item 同时存在。
**Done 条件:**
- `file_tree` item 可以区分来自 local 还是 convex。
- UI 仍只读 projection 字段,不直接访问 source adapter。
- fixture 覆盖 `directory/md file/normal file/document/index/asset`
### A3. 统一 command envelope
- [x] 定义 `TreeCommandEnvelope` 的 source 字段。
- [x] envelope 至少包含 `sourceKind``workspaceId``rootUri``targetNodeId``targetResourceMeta``selection``operation`
- [x] 现有 `tree.node.*``tree.subtree.*``tree.resource.*` 进入统一 router。
- [x] router 按 source kind 分发到 executor。
- [x] 对 unknown source / capability missing 返回可解释错误。
**Done 条件:**
- 同一个 UI action 可以构造同一种 envelope。
- local / convex 的 executor 可以在测试中分别接收 envelope。
- 没有新增 `/api/local-filetree/*` 这类平行命令入口。
## 2. Phase BLocal Folder 只读主链
### B1. 打开本地文件夹入口
- [x] 增加选择本地文件夹的入口。
- [x] 生成 `SpaceRoot=file://...`
- [x] 记录最近打开的 local root。
- [x] 禁止 root 外路径访问。
- [x] 对无权限目录返回明确错误。
**Done 条件:**
- 用户能选择一个本地文件夹并进入 local workspace。
- 3000 `desktop:hot` 主入口不能跳到 `/tree` debug shell,也不能落入 `legacy_next_compat_disabled`
- Convex workspace 入口不受影响。
- smoke 能断言当前 source kind 为 `local_folder`
### B2. LocalFolderSource 只读扫描
- [x] 扫描目录。
- [x] 识别 directory。
- [x] 识别 `.md` 文件。
- [x] 识别普通文件。
- [x] 默认忽略 `.git``node_modules``.mnote/trash` 等高噪音目录。
- [x] 处理 symlink:P0 只显示,不默认跟随写入。
**Done 条件:**
- 本地文件夹能输出 read-only snapshot。
- snapshot 不越过 root。
- 扫描失败不会让整个 app 崩溃。
### B3. 输出 local `file_tree`
- [x] directory 映射为 folder row。
- [x] `.md` 映射为 markdown file row。
- [x] 普通文件映射为 asset row。
- [x] iconHint 覆盖 markdown/image/video/audio/pdf/book/table/unknown。
- [x] childCount、depth、parentNodeId、position 由 kernel 生成。
- [x] 排序规则先固定为 VSCode defaultfolder firstname sort。
**Done 条件:**
- `kernel.project_view(file_tree)` 可以返回 local folder projection。
- UI 使用同一 Explorer surface 渲染 local projection。
- 浏览器 smoke 可看到真实目录、`.md`、普通文件。
### B4. 输出 md-only `page_tree`
- [x] 只把 `.md` 文件纳入 page tree。
- [x] 目录只作为页面树分组/父级,不作为正文页面打开。
- [x] 页面标题优先级固定为 frontmatter `title` > 第一行 H1 > 文件名。
- [x] parent/child 来自目录层级。
- [x] 同名 md 文件必须通过 path / page id 区分。
**Done 条件:**
- 同一个 local root 下,Explorer 显示所有文件,页面树只显示 `.md`
- 点击页面树 md 节点能定位到同一个 `.md` resource。
- 不因过滤普通文件影响 page tree 层级。
### B5. `.md` 只读 Page Aggregate
- [x] 读取 `.md` 文件。
- [x] 解析 frontmatter。
- [x] 解析标题。
- [x] 生成只读 `page_aggregate`
- [x] 文档页用现有 `DocumentShell / DocumentContent` 消费。
- [x] 文件读取失败显示错误页,不清空 Explorer。
**Done 条件:**
- 点击本地 `.md` 文件能打开文档阅读态。
- 文档页不关心内容来自 local 还是 Convex。
- `page_aggregate` 字段满足 `5-5-1` 最小契约。
## 3. Phase C:页面身份与本地元数据
### C1. 本地 page id 策略
- [x] 支持 frontmatter `mnote_id`
- [x] 支持 `.mnote/page-ids.json`
- [x] 支持 path-derived 临时 id,但仅用于未初始化只读模式。
- [x] page id 生成必须稳定且不与 Convex document id 冲突。
- [x] 写测试覆盖 rename/move 后 id 保持。
**Done 条件:**
- 重命名或移动 `.md` 后,page id 不变。
- 没有 id 的文件能被安全初始化。
- 同名不同路径 `.md` 不冲突。
### C2. 本地 `.mnote` 元数据目录
- [x] 定义 `.mnote/page-ids.json`
- [x] 定义 `.mnote/page-options.json`
- [x] 定义 `.mnote/trash` 或系统 trash 策略。
- [x] `.mnote` 不出现在默认 Explorer 展示中,除非开启 debug。
- [x] 元数据写入必须原子化或有失败回滚策略。
**Done 条件:**
- `.mnote` 元数据能支持 page id 和页面设置。
- 默认用户视图不被 `.mnote` 噪音污染。
- 元数据损坏时有可解释错误和恢复建议。
## 4. Phase D:统一 Explorer P0 交互
### D1. Active / Selection / Focus 分离
- [x] 统一 row state`activeRowId``selectedRowIds``focusedRowId`
- [x] active 表示右侧打开资源。
- [x] selection 表示 Explorer 操作集合。
- [x] focus 表示键盘焦点。
- [x] local 和 convex 共用 reducer。
- [x] 多选时 selection 视觉优先于 active。
**Done 条件:**
- 截图中的 `index.md` active/focus/selection 歧义被测试覆盖。
- local `.md` 和 Convex `index.md` 使用同一视觉规则。
- selection 不回写 projection。
### D2. 键盘导航
- [x] `ArrowUp/ArrowDown` 移动 focus。
- [x] `ArrowLeft/ArrowRight` 折叠 / 展开 / 父子导航。
- [x] `Home/End` 跳首尾。
- [x] `Enter` 打开 focused row。
- [x] `F2` 进入 inline rename。
- [x] `Delete/Backspace` 触发删除 preflight。
- [x] `Ctrl/Cmd+C` copy。
- [x] `Ctrl/Cmd+X` cut。
- [x] `Ctrl/Cmd+V` paste。
- [x] `Esc` 取消 rename / menu / drag feedback。
**Done 条件:**
- 键盘能力在 runtime contract 中测试,不只散落在 React handler。
- local 和 convex 使用同一组按键浏览器 smoke。
- `cut` 不再缺失。
### D3. 选择与右键
- [x] 单击单选并设置 anchor/focus。
- [x] `Ctrl/Cmd + Click` 切换选中。
- [x] `Shift + Click` 范围选。
- [x] 右键已选区内 row 保留选区。
- [x] 右键未选 row 切换到该 row。
- [x] 空白区点击清空 selection/focus。
**Done 条件:**
- 多选 context menu 不会被右键误清空。
- local/convex 的 selection reducer 共用。
- 过滤可见行变化后 selection normalize 正确。
- 已选区内右键、未选项右键、空白区点击三条路径都必须在真实浏览器中验证。
### D4. Row grid 与 root toolbar
- [x] 固定 indent / twistie / icon / label / decoration / actions 六列。
- [x] section header 使用 `EXPLORER`
- [x] root row 明确显示 local folder 或 cloud workspace。
- [x] toolbar 至少有新建、上传/导入、刷新、全部折叠、更多。
- [x] toolbar action 由 capability 控制可用性。
**Done 条件:**
- hover action 不改变行高和缩进。
- local root 与 Convex root 在同一 Explorer 框架下展示。
- toolbar 不创建 source-specific UI 分支。
- hover / focus 显示 actions 时,row height、indent、twistie/icon/label 列位置必须有浏览器截图或 DOM 断言。
## 5. Phase E:统一 Preflight 与 Context Menu
### E1. Context menu profile
- [x] root 菜单:新建页面、新建文件夹、上传/导入、刷新、全部折叠。
- [x] md/page 菜单:打开、重命名、复制、剪切、粘贴、移动到、删除、Reveal。
- [x] folder 菜单:新建页面、新建文件夹、粘贴、重命名、删除。
- [x] asset 菜单:打开、重命名、复制、剪切、删除、下载、移动到。
- [x] 多选菜单:复制、剪切、删除、移动到。
- [x] share/publish 只在 Convex capability 存在时出现。
**Done 条件:**
- 菜单项由 rowKind/resourceKind/capability/selection 共同裁剪。
- 不支持的能力有禁用原因或隐藏规则。
- mindmap/table 的“不支持”状态被 capability 表达,而不是散落提示。
### E2. Preflight result UI
- [x] delete preflight 展示影响集合。
- [x] move preflight 展示目标和冲突。
- [x] paste preflight 展示命名冲突策略。
- [x] upload/drop preflight 展示目标是否可写。
- [x] readonly / permission denied / root escape 返回统一错误结构。
**Done 条件:**
- local 和 convex 的 preflight 使用同一 UI 组件展示。
- 失败时不发 execute。
- 危险动作不会静默执行。
## 6. Phase FLocal 写操作 P1
### F1. 新建
- [x] `tree.node.create` 在 local source 创建 `.md`
- [x] `tree.folder.create` 或等价命令创建目录。
- [x] 新建后进入 inline rename。
- [x] 命名冲突走 incremental naming 或 preflight 提示。
- [x] Convex 对 `New Folder` 按 capability 禁用,除非已有 folder resource。
**Done 条件:**
- 同一个新建入口可在 local 创建文件,在 Convex 创建 document。
- Convex 不支持 folder 时不会显示假入口。
- 新建结果通过 projection resync 出现。
### F2. Inline rename
- [x] 本地文件 / 目录 rename 写入文件系统。
- [x] `.md` rename 保持 page id。
- [x] Convex document rename 继续走 `tree.node.rename`
- [x] asset rename 收口到 `tree.resource.rename` 或等价正式命令。
- [x] `Enter` 提交,`Esc` 取消。
- [x] 文件型资源默认选中文件名 stem。
**Done 条件:**
- 不再使用 `prompt` 做重命名。
- local 和 convex 的 rename UI 完全一致。
- rename 失败不破坏 selection/focus。
### F3. Move / Copy / Cut / Paste
- [x] local move/copy 使用文件系统操作。
- [x] Convex move/copy 使用 existing tree/resource command。
- [x] cut 状态有 decoration。
- [x] paste 成功后清空 cut。
- [x] 多选时过滤父子重复选择。
- [x] 禁止 move 到自身或后代。
**Done 条件:**
- local/convex 都通过同一 clipboard payload。
- cut/copy/paste 浏览器 smoke 覆盖两个 source。
- projection resync 后 selection/focus 合理保留。
### F4. Delete / Restore / Purge
- [x] local delete 进入 `.mnote/trash` 或系统 trash,策略需冻结。
- [x] local restore 从 trash 恢复。
- [x] local purge 永久删除。
- [x] Convex delete/restore/purge 继续走 `tree.node.*` / `tree.resource.*`
- [x] asset delete/restore 从 legacy `/api/media/batch` 收口。
**Done 条件:**
- 删除不是直接 `rm` 静默执行。
- restore 在 local 和 Convex 都有统一菜单语义。
- purge 有更强确认。
### F5. Drop 外部文件
- [x] local drop 文件复制进目标目录。
- [x] Convex drop 文件上传对象存储。
- [x] drop 到 readonly 目标被拒绝。
- [x] drop 到 md 文件时目标归属规则明确。
- [x] before/inside/after indicator 覆盖本地目录和 Convex document/asset-folder。
**Done 条件:**
- 外部拖入在 local 和 convex 使用同一 drop preflight UI。
- 非法目标不执行写入。
- 文件写入后 projection 自动刷新。
- 外部文件 drop、内部拖拽、before/inside/after indicator、hover auto expand 都必须在真实浏览器中验收,不能只靠 contract test 或 jsdom。
- 至少覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法拖拽场景。
**验收记录:**
- local 外部 drop`scripts/task163-local-folder-unified-tree-browser-smoke.js` 使用真实 Chromium `DataTransfer/File`,覆盖取消 preflight 不写入、确认后复制进目录、drop 到 `.md` 时写入父目录、readonly 目标拒绝且不发 execute。
- Convex 外部 drop:同一 smoke 通过 Rust DOM host 监听 `tree.filetree.external-drop`,覆盖 Convex document 与 asset-folder 两类目标;宿主侧继续由 `tree-shell-dom-host.tsx -> Sidebar.handleResourcePaneDropFiles -> uploadFileTreeResourceAsset -> /api/media/upload` 上传对象存储。历史 `tree-shell-iframe-host.tsx` 已于 2026-05-16 移入 `recycle/`,不再作为 active 路径。
- 对象存储链路:`resource-command-client.test.ts` 覆盖 `uploadFileTreeResourceAsset``app/api/media/upload/route.test.ts` 覆盖 `tree.resource.upload` plan、Convex upload URL、`mediaAssets:createWithStorage` 与资源 delta artifact。
- indicator:同一 smoke 覆盖本地目录 before/inside/after、hover auto expand,以及 Convex document before/inside/after 与 asset-folder inside。
## 7. Phase GWatcher / Realtime / Refresh
### G1. Local file watcher
- [x] 监听 root 下文件新增、删除、重命名、修改。
- [x] 忽略 `.git/node_modules/.mnote/trash` 等目录。
- [x] 防抖合并事件。
- [x] source 输出 tree delta 或触发 snapshot refresh。
- [x] 写操作自身产生的 watcher 事件不造成重复刷新风暴。
**Done 条件:**
- 外部编辑器新增 `.md` 后 page_tree 自动更新。
- 外部删除文件后 Explorer 自动更新。
- watcher 不越过 root。
**验收记录:**
- Rust 侧新增 `/api/tree/local-folder-watch`,由 `LocalFolderSource` 计算 root revision,只在可见文件/目录变化时变更 revision,并复用 root escape 与 ignore 规则。
- 浏览器端 local source 不再固定 2.5 秒 reload,而是轮询 revision,变化后防抖触发 snapshot refresh。
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖 `.mnote` 噪音不改变 revision、可见文件内容变更会改变 revision、root escape 防护。
- `scripts/task163-local-folder-unified-tree-browser-smoke.js` 覆盖外部新增 `.md` 后 page_tree 自动更新,以及外部新增/删除普通文件后 Explorer 自动更新。
### G2. Convex realtime parity
- [x] Convex 继续使用现有 tree stream。
- [x] preferred snapshot 选择不因 local source 破坏。
- [x] local source 和 Convex source 都能进入统一 tree event consumer。
- [x] source 切换时清理上一 source 的 watcher / stream。
**Done 条件:**
- local 和 convex 的实时刷新状态都可观测。
- 切换 workspace 不出现旧 source 事件污染新 workspace。
- 当前 Convex smoke 不回退。
**验收记录:**
- Convex 侧继续由 `useSidebarTreeStream` / tree stream server 处理 realtime`pnpm vitest run src/lib/tree-stream/use-sidebar-tree-stream.test.tsx src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/server.test.ts` 通过。
- local 侧通过同一 tree shell source metadata 和 revision watcher 触发 snapshot refresh,不引入第二套 UI。
- 真实浏览器 smoke 覆盖 local -> Convex source 切换,断言旧 local watcher 内容和 selection 不污染 Convex file tree。
## 8. Phase HMarkdown 编辑与写回 P2
### H1. Markdown AST 到编辑器文档
- [x] Markdown parse 成 editor document。
- [x] frontmatter 保留。
- [x] 基础块覆盖 paragraph / heading / list / quote / code / divider。
- [x] 附件链接解析为 resource reference。
- [x] 不支持块有降级显示。
**Done 条件:**
- 本地 `.md` 可进入可编辑态。
- 基础 Markdown 往返不丢主体内容。
- 不支持格式不会导致保存破坏原文件。
**验收记录:**
- `LocalFolderSource` 的 Markdown 读取侧已覆盖 heading 1-6、paragraph、bullet / numbered list、quote、fenced code、divider、相对附件链接 `media` reference;未知/不支持语法降级为 paragraph。
- `cargo test -p mnote-web local_markdown -- --nocapture` 覆盖基础块解析、附件链接解析、unsupported table 降级显示。
- 真实浏览器 smoke 打开 `docs/blocks.md`,断言 frontmatter title、列表、quote、code 与附件链接文本均可在文档页读出。
### H2. 编辑器文档写回 Markdown
- [x] 保存正文写回 `.md`
- [x] 标题编辑规则冻结:文件名 / H1 / frontmatter 如何同步。
- [x] 页面设置写入 frontmatter 或 `.mnote/page-options.json`
- [x] 保存失败保留编辑器状态并显示错误。
- [x] watcher 与保存事件去重。
**Done 条件:**
- 修改本地 `.md` 后刷新仍能看到新内容。
- 文件名、H1、frontmatter title 不互相打架。
- Convex 编辑链路不受本地写回影响。
**验收记录:**
- 正文保存通过 `/api/documents/save` 写回 `.md`,标题编辑通过 `/api/documents/title` 只写 frontmatter `title`,文件名变更仍由 Explorer rename 负责;三者职责分离。
- 页面设置写入 `.mnote/page-options.json`,不混入正文。
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖本地正文、标题和页面设置写回;`local_markdown_save_preserves_frontmatter_and_writes_basic_blocks` 覆盖 frontmatter 保留与基础块 Markdown 输出。
- 真实浏览器 smoke 通过 document APIs 保存本地 Markdown,刷新后仍能看到新标题和正文;revision watcher 只在本地 root revision 变化后防抖刷新,避免固定刷新风暴。
## 9. Phase ILocal / Cloud 显式桥接 P3
### I1. Import / Export
- [x] local folder import 到 cloud workspace。
- [x] cloud workspace export 到 local folder。
- [x] 冲突策略明确。
- [x] page id 映射保留。
- [x] 附件路径和对象存储映射保留。
**Done 条件:**
- import/export 是显式动作,不是 local 打开时自动发生。
- 用户可预览影响集合。
- 失败可重试或回滚。
**验收记录:**
- P3 候选合同已冻结在 `4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- import/export 均被定义为 `preview -> execute` 显式动作,不能在打开 local folder、保存 local `.md`、watcher 或普通树操作中隐式发生。
- 冲突策略固定为 preview 输出冲突集合,execute 默认 incremental naming,不静默覆盖。
- page id 映射分别通过 `mnote_id` / `.mnote/page-ids.json``cloud_document_id``.mnote/cloud-export-map.json` 保留。
- 附件路径和对象存储 id 必须进入 export/import mapexecute 失败必须返回 retry cursor 和 rollback plan。
### I2. Publish / Sync 候选
- [x] local page publish 到 Convex public page。
- [x] 手动 sync 候选设计。
- [x] 不在 P0/P1 引入隐式双向同步。
- [x] 权限模型不混淆:local OS 权限不同于 Convex ACL。
**Done 条件:**
- 本地 workspace 不依赖 Convex 才能工作。
- Cloud 功能通过显式桥接启用。
- 没有自动双真相。
**验收记录:**
- `4-23` 固定 publish 为 `tree.bridge.publish.preview/execute`,只发布显式选择的单页或子树。
- sync 只进入 `tree.bridge.sync.preview` 候选,不提供默认 `sync.execute`,避免形成自动双真相。
- local OS 权限与 Convex ACL 在 preview 中必须单独展示,不互相推导。
- 当前实现中的 local folder 读写、watcher、Markdown 保存均不依赖 ConvexConvex 能力只通过显式 bridge 候选启用。
## 10. 全局验收矩阵
### 10.1 Contract tests
- [x] `WorkspaceSource` mock local / mock convex。
- [x] command envelope source routing。
- [x] projection source metadata。
- [x] capability-driven menu profile。
- [x] preflight result schema。
- [x] active / selection / focus reducer。
- [x] cut / copy / paste reducer。
**Done 条件:**
- 不启动浏览器也能验证核心协议。
- local/convex 至少各有一套同命令不同 executor 的测试。
- Contract tests 只负责协议、schema、reducer、payload 与 executor 分发,不替代拖拽、右键、多选、外部 drop、source 切换的真实浏览器验收。
### 10.2 Rust tests
- [x] local folder scan。
- [x] md-only page_tree。
- [x] file_tree folder first sorting。
- [x] page id persistence。
- [x] root escape 防护。
- [x] local write executor。
- [x] Convex executor regression。
**Done 条件:**
- Rust 测试覆盖 source adapter 主路径。
- 安全边界有失败用例。
- Convex 旧路径没有被本地 source 破坏。
### 10.3 Browser smoke
- [x] 打开 local folder。
- [x] 3000 `desktop:hot` 主入口覆盖打开本地文件夹:不触发原生 prompt,不跳 `/tree`,不落入 `legacy_next_compat_disabled`
- [x] Explorer 显示目录、md、普通文件。
- [x] 页面树只显示 md。
- [x] 打开 md 到文档页。
- [x] 同一套右键菜单在 local / convex 按 capability 裁剪。
- [x] 已选区内右键保留多选,未选项右键切换 selection 后开菜单,空白区点击清空 selection / focus。
- [x] `Ctrl/Cmd+Click``Shift+Click``Ctrl/Cmd+A` 多选链路。
- [x] 3000 主 Sidebar file tree 的 `Shift+Click` 范围多选由 `scripts/task164-desktop-hot-local-folder-main-entry-smoke.js` 验收。
- [x] inline rename。
- [x] `Ctrl/Cmd+C/X/V` 与菜单 Copy / Cut / Paste 都覆盖,cut decoration 出现,paste 成功后消失。
- [x] 内部拖拽覆盖单选、多选、move / copy 修饰键切换。
- [x] 内部拖拽覆盖 before / inside / after 三态 indicator。
- [x] 内部拖拽覆盖 hover auto expand。
- [x] 内部拖拽覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法场景,并断言不会发 execute。
- [x] 外部文件 drop 使用真实 `DataTransfer/File` 或等价 Playwright 文件拖入,覆盖 local folder、Convex document/asset-folder、readonly 目标、md 文件目标。
- [x] 外部文件 drop 覆盖 preflight UI、写入/上传结果、projection refresh。
- [x] watcher 外部修改刷新。
- [x] 切回 Convex workspace 后原有页面树正常。
- [x] source 切换后旧 watcher / stream 已清理,selection / focus / active 不被旧 source 污染。
- [x] local 与 Convex 上同一动作保持同一 UI 和同一命令名,但 executor 行为可不同。
- [x] 组合场景覆盖多同名 `新页面`、每页下 `index.md`、多选、右键、`F2``Ctrl/Cmd+X/V`、外部文件拖入、切回 Convex workspace。
**Done 条件:**
- smoke 覆盖 source 切换。
- smoke 覆盖 3000 主入口本地文件夹链路;不能只用 `/tree` debug shell 作为完成证据。
- smoke 覆盖至少一个同 UI、同命令、不同 executor 的动作。
- 截图中 Explorer root / active / selection / focus 可解释。
- 浏览器 smoke 必须是真实浏览器自动化,不接受纯 jsdom、DOM 单测或只跑 reducer 的结果。
- 拖拽、外部 drop、上下文菜单、hover 和键盘导航必须在浏览器里触发真实事件链,不接受手工只看截图的验收。
## 11. 最终 done 条件
只有同时满足以下条件,`4-20/4-21/4-22` 才能整体移动到 `done/`
- [x] `WorkspaceSource` 抽象落地。
- [x] `LocalFolderSource` 支持本地只读 `file_tree/page_tree/page_aggregate`
- [x] `ConvexWorkspaceSource` 继续通过同一 projection/command 消费路径工作。
- [x] Explorer P0 交互完成:active/selection/focus、keyboard、context menu、inline rename、cut/copy/paste、drag/drop。
- [x] 至少一组写操作在 local 和 convex 上共用 UI 和 command envelope。
- [x] 本地文件夹不经过 Convex 中介。
- [x] page id 不因本地 rename/move 断裂。
- [x] UI 没有 source-specific tree fork。
- [x] 所有危险写操作都有 preflight。
- [x] 浏览器 smoke 和 Rust/TS contract tests 通过。
- [x] 3000 `desktop:hot` 主入口 smoke 通过,覆盖本地文件夹入口、legacy fallback 拒绝和主 Sidebar `Shift+Click` 多选。
- [x] 拖拽、外部 drop、右键、多选、键盘、source 切换的关键路径在真实浏览器中逐项验收通过;这些 hard gate 未通过时,即使 contract/Rust 测试通过也不得移动到 `done/`
## 12. 明确非目标
- [x] 不在 P0 实现本地和云端自动同步。
- [x] 不在 P0 承诺 Markdown 与编辑器完全无损互转。
- [x] 不用本地 per-user 文件夹替代 Convex 协作权限。
- [x] 不把 VSCode 源码作为直接复制对象;只参考 Explorer 行为模型。
- [x] 不把 Convex 虚拟空间强行伪装成真实磁盘目录。
- [x] 不把 Local Folder 模式做成必须登录才能使用。
@@ -1,237 +0,0 @@
# 4-35 [done] Convex File Tree 标题即 Markdown 文件名体系切换 v1
> 更新时间:2026-05-15
>
> 背景:
> - 用户反馈本地文件夹中新建页面会先显示 `/mnt/Data1T/mnote/tmp/image copy 98.png`,再转回 `新页面.md`。
> - 用户判断这是在线文件夹与本地文件夹的冲突,后续希望“统一而不是继续局部修改”。
> - 目标倾向:不再采用 Convex `page -> index.md` 体系,而是采用直接 `新页面.md` 体系,文件名与标题相同;在线文件夹尽量向本地文件夹靠拢。
## 1. 决策问题
当前 `convex_workspace` 的 File Tree 长期以复合资源树表达页面:
```text
page/document
index.md
mindmap asset
attachment asset
```
`local_folder` 则天然以真实文件系统表达:
```text
新页面.md
图片.png
子目录/
```
这两种形态同时存在时,UI、active row、rename、新建、外部 watcher、projection refresh 容易出现不一致。用户观察到的“先显示 tmp 图片路径,再转回新页面.md”就是这类冲突的症状之一:同一套 File Tree UI 里混入了本地路径资源、Convex 页面容器、`index.md` 伪文件和异步投影校准。
本设计稿用于冻结新的方向:后续 Convex File Tree 应逐步向本地文件夹模型靠拢,把页面在文件树中的默认表现改为直接的 Markdown 文件行,例如 `新页面.md`,而不是页面容器下的 `index.md`
## 2. 建议结论
建议新建一个设计文档,而不是只建 bug 修补。
原因:
1. 这会改变 `file_tree` projection 合同,不只是修一个显示名称。
2. 这会影响 `ObjectIdentity`、active row、rename/title sync、resource parent、trash、restore、search、open intent 和 smoke 期望。
3. 现有 `4-24` / `5-12` 已把 `index.md` 作为 Page Aggregate body 的 object identity 收口到 done;如果要废弃或弱化该 UI 体系,必须有新的迁移设计覆盖旧口径,不能在代码里零散改。
4. Convex 存储层仍然是 document record,不等于必须在 File Tree UI 中展示 `index.md` 伪文件。可以先改 projection / UI 语义,再决定是否迁移底层存储。
## 3. 范围
本设计只讨论 `convex_workspace` 在 File Tree 中的页面呈现与命令语义,不改变 `local_folder` 已有真实文件系统规则。
纳入范围:
- Convex 页面在 File Tree 中显示为 `{title}.md`
- 新建页面时直接出现 `新页面.md`,不先出现 `page/index.md` 或临时资源路径。
- 重命名页面时,标题与文件名同源:`新标题` 对应 `新标题.md`
- 点击 `{title}.md` 打开 Page Aggregate body。
- mindmap、附件、OnlyOffice、代码附件继续作为同一页面下的资源对象,但不能再依赖可见的 `index.md` 子行作为锚点。
- Active / reveal 以 `{title}.md` row 或明确 asset row 为准。
暂不纳入范围:
- 立即迁移 Convex 底层 documents 表结构。
- 自动双向同步 local folder 与 Convex。
- Markdown 文件名与标题的所有非法字符、大小写、重名冲突最终规则;本稿先冻结方向,具体规则后续 checklist 补齐。
## 4. 新旧模型对比
旧模型:
```text
新页面
index.md
mindmap-mindmap_xxx.json
image.png
```
新目标模型:
```text
新页面.md
mindmap-mindmap_xxx.json
image.png
```
解释:
- `新页面.md` 是页面正文对象的 File Tree row。
- 该 row 的 object identity 可继续内部映射到 Page Aggregate body,但 UI 不再展示 `index.md`
- 资源行仍然挂在页面正文对象下面,表达“属于该页面/正文 block 关联资源”。
- Page Tree 仍可显示 `新页面`,不带 `.md`,作为页面导航 projection。
## 5. 关键合同变更
### 5.1 File Tree row
Convex 页面正文 row
- `rowKind=markdown_page` 或沿用 `index` 但对 UI 隐藏 `index.md` 语义,需要后续冻结。
- `title="{pageTitle}.md"`
- `objectIdentity="page:markdown:{documentId}"` 或兼容映射到既有 `page:index:{documentId}`,但对外不再暴露为 `index.md`
- `documentId` 仍是 Convex document id。
- `resourceMeta.sourceKind="convex_workspace"`
本地 `.md` row
- 保持真实文件名。
- `objectIdentity` 与本地 page identity 绑定,不以裸路径作为长期唯一 id。
### 5.2 Rename / title sync
Convex workspace 中:
- 文件树重命名 `新标题.md` 应写入页面标题 `新标题`
- 页面标题编辑应能同步更新 File Tree 行显示为 `新标题.md`
- `.md` 扩展名是 projection/UI 层文件语义,不应直接写进页面标题。
- 重名冲突必须返回结构化 preflight,不允许静默覆盖或临时跳成其它资源路径。
Local folder 中:
- 继续以真实文件名为主,标题优先级仍按现有本地 Markdown 规则处理。
### 5.3 Create
Convex workspace 新建页面:
- 命令结果中应直接携带最终 File Tree row identity 与 `title="新页面.md"`
- UI 乐观插入时不得先显示本地 tmp 路径、上传文件名或 `index.md` fallback。
- 如果后端 title 去重为 `新页面 2`,前端最终只校准成 `新页面 2.md`,中间不出现无关资源名。
### 5.4 Resource parent
mindmap / attachment 等资源仍挂在页面正文对象下,但 parent row 从旧 `index:{documentId}` 或 page container 迁移到新的 markdown page row。
需要特别验证:
- 点击 mindmap asset 后 active row 不跳回 `{title}.md`,除非确实打开的是页面正文。
- 删除 `{title}.md` 进入页面垃圾箱;删除资源进入资源垃圾箱。
- purge 页面时 Convex document 与其资源关系按正式命令同步清理或标记孤儿处理。
## 6. 与既有设计的覆盖关系
本稿不否定 `4-24` / `5-12` 的单一真源目标,只调整 File Tree 的可见模型:
- 仍然保留 Page Aggregate body 是页面正文真源。
- 仍然保留 mindmap / attachment / OnlyOffice 是独立 resource object。
- 仍然要求 object identity 隔离,避免 mindmap 污染页面正文。
- 变化是:Convex File Tree 不再必须通过可见 `index.md` 行表达页面正文对象。
后续如执行本设计,必须同步更新以下旧口径:
- `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` 中“页面节点下固定派生 index.md row”的可见 UI 表达。
- `design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md``index.md tab` 的命名口径,可迁移为 `markdown page tab``{title}.md tab`
- `design/10-review/done/05-tree.md` 作为历史记录不直接改写,但 README 或新设计需说明其 `index.md` 口径已被本稿覆盖。
## 7. 建议实施阶段
### P0:只改 Convex File Tree projection 的页面正文显示
- 新建页面后 File Tree 直接出现 `{title}.md`
- 点击 `{title}.md` 打开 Page Aggregate body。
- Page Tree 仍显示无扩展名标题。
- 保持底层 object identity 兼容,减少一次性改动。
### P1Active / reveal / object identity 迁移
- active page body 映射到 `{title}.md` row。
- mindmap asset 打开后保持 asset active,不降级为页面正文 row。
- 长列表 reveal 稳定。
### P2Rename / create / conflict preflight
- 文件树 rename `{title}.md` 与页面标题同步。
- 新建同名页面明确生成 `新页面 2.md` 或返回冲突选择。
- 不再出现临时 tmp 路径或资源文件名作为页面 row title。
### P3:旧 index.md UI 退场
- 默认 Convex File Tree 不显示 `index.md`
- 如仍需 debug,可通过显式 debug/projection inspector 展示旧 identity。
- 更新 smoke 和设计口径,避免后续继续围绕 `index.md` 修补 UI。
## 8. 验收标准
- 在 Convex workspace 新建页面后,File Tree 首次可见 row 就是 `新页面.md` 或去重后的 `{title}.md`,不得短暂显示 `/tmp/...`、图片文件名、`index.md` 或其它 fallback。
- 点击 `{title}.md` 打开页面正文;点击 mindmap asset 打开 mindmap object editor,二者 active row 不互相覆盖。
- 页面标题编辑与 File Tree rename 最终保持一致:标题 `ABC` 对应 File Tree `ABC.md`
- 同名冲突、非法字符、扩展名输入等情况有结构化 preflight 和可预测结果。
- Local folder 与 Convex workspace 共用同一 File Tree UI 组件、selection/focus/reveal/context menu/keyboard 行为;差异只来自 source capability 与 executor。
- 真实浏览器 smoke 覆盖:新建页面、重命名、点击页面正文、点击 mindmap、滚动下半部分点击、删除进垃圾箱、restore 后 reveal。
## 9. 当前状态
当前状态:`done`
本文已从 `process/` 迁入 `done/`。Convex File Tree 默认可见页面正文行当前以 `doc:<documentId>` + `{title}.md` 为准;旧设计稿中的可见 `index.md` 仅作为历史 object identity / 兼容语义理解,不再作为默认 UI 模型继续派生新任务。
2026-05-15 已完成 P0/P1/P2 的最小切片:
- `bridge-runtime` 的 Convex `file_tree` projection 不再为页面正文派生可见 `index.md` row。
- Convex 页面正文 row 继续使用 `rowKind=document` / `rowId=doc:<documentId>`,但标题改为 `{pageTitle}.md`
- mindmap / attachment / table 等资源继续挂在 `doc:<documentId>` 下。
- 3000 主文档壳本地 create apply 同步改为只插入 `doc:<documentId>`,标题显示为 `{title}.md`,不再插入 `index:<documentId>`
- File Tree 当前页面 selected / focused 默认改为 `doc:<documentId>`
- `task169` 的“回到页面正文”验证从点击 `index:<documentId>` 改为点击 `doc:<documentId>`object identity 仍为 `page`
- File Tree inline rename 对页面正文 row 输入 `{title}.md` 时,提交给 `tree.node.rename` 前会剥离 `.md`,页面标题保持 `{title}`File Tree 显示保持 `{title}.md`
- File Tree inline rename 已加入主壳内校验:空名、非法文件名字符、同级同名页面会停留在输入框内并显示结构化提示,不提交 rename command。
已通过验证:
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime file_tree_projection -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web file_tree_projection -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell_filetree_renderer_outputs_initial_nested_html_contract -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell -- --nocapture
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task426-mnote-web-main-no-reload-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task438-filetree-title-md-active-reveal-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task439-filetree-title-md-rename-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task440-page-title-filetree-md-sync-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3428 node scripts/task169-mindmap-realtime-smoke.js
```
真实浏览器结果文件:
- `tmp/task426-mnote-web-main-no-reload-smoke/result.json`
- `tmp/task438-filetree-title-md-active-reveal-smoke/result.json`
- `tmp/task439-filetree-title-md-rename-smoke/result.json`
- `tmp/task440-page-title-filetree-md-sync-smoke/result.json`
- `tmp/task169-mindmap-realtime-smoke/result.json`
本轮 P3 收口:
- 页面标题编辑反向同步到 File Tree `{title}.md` 已由真实浏览器 smoke `scripts/task440-page-title-filetree-md-sync-smoke.js` 覆盖:标题栏输入 `{title}` 后,File Tree 显示 `{title}.md`,Page Tree 与标题输入框保持 `{title}`,且不出现 `index:<documentId>` 行。
- `tree_shell/filetree_renderer.rs` 的默认 nested fixture 已从可见 `index.md` 子行改为页面 markdown row 下挂 mindmap asset`rowKind=index` 仍只作为历史 projection / 命令兼容解析保留,不再作为默认 Convex File Tree UI 口径。
- 旧设计中的“可见 `index.md`”表述由本稿覆盖:后续 UI / smoke / review 应以 `doc:<documentId>` + `{title}.md` 作为 Convex 页面正文 row;如果文档讨论历史 `index.md`,必须明确它是兼容身份或历史记录,不是默认可见行。
仍保留的后续增强:
- 删除进垃圾箱、restore 后 reveal、双浏览器 resync、搜索过滤下 reveal 等更宽场景继续由 `4-28``4-34` 与后续 tree domain checklist 覆盖。
@@ -1,174 +0,0 @@
# 4-5 [done] Tree Command Envelope 第一批收口方案 v1
> 更新时间:2026-04-17
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
## 1. 文档目标
这份文档用于固定 Stage B-2 的真实边界:
- 前端已经不应该再扩散树命令语义。
- 但当前仓库里,浏览器组件层仍然散落着大量 `fetch("/api/documents/...")` 入口。
- 第一批要先收口 `create / rename / move`,把前端限制为 optimistic UI 与用户交互壳。
这里的核心口径是:
> 前端只保留 optimistic UI,真语义和 command envelope 收口到 Rust/bridge。
## 2. 当前命令入口盘点
### 2.1 Sidebar 主入口
当前树命令入口主要集中在:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
其中:
- `sidebar.tsx`
- `create``/api/documents/create`
- `rename``/api/documents/title`
- `move``/api/documents/move`
- `delete``/api/documents/delete`
- `restore``/api/documents/restore`
- `purge``/api/documents/purge`
- `embed``/api/documents/embed`
- `copy-tree``/api/documents/copy-tree`
- `document-content.tsx`
- 文档标题更新走 `/api/documents/title`
- 移动页走 `/api/documents/move`
- 页面嵌入走 `/api/documents/embed`
- `CustomSideMenu.tsx`
- block 转子页面走 `/api/documents/create-child`
- `pageReference` 删除走 `/api/documents/delete`
### 2.2 当前哪些 route 已接到 envelope
第一批最关键的三条主路径,已经接入 `buildDocumentCommandEnvelope`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
- 命令名:`documents.create`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
- 命令名:`documents.title.update`
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
- 命令名:`documents.move`
当前真实执行链路是:
1. Next route 构造 `buildDocumentCommandEnvelope`
2. `resolveRustBridgeCommandPlan`
3. TS runtime 负责 transport dispatch
4. source-specific executor 负责实际持久化;Convex mutation 仅属于历史 / cloud / compat source
因此当前形态是:
- `WorkspaceSource substrate`Convex 仅为历史 / cloud / compat source
- `Rust semantic owner`
- `TS runtime transport dispatcher`
不是“所有 source 都由同一个前端 route 自行落库”;默认 local-first source 已继续演进为 Rust / LocalFS / SQLite control-plane。
### 2.3 当前仍留在前端的树语义
虽然 route 已接入 envelope,但浏览器端仍残留较多树语义:
- `sidebar.tsx`
- 本地 `insertNode` / `moveLocalNode`
- 非法拖拽判断
- `targetParentId` / `position` 推导
- `copy-tree` 的递归策略与拖放目标推导
- `document-content.tsx`
- 移动页时仍由前端给出固定 `position`
- `CustomSideMenu.tsx`
- block 转子页面的 UI 级决策仍在前端
这说明当前主问题已经不是 route 是否接 bridge,而是:
> 命令 transport 已经收口,但命令入口和部分树策略仍散落在前端组件层。
## 3. 第一批 cutover 范围
Stage B-2 第一批只处理:
- `create`
- `rename`
- `move`
原因:
- 这三条覆盖 sidebar 主交互、文档页主交互、block 转页入口。
- 它们已经具备稳定的 route -> envelope -> Rust bridge -> Convex 主链。
- 继续向前推进时,前端只需要保留 optimistic UI 和错误提示,不需要继续扩散 transport 细节。
## 4. 第一批 cutover 顺序
推荐固定为两段:
### 4.1 第一段:先 `create / rename / move`
目标:
- 浏览器组件不再直接散落 `fetch("/api/documents/create|title|move")`
- 收成共享的 tree command client
-`sidebar.tsx``document-content.tsx``CustomSideMenu.tsx` 只表达交互和 optimistic UI
这一步已经开始落地:
- 新增共享入口:
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`
- 第一批已接入的组件:
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
### 4.2 第二段:再 `delete / restore / purge / embed`
原因:
- `delete / restore / purge` 已接 envelope,但仍有多处页面级分叉入口。
- `embed` 当前最特殊,它不是独立 `documents.embed` 命令,而是 TS adapter 里拼 `pageReference` 后走 `documents.save`
- `embed` 必须等 Rust kernel 定义正式页面嵌入语义后,再从“TS adapter 语义”切成“Rust tree command 语义”。
## 5. 不纳入第一批的内容
以下内容先不纳入 Stage B-2 第一批:
- `copy-tree`
- 当前仍强依赖前端拖放和递归策略
- block 级 `blocks.move / blocks.embed`
- 这是块域命令,不是页面树命令主链
- 树排序算法本身
- 当前仍有一部分位置推导在前端,后续要继续收回 Rust kernel
## 6. 长期边界说明
长期固定如下:
- 前端负责:
- 用户交互
- optimistic UI
- 选择器、确认框、拖拽体验
- Next route / shared client 负责:
- 稳定 transport 边界
- 请求格式统一
- Rust kernel / bridge 负责:
- `tree command` 语义
- `page_tree / sidebar_tree / file_tree` 真相
- 审计、trace、版本口径
- Convex 负责:
- 持久化
- 订阅
- 实时底座
## 7. 下一步建议
Stage B-2 后续应继续做三件事:
1.`delete / restore / purge` 也收成同一层 shared tree command client。
2.`embed` 定义独立的 Rust page-tree 命令,而不是继续停留在 TS adapter + `documents.save`
3.`targetParentId / sortOrder / subtree move legality` 等策略继续从前端移回 Rust kernel。
@@ -1,75 +0,0 @@
# 4-8 [done] Tree Shell Cutover Performance Report v1
> 更新时间:2026-04-18
>
> 测试环境:
> - 前端主站:`http://127.0.0.1:3000`
> - Rust Web`http://127.0.0.1:3104`
> - 浏览器:Playwright Chromium headless
> - 数据口径:同一工作区下真实 Sidebar + 临时父/子页面样本 + move/embed picker 对话框
## 1. 测量口径
- `首包`
- 指主树或 picker 打开后,到对应 surface 首次可见的时间。
- `首次可交互`
- 指 surface 已出现,且首个可点击行/按钮已可交互。
- `切页延迟`
- 指点击页面树或文件树行,到浏览器 URL 切到目标页面的时间。
- `大树展开延迟`
- 指点击页面树真实可展开节点的展开/收起往返显隐完成时间。
- `fallback`
-`/api/stream/events` 被阻断或失败时,Sidebar 仍能在主站内回到可见可用树视图的结果。
## 2. 实测结果
### 2.1 首页与切流 smoke
- `task097-homepage-entry-smoke.js`
- `/auth` 返回 `200 text/html`
- `/` 返回 `307 -> /auth`
- `3104 /health` 返回 `200`
### 2.2 Sidebar 主树 cutover
- `task101-tree-shell-cutover-smoke.js`
- `首包 / 首次可交互``9ms`
- `切页延迟``205ms`
- `stream``/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=...`
- `fallback`:阻断 stream 后仍在 `22ms` 内显示可用主树
### 2.3 filetree / picker 回归
- `task102-tree-shell-regression.js`
- `filetree 首包 / 首次可交互``6ms`
- `filetree 切页延迟``179ms`
- `picker 首包 / 首次可交互``264ms`
- `picker 搜索输入后结果/空态稳定出现``1005ms`
### 2.4 页面树展开样本
- 真实可展开节点往返展开/收起延迟:`124ms`
## 3. 对照与结论
- 旧 iframe/postMessage 实验壳已退出 Sidebar / filetree / picker 主路径。
- 当前默认主路径已经是 Next 内统一 tree surface + stream/projection 主链。
- `3104` 与 Convex 连接正常时,主树按 stream 路径工作;`3104`/stream 失败时,主树仍能通过 fallback 保持可见。
- 当前开发机口径下,主路径交互均在亚秒级,未再出现此前 `3000` 因 Convex/tree shell 卡死而无响应的问题。
## 4. fallback 结果
- `fallback` 触发条件:
- 主动阻断 `/api/stream/events`
- 观测结果:
- 页面不空白
- Sidebar 主树仍渲染
- 可继续看到目标页面树节点
- 结论:
- `fallback` 已从“实验性兜底”变成正式切流护栏
## 5. 残余风险
- 当前指标来自本地开发环境与小样本 smoke,不代表生产环境大规模树深/大附件工作区的长期 p95。
- picker 搜索链路仍受搜索索引可见性影响;本轮已验证“搜索后结果或空态稳定出现”,但未把索引收敛时间当作树域切流 blocker。
- 后续如继续增强 Rust-first renderer,可在不改变当前 cutover 结论的前提下,把这份报告升级为多样本趋势报表。