Files
mnote/design/04-tree-domain/done/4-22-local-folder-convex-unified-tree-execution-checklist-v1.md
T
lix-2026 f9336257a5 chore(tree): remove legacy iframe tree shell
Remove the active TreeShellIframeHost path and legacy env flag so rust_family only mounts the Rust/WASM DOM shell host.

Move legacy iframe host files to ignored recycle storage, clear tracked recycle cache entries, and update tree-domain design wording to the current no-iframe position.

Align filetree default active rows with doc:<documentId> and update the renderer artifact bridge contract to dom_wasm.
2026-05-16 08:57:40 +08:00

26 KiB
Raw Blame History

4-22 [done] Local Folder / Convex 统一树源顺序执行清单 v1

更新时间:2026-05-06

来源:

  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-20-vscode-explorer-file-tree-alignment-v1.md
  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-21-local-folder-convex-unified-tree-source-v1.md
  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md
  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md
  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md
  • /mnt/Data1T/mnote/design/04-tree-domain/done/4-7-tree-shell-ui-state-boundary-v1.md
  • /mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md
  • /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode

状态说明:本清单对应的 Local Folder / Convex 统一树源主链已进入已完成基线;后续 Markdown parser、Page Aggregate Rust-first、tree live stream 与设计状态漂移由后续稿继续跟踪。

0. 执行原则

本清单按顺序执行,不跳阶段。

总目标:

只维护一套 tree projection、tree command、preflight、runtime state 和 UILocal Folder 与 Convex 只作为不同 WorkspaceSource executor 存在。

硬约束:

  • 不让本地文件夹先导入 Convex 再渲染。
  • 不在 UI 层用本地 path 或 Convex record 拼第二套树。
  • 不新增 local 专用文件树 UI。
  • 不绕过 Rust kernel 写页面树、文件树、排序或权限。
  • 不把 expanded/selected/focus/drag/drop/contextMenu 写进 projection。
  • 所有写操作先 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 合同

  • 在 Rust 侧定义 WorkspaceSource trait / interface 草案。
  • 字段至少覆盖 sourceKindrootUriworkspaceIdcapabilities
  • 方法至少覆盖 loadSnapshotwatchpreflightCommandexecuteCommandresolvePageAggregate
  • 明确 local_folderconvex_workspace 两个 source kind。
  • 写 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

  • 为 projection item 补 source metadata 方案。
  • 字段候选固定为 sourceKindsourceUrirelativePathstorageIdentityoperationProfile
  • 优先放入 resourceMeta.extra,除非需要正式扩展 core-protocol
  • 更新 TS 类型映射。
  • 补 fixturelocal folder item、convex item 同时存在。

Done 条件:

  • file_tree item 可以区分来自 local 还是 convex。
  • UI 仍只读 projection 字段,不直接访问 source adapter。
  • fixture 覆盖 directory/md file/normal file/document/index/asset

A3. 统一 command envelope

  • 定义 TreeCommandEnvelope 的 source 字段。
  • envelope 至少包含 sourceKindworkspaceIdrootUritargetNodeIdtargetResourceMetaselectionoperation
  • 现有 tree.node.*tree.subtree.*tree.resource.* 进入统一 router。
  • router 按 source kind 分发到 executor。
  • 对 unknown source / capability missing 返回可解释错误。

