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:
+330
@@ -0,0 +1,330 @@
|
||||
# [recycle] 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 plan,3000 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 F:final 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 HTML;runtime 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` artifact;active 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 model(focused id normalize / next / previous / home / end)
|
||||
- [x] selection model(filetree selection state family)
|
||||
- [x] picker state model(高亮 / Enter / exclude)
|
||||
- [x] keyboard model(导航 / 打开 / expand-collapse / context menu intent)
|
||||
- [x] drag/drop model(payload normalize / copy-move effect)
|
||||
- [x] action registry(page / filetree / picker action set)
|
||||
- [x] runtime facade(`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API,输出统一 DOM patch / host event / command event)
|
||||
- [x] `page tree` 先切 final renderer。
|
||||
- 当前状态:默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOM,focus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。
|
||||
- [x] `picker` 以轻量模式复用同一 renderer state family。
|
||||
- 当前状态:默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOM,keyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动,测试已禁止默认 iframe postMessage 成功路径。
|
||||
- [x] `file tree` 后切 final renderer,并保留 `doc / index / asset-folder / asset` 能力。
|
||||
- 当前状态:默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOM,selection、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 renderer;host 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 H:Rust 搜索 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 I:move 排序执行下沉
|
||||
|
||||
### 目标
|
||||
|
||||
把 `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 schema(TS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1)
|
||||
- [x] Rust 侧由 command plan 生成 delta / event hint,Next 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 继续扩写长期树命令语义。
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# [recycle] 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 验证边界。
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# [recycle] 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-26:3000 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`。
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# [recycle] 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-26:task-005 完整验证通过,未发现需要新增生产改动的缺口;本清单作为能力门槛记录。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本清单不拆除 compat host 的所有 debug fallback;对应 `task-006`。
|
||||
- 本清单不新增业务命令语义;file tree drop/delete/paste/upload preflight 已在前置任务中下沉 Rust。
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# [recycle] 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__` 注入第二份主路径 items;page/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 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
# [recycle] 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 facade:page / 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 seam:hydrated 与 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 artifact,HTTP `/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 与 blocks,snapshot 版本/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 pick,HTTP 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 L:tree.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 M:block/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 N:3000 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 重新暴露为默认前端入口。
|
||||
- 不在前端新增第二套树结构真相或排序真相。
|
||||
@@ -0,0 +1,72 @@
|
||||
# [recycle] 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。
|
||||
@@ -0,0 +1,174 @@
|
||||
# [recycle] 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。
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# [recycle] 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 / drop,filetree 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 host;legacy 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 artifact,HTTP 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。
|
||||
@@ -0,0 +1,588 @@
|
||||
# [recycle] 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。
|
||||
- 本地全文索引。
|
||||
|
||||
### P3:Local 与 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 或制造第二套树真相。
|
||||
+592
@@ -0,0 +1,592 @@
|
||||
# [recycle] 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 和 UI;Local 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] 补 fixture:local 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 B:Local 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 default:folder first,name 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 F:Local 写操作 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 G:Watcher / 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 H:Markdown 编辑与写回 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 I:Local / 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 map;execute 失败必须返回 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 保存均不依赖 Convex;Convex 能力只通过显式 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 模式做成必须登录才能使用。
|
||||
@@ -0,0 +1,237 @@
|
||||
# [recycle] 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 兼容,减少一次性改动。
|
||||
|
||||
### P1:Active / reveal / object identity 迁移
|
||||
|
||||
- active page body 映射到 `{title}.md` row。
|
||||
- mindmap asset 打开后保持 asset active,不降级为页面正文 row。
|
||||
- 长列表 reveal 稳定。
|
||||
|
||||
### P2:Rename / 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 覆盖。
|
||||
@@ -0,0 +1,174 @@
|
||||
# [recycle] 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。
|
||||
@@ -0,0 +1,75 @@
|
||||
# [recycle] 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 结论的前提下,把这份报告升级为多样本趋势报表。
|
||||
Reference in New Issue
Block a user