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.
593 lines
26 KiB
Markdown
593 lines
26 KiB
Markdown
# [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 模式做成必须登录才能使用。
|