Done 条件:

  • 同一个 UI action 可以构造同一种 envelope。
  • local / convex 的 executor 可以在测试中分别接收 envelope。
  • 没有新增 /api/local-filetree/* 这类平行命令入口。

2. Phase BLocal Folder 只读主链

B1. 打开本地文件夹入口

  • 增加选择本地文件夹的入口。
  • 生成 SpaceRoot=file://...
  • 记录最近打开的 local root。
  • 禁止 root 外路径访问。
  • 对无权限目录返回明确错误。

Done 条件:

  • 用户能选择一个本地文件夹并进入 local workspace。
  • 3000 desktop:hot 主入口不能跳到 /tree debug shell,也不能落入 legacy_next_compat_disabled
  • Convex workspace 入口不受影响。
  • smoke 能断言当前 source kind 为 local_folder

B2. LocalFolderSource 只读扫描

  • 扫描目录。
  • 识别 directory。
  • 识别 .md 文件。
  • 识别普通文件。
  • 默认忽略 .gitnode_modules.mnote/trash 等高噪音目录。
  • 处理 symlink:P0 只显示,不默认跟随写入。

Done 条件:

  • 本地文件夹能输出 read-only snapshot。
  • snapshot 不越过 root。
  • 扫描失败不会让整个 app 崩溃。

B3. 输出 local file_tree

  • directory 映射为 folder row。
  • .md 映射为 markdown file row。
  • 普通文件映射为 asset row。
  • iconHint 覆盖 markdown/image/video/audio/pdf/book/table/unknown。
  • childCount、depth、parentNodeId、position 由 kernel 生成。
  • 排序规则先固定为 VSCode defaultfolder firstname sort。

Done 条件:

  • kernel.project_view(file_tree) 可以返回 local folder projection。
  • UI 使用同一 Explorer surface 渲染 local projection。
  • 浏览器 smoke 可看到真实目录、.md、普通文件。

B4. 输出 md-only page_tree

  • 只把 .md 文件纳入 page tree。
  • 目录只作为页面树分组/父级,不作为正文页面打开。
  • 页面标题优先级固定为 frontmatter title > 第一行 H1 > 文件名。
  • parent/child 来自目录层级。
  • 同名 md 文件必须通过 path / page id 区分。

Done 条件:

  • 同一个 local root 下,Explorer 显示所有文件,页面树只显示 .md
  • 点击页面树 md 节点能定位到同一个 .md resource。
  • 不因过滤普通文件影响 page tree 层级。

B5. .md 只读 Page Aggregate

  • 读取 .md 文件。
  • 解析 frontmatter。
  • 解析标题。
  • 生成只读 page_aggregate
  • 文档页用现有 DocumentShell / DocumentContent 消费。
  • 文件读取失败显示错误页,不清空 Explorer。

Done 条件:

  • 点击本地 .md 文件能打开文档阅读态。
  • 文档页不关心内容来自 local 还是 Convex。
  • page_aggregate 字段满足 5-5-1 最小契约。

3. Phase C:页面身份与本地元数据

C1. 本地 page id 策略

  • 支持 frontmatter mnote_id
  • 支持 .mnote/page-ids.json
  • 支持 path-derived 临时 id,但仅用于未初始化只读模式。
  • page id 生成必须稳定且不与 Convex document id 冲突。
  • 写测试覆盖 rename/move 后 id 保持。

Done 条件:

  • 重命名或移动 .md 后,page id 不变。
  • 没有 id 的文件能被安全初始化。
  • 同名不同路径 .md 不冲突。

C2. 本地 .mnote 元数据目录

  • 定义 .mnote/page-ids.json
  • 定义 .mnote/page-options.json
  • 定义 .mnote/trash 或系统 trash 策略。
  • .mnote 不出现在默认 Explorer 展示中,除非开启 debug。
  • 元数据写入必须原子化或有失败回滚策略。

Done 条件:

  • .mnote 元数据能支持 page id 和页面设置。
  • 默认用户视图不被 .mnote 噪音污染。
  • 元数据损坏时有可解释错误和恢复建议。

4. Phase D:统一 Explorer P0 交互

D1. Active / Selection / Focus 分离

  • 统一 row stateactiveRowIdselectedRowIdsfocusedRowId
  • active 表示右侧打开资源。
  • selection 表示 Explorer 操作集合。
  • focus 表示键盘焦点。
  • local 和 convex 共用 reducer。
  • 多选时 selection 视觉优先于 active。

Done 条件:

  • 截图中的 index.md active/focus/selection 歧义被测试覆盖。
  • local .md 和 Convex index.md 使用同一视觉规则。
  • selection 不回写 projection。

D2. 键盘导航

  • ArrowUp/ArrowDown 移动 focus。
  • ArrowLeft/ArrowRight 折叠 / 展开 / 父子导航。
  • Home/End 跳首尾。
  • Enter 打开 focused row。
  • F2 进入 inline rename。
  • Delete/Backspace 触发删除 preflight。
  • Ctrl/Cmd+C copy。
  • Ctrl/Cmd+X cut。
  • Ctrl/Cmd+V paste。
  • Esc 取消 rename / menu / drag feedback。

Done 条件:

  • 键盘能力在 runtime contract 中测试,不只散落在 React handler。
  • local 和 convex 使用同一组按键浏览器 smoke。
  • cut 不再缺失。

D3. 选择与右键

  • 单击单选并设置 anchor/focus。
  • Ctrl/Cmd + Click 切换选中。
  • Shift + Click 范围选。
  • 右键已选区内 row 保留选区。
  • 右键未选 row 切换到该 row。
  • 空白区点击清空 selection/focus。

Done 条件:

  • 多选 context menu 不会被右键误清空。
  • local/convex 的 selection reducer 共用。
  • 过滤可见行变化后 selection normalize 正确。
  • 已选区内右键、未选项右键、空白区点击三条路径都必须在真实浏览器中验证。

D4. Row grid 与 root toolbar

  • 固定 indent / twistie / icon / label / decoration / actions 六列。
  • section header 使用 EXPLORER
  • root row 明确显示 local folder 或 cloud workspace。
  • toolbar 至少有新建、上传/导入、刷新、全部折叠、更多。
  • 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

  • root 菜单:新建页面、新建文件夹、上传/导入、刷新、全部折叠。
  • md/page 菜单:打开、重命名、复制、剪切、粘贴、移动到、删除、Reveal。
  • folder 菜单:新建页面、新建文件夹、粘贴、重命名、删除。
  • asset 菜单:打开、重命名、复制、剪切、删除、下载、移动到。
  • 多选菜单:复制、剪切、删除、移动到。
  • share/publish 只在 Convex capability 存在时出现。

Done 条件:

  • 菜单项由 rowKind/resourceKind/capability/selection 共同裁剪。
  • 不支持的能力有禁用原因或隐藏规则。
  • mindmap/table 的“不支持”状态被 capability 表达,而不是散落提示。

E2. Preflight result UI

  • delete preflight 展示影响集合。
  • move preflight 展示目标和冲突。
  • paste preflight 展示命名冲突策略。
  • upload/drop preflight 展示目标是否可写。
  • readonly / permission denied / root escape 返回统一错误结构。

Done 条件:

  • local 和 convex 的 preflight 使用同一 UI 组件展示。
  • 失败时不发 execute。
  • 危险动作不会静默执行。

6. Phase FLocal 写操作 P1

F1. 新建

  • tree.node.create 在 local source 创建 .md
  • tree.folder.create 或等价命令创建目录。
  • 新建后进入 inline rename。
  • 命名冲突走 incremental naming 或 preflight 提示。
  • Convex 对 New Folder 按 capability 禁用,除非已有 folder resource。

Done 条件:

  • 同一个新建入口可在 local 创建文件,在 Convex 创建 document。
  • Convex 不支持 folder 时不会显示假入口。
  • 新建结果通过 projection resync 出现。

F2. Inline rename

  • 本地文件 / 目录 rename 写入文件系统。
  • .md rename 保持 page id。
  • Convex document rename 继续走 tree.node.rename
  • asset rename 收口到 tree.resource.rename 或等价正式命令。
  • Enter 提交,Esc 取消。
  • 文件型资源默认选中文件名 stem。

Done 条件:

  • 不再使用 prompt 做重命名。
  • local 和 convex 的 rename UI 完全一致。
  • rename 失败不破坏 selection/focus。

F3. Move / Copy / Cut / Paste

  • local move/copy 使用文件系统操作。
  • Convex move/copy 使用 existing tree/resource command。
  • cut 状态有 decoration。
  • paste 成功后清空 cut。
  • 多选时过滤父子重复选择。
  • 禁止 move 到自身或后代。

Done 条件:

  • local/convex 都通过同一 clipboard payload。
  • cut/copy/paste 浏览器 smoke 覆盖两个 source。
  • projection resync 后 selection/focus 合理保留。

F4. Delete / Restore / Purge

  • local delete 进入 .mnote/trash 或系统 trash,策略需冻结。
  • local restore 从 trash 恢复。
  • local purge 永久删除。
  • Convex delete/restore/purge 继续走 tree.node.* / tree.resource.*
  • asset delete/restore 从 legacy /api/media/batch 收口。

Done 条件:

  • 删除不是直接 rm 静默执行。
  • restore 在 local 和 Convex 都有统一菜单语义。
  • purge 有更强确认。

F5. Drop 外部文件

  • local drop 文件复制进目标目录。
  • Convex drop 文件上传对象存储。
  • drop 到 readonly 目标被拒绝。
  • drop 到 md 文件时目标归属规则明确。
  • 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 外部 dropscripts/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 覆盖 uploadFileTreeResourceAssetapp/api/media/upload/route.test.ts 覆盖 tree.resource.upload plan、Convex upload URL、mediaAssets:createWithStorage 与资源 delta artifact。
  • indicator:同一 smoke 覆盖本地目录 before/inside/after、hover auto expand,以及 Convex document before/inside/after 与 asset-folder inside。

7. Phase GWatcher / Realtime / Refresh

G1. Local file watcher

  • 监听 root 下文件新增、删除、重命名、修改。
  • 忽略 .git/node_modules/.mnote/trash 等目录。
  • 防抖合并事件。
  • source 输出 tree delta 或触发 snapshot refresh。
  • 写操作自身产生的 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

  • Convex 继续使用现有 tree stream。
  • preferred snapshot 选择不因 local source 破坏。
  • local source 和 Convex source 都能进入统一 tree event consumer。
  • source 切换时清理上一 source 的 watcher / stream。

Done 条件:

  • local 和 convex 的实时刷新状态都可观测。
  • 切换 workspace 不出现旧 source 事件污染新 workspace。
  • 当前 Convex smoke 不回退。

验收记录:

  • Convex 侧继续由 useSidebarTreeStream / tree stream server 处理 realtimepnpm vitest run src/lib/tree-stream/use-sidebar-tree-stream.test.tsx src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/server.test.ts 通过。
  • local 侧通过同一 tree shell source metadata 和 revision watcher 触发 snapshot refresh,不引入第二套 UI。
  • 真实浏览器 smoke 覆盖 local -> Convex source 切换,断言旧 local watcher 内容和 selection 不污染 Convex file tree。

8. Phase HMarkdown 编辑与写回 P2

H1. Markdown AST 到编辑器文档

  • Markdown parse 成 editor document。
  • frontmatter 保留。
  • 基础块覆盖 paragraph / heading / list / quote / code / divider。
  • 附件链接解析为 resource reference。
  • 不支持块有降级显示。

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

  • 保存正文写回 .md
  • 标题编辑规则冻结:文件名 / H1 / frontmatter 如何同步。
  • 页面设置写入 frontmatter 或 .mnote/page-options.json
  • 保存失败保留编辑器状态并显示错误。
  • watcher 与保存事件去重。

Done 条件:

  • 修改本地 .md 后刷新仍能看到新内容。
  • 文件名、H1、frontmatter title 不互相打架。
  • Convex 编辑链路不受本地写回影响。

验收记录:

  • 正文保存通过 /api/documents/save 写回 .md,标题编辑通过 /api/documents/title 只写 frontmatter title,文件名变更仍由 Explorer rename 负责;三者职责分离。
  • 页面设置写入 .mnote/page-options.json,不混入正文。
  • cargo test -p mnote-web local_ -- --nocapture 覆盖本地正文、标题和页面设置写回;local_markdown_save_preserves_frontmatter_and_writes_basic_blocks 覆盖 frontmatter 保留与基础块 Markdown 输出。
  • 真实浏览器 smoke 通过 document APIs 保存本地 Markdown,刷新后仍能看到新标题和正文;revision watcher 只在本地 root revision 变化后防抖刷新,避免固定刷新风暴。

9. Phase ILocal / Cloud 显式桥接 P3

I1. Import / Export

  • local folder import 到 cloud workspace。
  • cloud workspace export 到 local folder。
  • 冲突策略明确。
  • page id 映射保留。
  • 附件路径和对象存储映射保留。

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.jsoncloud_document_id.mnote/cloud-export-map.json 保留。
  • 附件路径和对象存储 id 必须进入 export/import mapexecute 失败必须返回 retry cursor 和 rollback plan。

I2. Publish / Sync 候选

  • local page publish 到 Convex public page。
  • 手动 sync 候选设计。
  • 不在 P0/P1 引入隐式双向同步。
  • 权限模型不混淆:local OS 权限不同于 Convex ACL。

Done 条件:

  • 本地 workspace 不依赖 Convex 才能工作。
  • Cloud 功能通过显式桥接启用。
  • 没有自动双真相。

验收记录:

  • 4-23 固定 publish 为 tree.bridge.publish.preview/execute,只发布显式选择的单页或子树。
  • sync 只进入 tree.bridge.sync.preview 候选,不提供默认 sync.execute,避免形成自动双真相。
  • local OS 权限与 Convex ACL 在 preview 中必须单独展示,不互相推导。
  • 当前实现中的 local folder 读写、watcher、Markdown 保存均不依赖 ConvexConvex 能力只通过显式 bridge 候选启用。

10. 全局验收矩阵

10.1 Contract tests

  • WorkspaceSource mock local / mock convex。
  • command envelope source routing。
  • projection source metadata。
  • capability-driven menu profile。
  • preflight result schema。
  • active / selection / focus reducer。
  • cut / copy / paste reducer。

Done 条件:

  • 不启动浏览器也能验证核心协议。
  • local/convex 至少各有一套同命令不同 executor 的测试。
  • Contract tests 只负责协议、schema、reducer、payload 与 executor 分发,不替代拖拽、右键、多选、外部 drop、source 切换的真实浏览器验收。

10.2 Rust tests

  • local folder scan。
  • md-only page_tree。
  • file_tree folder first sorting。
  • page id persistence。
  • root escape 防护。
  • local write executor。
  • Convex executor regression。

Done 条件:

  • Rust 测试覆盖 source adapter 主路径。
  • 安全边界有失败用例。
  • Convex 旧路径没有被本地 source 破坏。

10.3 Browser smoke

  • 打开 local folder。
  • 3000 desktop:hot 主入口覆盖打开本地文件夹:不触发原生 prompt,不跳 /tree,不落入 legacy_next_compat_disabled
  • Explorer 显示目录、md、普通文件。
  • 页面树只显示 md。
  • 打开 md 到文档页。
  • 同一套右键菜单在 local / convex 按 capability 裁剪。
  • 已选区内右键保留多选,未选项右键切换 selection 后开菜单,空白区点击清空 selection / focus。
  • Ctrl/Cmd+ClickShift+ClickCtrl/Cmd+A 多选链路。
  • 3000 主 Sidebar file tree 的 Shift+Click 范围多选由 scripts/task164-desktop-hot-local-folder-main-entry-smoke.js 验收。
  • inline rename。
  • Ctrl/Cmd+C/X/V 与菜单 Copy / Cut / Paste 都覆盖,cut decoration 出现,paste 成功后消失。
  • 内部拖拽覆盖单选、多选、move / copy 修饰键切换。
  • 内部拖拽覆盖 before / inside / after 三态 indicator。
  • 内部拖拽覆盖 hover auto expand。
  • 内部拖拽覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法场景,并断言不会发 execute。
  • 外部文件 drop 使用真实 DataTransfer/File 或等价 Playwright 文件拖入,覆盖 local folder、Convex document/asset-folder、readonly 目标、md 文件目标。
  • 外部文件 drop 覆盖 preflight UI、写入/上传结果、projection refresh。
  • watcher 外部修改刷新。
  • 切回 Convex workspace 后原有页面树正常。
  • source 切换后旧 watcher / stream 已清理,selection / focus / active 不被旧 source 污染。
  • local 与 Convex 上同一动作保持同一 UI 和同一命令名,但 executor 行为可不同。
  • 组合场景覆盖多同名 新页面、每页下 index.md、多选、右键、F2Ctrl/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/

  • WorkspaceSource 抽象落地。
  • LocalFolderSource 支持本地只读 file_tree/page_tree/page_aggregate
  • ConvexWorkspaceSource 继续通过同一 projection/command 消费路径工作。
  • Explorer P0 交互完成:active/selection/focus、keyboard、context menu、inline rename、cut/copy/paste、drag/drop。
  • 至少一组写操作在 local 和 convex 上共用 UI 和 command envelope。
  • 本地文件夹不经过 Convex 中介。
  • page id 不因本地 rename/move 断裂。
  • UI 没有 source-specific tree fork。
  • 所有危险写操作都有 preflight。
  • 浏览器 smoke 和 Rust/TS contract tests 通过。
  • 3000 desktop:hot 主入口 smoke 通过,覆盖本地文件夹入口、legacy fallback 拒绝和主 Sidebar Shift+Click 多选。
  • 拖拽、外部 drop、右键、多选、键盘、source 切换的关键路径在真实浏览器中逐项验收通过;这些 hard gate 未通过时,即使 contract/Rust 测试通过也不得移动到 done/

12. 明确非目标

  • 不在 P0 实现本地和云端自动同步。
  • 不在 P0 承诺 Markdown 与编辑器完全无损互转。
  • 不用本地 per-user 文件夹替代 Convex 协作权限。
  • 不把 VSCode 源码作为直接复制对象;只参考 Explorer 行为模型。
  • 不把 Convex 虚拟空间强行伪装成真实磁盘目录。
  • 不把 Local Folder 模式做成必须登录才能使用。