checkpoint before gfm ast parser design

This commit is contained in:
lix-2026
2026-05-08 00:41:03 +08:00
parent e8ba12e461
commit c620b9e40c
41 changed files with 12270 additions and 263 deletions
@@ -1,15 +1,25 @@
# 3-10 [done] Rust Web 3000 Tree / Editor Runtime Integration Checklist v1
> 更新时间:2026-04-29
>
> 纠偏补充(2026-04-29):用户复核发现此前“完成”口径只覆盖 API / 直达文档页 smoke,没有覆盖默认根页真实 UI 工作流。默认 `3000 /` 重新接入既有 Rust page tree / file tree renderer 与 UI 新建到 `leptos-tiptap` 的闭环,详见 `/mnt/Data1T/mnote/design/03-rust-web/done/3-11-rust-web-3000-existing-tree-editor-runtime-relink-checklist-v1.md`。
>
> 对应计划:
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-9-rust-web-3000-tree-editor-runtime-integration-plan-v1.md`
>
> 参考输入:
> - 当前 3000 截图:`/mnt/Data1T/mnote/tmp/image copy 32.png`
> - 当前 shell parity checklist`/mnt/Data1T/mnote/design/03-rust-web/done/3-8-rust-web-3000-wolai-ui-parity-checklist-v1.md`
> - Page Aggregate 对齐主线:`/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
## 1. 当前结论
@@ -17,17 +27,25 @@
当前已经完成的是:
- [x] `3000` 默认由 `mnote-web` 监听。
- [x] `3000` 根页已经有 Wolai 风格 workspace shell。
- [x] 根页包含左栏、顶栏、右下浮动按钮和 `data-mnote-shell="workspace"`
- [x] `/documents/:id` 已有 Rust Web document shell、Page Aggregate marker 和 `data-editor-host="leptos_tiptap_island"` marker。
[x] `/documents/:id` 已有 Rust Web document shell、Page Aggregate marker 和 `data-editor-host="leptos_tiptap_island"` marker。
当前没有完成的是:
- [x] 左栏 `我的页面` 显示真实 file/page tree rows,而不是空 section。
- [x] 根页正文不再使用设计稿静态链接列表作为伪主内容。
- [x] active page selection 能从 projection / recent / URL 一致落到 sidebar 和 document shell。
- [x] `/documents/:id` 浏览器运行态能挂载 `data-testid="mnote-leptos-tiptap-island-editor-root"`
- [x] `.ProseMirror[contenteditable="true"]` 可见、可输入、可保存、reload 后可读回。
- [x] smoke 能区分“只有外壳 UI”与“真实树和编辑器已接入”。
一句话判断:
@@ -37,10 +55,15 @@
## 2. 不可误判为完成的情况
- [x] 只看到 `data-testid="wolai-sidebar"`,不算页面树接入。
- [x] 只看到 `我的页面` 文案,不算真实树接入。
- [x] 只看到正文静态链接列表,不算 Page Aggregate 主内容接入。
- [x] 只看到 `data-editor-host="leptos_tiptap_island"`,不算 editor island 已 hydrate。
- [x] 只看到 `#mnote-editor-island` 空占位,不算主编辑区完成。
- [x] 只在 fixture gateway 下通过,不算当前真实 `3000` 主链完成。
## 3. Phase E-0:冻结真实树和编辑器失败用例
@@ -48,24 +71,38 @@
目标:让当前截图暴露的问题可以自动失败。
- [x] 新增 `scripts/task120-rust-web-tree-integration-smoke.js`
- [x] `task120` 通过 `POST /api/tree/commands action=create` 创建临时根页面。
- [x] `task120` 创建临时子页面并记录 `workspaceId` / `documentId`
- [x] `task120` 断言 `GET /``/documents/:id` 的左栏包含 `data-testid="wolai-sidebar-row"`
- [x] `task120` 断言临时页面 row 的 `data-node-id` 等于真实 document id。
- [x] `task120` 断言 active row 有 `data-active="true"` 或等价稳定标记。
- [x] `task120` 点击 row 后能打开 Rust Web `/documents/:id`
- [x] `task120``purge` 清理临时页面。
- [x] 新增 `scripts/task121-rust-web-editor-island-hydration-smoke.js`
- [x] `task121` 断言 `data-testid="mnote-leptos-tiptap-island-editor-root"` 可见。
- [x] `task121` 断言 `.editor-surface .ProseMirror[contenteditable="true"]` 可见。
- [x] `task121` 输入唯一文本并触发保存。
- [x] `task121` reload 后仍能读回唯一文本。
- [x] `task121` 拉取 `/api/page-aggregate/:id` 后能读回保存内容或版本变化。
- [x] `task119` 增加负向断言,不能只靠 `.mnote-home-links` 静态列表通过。
验证命令:
```bash
```
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task120-rust-web-tree-integration-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js
@@ -76,17 +113,24 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-isl
目标:左栏 `我的页面` 不再是空 section,而是 projection 驱动的真实页面树。
- [x] `WorkspaceShellProjection.myPageItems` 来源于 `sidebar_tree` / `sidebar.dataset.list` / kernel projection。
- [x] `WorkspaceShellProjection.starredItems` 来源于真实 projection,不硬编码。
- [x] `render_workspace_shell_sidebar_html` 输出 `data-testid="wolai-sidebar-row"`
- [x] 每个 row 输出 `data-node-id`
- [x] 子页面 row 输出 `data-parent-id` 或等价层级标记。
- [x] active row 输出 `data-active="true"`
- [x] 空 workspace 输出明确 empty state,不回到设计稿静态链接列表。
- [x] Cargo 测试覆盖 root/child/active/empty 四类输入。
验证命令:
```bash
```
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web workspace_shell
```
@@ -96,16 +140,22 @@ cargo test -p mnote-web workspace_shell
目标:`GET /` 选择真实 active page 或显示真实空 workspace,不再把设计稿静态首页当主编辑区。
- [x] `gateway.rs::root_entry` 选择顺序固定为 `pageId` > recent cookie > projection active > first page > empty workspace。
- [x] 有 active page 时,根路径渲染或跳转到真实 document shell。
- [x] 无 active page 时,根路径显示空 workspace state。
- [x] 根页不再输出 `.mnote-home-links` 静态设计稿列表作为主内容。
- [x] recent page cookie 与 document shell 打开行为一致。
- [x] Rust 测试覆盖 root selection 顺序。
- [x] `task118` / `task119` 仍通过。
验证命令:
```bash
```
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web root_entry
cd /mnt/Data1T/mnote
@@ -118,17 +168,24 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visu
目标:文档页标题、页面树、正文 bootstrap 来自同一份 Page Aggregate / projection。
- [x] `/documents/:id` 标题来自 Page Aggregate `head.title`
- [x] breadcrumb / topbar active title 与 Page Aggregate 标题一致。
- [x] 左栏 active row 与当前 document id 一致。
- [x] 页面内 page tree / subtree 使用 Page Aggregate `tree.pageSubtree` 或 kernel `page_tree` projection。
- [x] HTML 内嵌 `__MNOTE_PAGE_AGGREGATE__`
- [x] HTML 内嵌 `__MNOTE_EDITOR_BOOTSTRAP__`
- [x] bootstrap 包含 `documentId``workspaceId``saveEndpoint``editorHostKind``assetMode`
- [x] 不从 URL 或静态 fallback 重新推导第二份页面真相。
验证命令:
```bash
```
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web document_shell page_aggregate
cd /mnt/Data1T/mnote
@@ -140,19 +197,28 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-s
目标:主编辑区不是 marker,而是浏览器内真实可编辑 runtime。
- [x] Rust document shell 输出稳定 mount root。
- [x] 前端 island adapter 能读取 `__MNOTE_PAGE_AGGREGATE__`
- [x] 前端 island adapter 能读取 `__MNOTE_EDITOR_BOOTSTRAP__`
- [x] `leptos_tiptap_island` 成为 active host。
- [x] 页面出现 `data-testid="mnote-leptos-tiptap-island-editor-root"`
- [x] 页面出现 `.editor-surface .ProseMirror[contenteditable="true"]`
- [x] `data-editor-host-active="leptos_tiptap_island"` 或等价 observability 可见。
- [x] BlockNote 没有成为默认主编辑器。
- [x] 输入文本能保存。
- [x] reload 后文本仍在。
验证命令:
```bash
```
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-island-hydration-smoke.js
```
@@ -162,15 +228,20 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-isl
目标:树和编辑器不是一次性 SSR,而是接入当前 Rust Web transport / realtime 主链。
- [x] 新建页面后 `/api/tree/events` 或 projection reload 可看到新增节点。
- [x] 重命名页面后 sidebar row title 更新。
- [x] 删除 / purge 页面后 sidebar row 消失。
- [x] 编辑保存走 `/api/documents/save` 或约定 Rust Web save endpoint。
- [x] 保存后 `/api/page-aggregate/:id` 能看到正文变化或 revision 变化。
- [x] `task114``task115``task118``task119``task120``task121` 在当前 3000 全部通过。
验证命令:
```bash
```
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task114-rust-web-gateway-entry-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
@@ -196,9 +267,15 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task121-rust-web-editor-isl
## 10. 完成判定
- [x] `task120` 在当前 `3000` 通过。
- [x] `task121` 在当前 `3000` 通过。
- [x] 左栏页面树来自真实 projection。
- [x] 根页不再用设计稿静态列表伪装主内容。
- [x] 文档页有真实 `leptos-tiptap` 可编辑 surface。
- [x] 保存后 Page Aggregate / reload 可读回。
- [x] `3-8` 可移动到 `done` 后,本 checklist 才能进入完成评估。
@@ -0,0 +1,480 @@
# 4-20 [process] VSCode Explorer 文件树对标设计 v1
> 更新时间:2026-05-06
>
> 关联:
> - `/mnt/Data1T/mnote/tmp/image copy 47.png`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.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/04-tree-domain/done/4-14-task-005-file-tree-final-renderer-capability-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
## 1. 目的
这份文档只处理 `filesystem / Explorer` 文件树下一阶段怎么对标 VSCode Explorer。
它不重新定义树域协议。已有结论继续成立:
- `tree-first graph kernel` 是长期对象真相层。
- `Convex` 继续作为本地自托管存储、实时、文件协作底座。
- `mnote-web` 是 Rust Web 主执行面,负责 projection、command、transport 与 realtime stream。
- 前端和 tree shell 只消费稳定 projection,并持有本地交互态。
本设计补的是此前文档没有写细的 Explorer 级显示和操作合同:行模型、选择/焦点、打开、右键、键盘、拖放、剪贴板、新建、重命名、删除、上传、资源移动和验收清单。
## 2. 当前截图判断
`/mnt/Data1T/mnote/tmp/image copy 47.png` 显示当前文件树已经有 Explorer 入口,但仍不像 VSCode Explorer
- `我的页面` 下直接混排页面节点和 `index.md`,但没有清晰的 workspace root / resource root 语义。
- 页面行使用小菱形 / 折叠箭头,文件行使用蓝色文档图标,信息密度接近,但行列网格不稳定。
- 当前选中态落在 `index.md` 文件行,和右侧文档页 `新页面` 的 active / focus / selection 关系不够清楚。
- `Explorer` 标题像 tab 文案,不像 VSCode Explorer 的 section header + toolbar。
- hover 操作只看到 `+`,缺少新建文件、新建文件夹、刷新、全部折叠、更多等容器级动作。
- 长列表中大量重复 `新页面/index.md`,缺少资源装饰、路径 disambiguation、命名冲突处理和 reveal 规则。
结论:当前不是“样式再修一点”的问题,而是 Explorer 行为合同还没有写到足够细。
## 3. 唯一真源
### 3.1 结构显示真源
文件树显示的唯一真源固定为:
`kernel.project_view(file_tree)` 产出的 `KernelProjectionResult`
当前代码证据:
- 首屏 query 会从 Convex `sidebar.datasetList` 取数据,再通过 `resolveKernelFileTreeProjection` 调 Rust `kernel.project_view(file_tree)` 附加 `kernel_file_tree_projection`
- 实时链路 `/api/mnote-web/stream` snapshot 也会附加同一份 file tree projection。
- Rust 侧生成点是 `bridge-runtime``build_file_tree_projection_result`
- projection item 的核心字段已经在 `core-protocol/src/kernel.rs` 与前端 `tree-protocol.ts` 固定。
因此,Explorer 不能在 UI 层重新推导层级、排序、资源类型、child count、capabilities 或合法性。UI 只能把 projection item materialize 成 row。
### 3.2 持久化操作真源
持久化动作的唯一正式命令面固定为:
- 页面:`tree.node.*``tree.subtree.*`
- 资源:`tree.resource.*`
- 文件树预检:`tree.filetree.*.preflight`
当前已经成立:
- 页面新建、重命名、移动、归档、恢复、永久删除、嵌入、复制子树已经接到 `tree.*`
- 文件树 `drop/delete/paste/upload-target` 已有 Rust preflight。
- 资源 `copy/move/upload` 已有 `tree.resource.copy/move/upload`
当前仍未完成:
- 资源 `rename/delete/restore` 仍经过 legacy `/api/media/batch` 直接 patch / restore。
- `cut` 还没有成为剪贴板动作。
- 资源“移动到...”仍有 `prompt("输入目标页面 ID")` 的临时 UX。
### 3.3 本地交互态
这些状态只能属于 tree shell / runtime 本地态,不得写进 projection
- `expanded`
- `selected`
- `focused`
- `hovered`
- `editingName`
- `clipboard`
- `dragging`
- `dropTarget`
- `contextMenuOpen`
- `keyboardAnchor`
- `revealPending`
`expandedByDefault` 只是 projection 给初始展开的建议,不是展开态真相。
## 4. VSCode Explorer 对标边界
### 4.1 必须对齐
文件树必须对齐 VSCode Explorer 的资源管理器模型:
- 一行就是一个资源命中目标。
- `twistie` 只负责展开/折叠,打开资源是另一种动作。
- 图标来自 `rowKind/resourceMeta/iconHint/assetKind`,不是装饰。
- `selection``focus``hover``active` 分离。
- 右键点击已选区内资源时保留选区,右键点击未选资源时切换到该资源。
- 键盘能完整操作可见行。
- 内部拖拽、外部文件拖入、剪贴板和 context menu 都基于同一 selection/focus 状态。
- 所有危险或跨资源类型动作先走 preflight plan,再落正式 command。
### 4.2 不能照搬
文件树不能照搬 VSCode 的真实文件系统语义:
- mnote 的文件树是页面、`index.md`、附件、mindmap 文件夹、导图子资源、table/book/pdf 等复合资源投影,不是磁盘目录树。
- `page -> index.md -> asset-folder -> asset` 这类多语义链路不能用 VSCode `compact folders` 强行压成一行。
- “在终端打开”“Reveal in Finder/Explorer”“文件比较”等 OS 动作不进入默认合同,除非资源模型已有明确承载。
- VSCode 的单击预览 / 双击固定标签页节奏不能直接照搬。mnote 的打开必须服从当前文档页、Page Aggregate、附件打开链路和权限边界。
- Explorer 是 sidebar 中的创新视图,不是整个产品的唯一主导航。`section` 页面树仍保留 Wolai / Notion 方向。
## 5. 显示合同
### 5.1 Row Grid
每个 file tree row 固定为以下视觉列:
| 列 | 说明 |
| --- | --- |
| indent | 只由 `depth` 决定,步长固定 |
| twistie | 仅 `expandable=true` 时可见并可点击 |
| icon | 由 `iconHint/assetKind/resourceKind` 决定 |
| label | 主名称,单行截断 |
| decoration | 条件性 badge、状态色、资源提示 |
| actions | hover / focus 时出现的轻量动作 |
不得让 label 长度、hover 按钮或选中态改变行高和缩进。
### 5.2 Row Kind
当前稳定行类型继续保留:
- `doc`
- `index`
- `asset-folder`
- `asset`
后续如果需要真实目录或资源组,必须先扩展 Rust projection 的 `rowKind/resourceKind`,不能在 UI 层把某类 asset 临时伪装成目录。
### 5.3 Root
Explorer 顶部应有明确 root
- 单工作区:显示 workspace rootroot 下再进入页面/资源树。
- 多工作区:允许多个 workspace root。
- root 行是否可折叠由 projection capability 决定。
当前截图中 `我的页面 -> Explorer` 的组合不够清晰,后续应改为:
- section header`EXPLORER`
- root row:当前 workspace 名称
- toolbar:新建页面 / 新建资源 / 刷新 / 全部折叠 / 更多
### 5.4 Icon 与 Decoration
图标语义至少覆盖:
- workspace / folder / page / index
- asset
- image / video / audio
- pdf / book
- table
- mindmap
- unknown
Decoration 只表达轻量状态,例如:
- stale / indexing / uploading
- read-only / shared / public
- conflict / invalid target
- cut clipboard visual
Decoration 不得变成第二行副标题。
### 5.5 Active、Selection、Focus
三者必须分离:
- `active`:右侧主区域正在打开的 document / asset。
- `selection`:Explorer 当前选中集合,支持多选。
- `focus`:键盘焦点所在 row。
截图中 `index.md` 选中与右侧 `新页面` active 的关系不清晰。目标规则:
- 打开文档时,`doc``index` 可共享 active 文档语义,但只能有一个 focused row。
- 如果 active document 映射到 `index.md`,应显式定义 active 高亮落在 `index` 还是 `doc`。建议:active 落 `index.md`doc 行只作为目录/页面容器。
- 多选时 active 不覆盖 selectionselection 背景优先。
## 6. 操作合同
### 6.1 打开
P0
- `Enter` 打开 focused row。
- 双击 row 打开资源。
- 单击只负责 selection/focus,不自动打开重资源。
- `index.md` 打开对应文档页。
- asset 打开对应资源 viewer / editor。
P1
- 支持“Reveal active resource in Explorer”。
- 页面切换后自动展开祖先链并定位 active row。
- 搜索过滤后保留 active/reveal 的路径上下文。
说明:当前代码已明确与 VSCode 单击打开不同,这是合理差异,但必须写成正式合同,避免后续反复摇摆。
### 6.2 展开 / 折叠
P0
- 点击 twistie 只展开/折叠,不打开。
- `ArrowRight`:可展开行未展开则展开;已展开或叶子则可进入打开动作,具体由 runtime 定义。
- `ArrowLeft`:已展开则折叠;未展开则焦点移动到父节点。
- `Home/End` 跳到第一/最后可见行。
P1
- `Collapse All` 容器动作。
- 外部拖拽悬停到可展开目标一段时间后自动展开。
### 6.3 选择与右键
P0
- 单击:单选并设置 anchor/focus。
- `Ctrl/Cmd + Click`:切换选中。
- `Shift + Click`:从 anchor 到目标做范围选。
- 右键已选区内 row:保留选区并打开菜单。
- 右键未选 row:切换 selection 到该 row,再打开菜单。
- 空白区点击:清空 selection/focus。
P1
- `Ctrl/Cmd + A` 选中当前 Explorer 可见资源。
- 过滤态下范围选只覆盖可见 row。
### 6.4 键盘
P0 最小按键:
| 按键 | 动作 |
| --- | --- |
| `ArrowUp/ArrowDown` | 上下移动 focus |
| `ArrowLeft/ArrowRight` | 折叠 / 展开 / 父子导航 |
| `Home/End` | 首尾行 |
| `Enter` | 打开 focused row |
| `F2` | 重命名 |
| `Delete/Backspace` | 删除 / 归档 |
| `Ctrl/Cmd+C` | copy |
| `Ctrl/Cmd+X` | cut |
| `Ctrl/Cmd+V` | paste |
| `Esc` | 取消 inline rename / 关闭菜单 / 清理 drag feedback |
当前缺口:已有 copy/paste,但缺 `cut`;键盘能力需要以 runtime contract 覆盖 file tree,而不是散在 `sidebar.tsx`
### 6.5 新建
P0
- `New Page`:映射 `tree.node.create`,在目标 doc/root 下创建页面,并进入 inline rename。
- `New File`:只允许创建 mnote 资源模型内真实存在的资源类型;如果当前没有通用文件创建命令,入口应隐藏或置灰。
- `New Folder`:当前没有真实目录语义,不提供默认入口;mindmap asset-folder 不是通用文件夹。
P1
- 支持上传文件作为 `New File` 的可用替代路径,走 `tree.filetree.upload-target.preflight -> tree.resource.upload`
- 如果未来引入通用资源目录,先扩展 kernel projection / command,再开启 `New Folder`
### 6.6 重命名
P0
- `F2` 和 context menu `Rename` 进入 inline rename,不再使用 `prompt`
- doc / index 重命名最终落 `tree.node.rename` 或 Page Aggregate title command 的正式命名面。
- asset 重命名必须从 legacy `/api/media/batch` 收口到 `tree.resource.rename` 或等价正式命令。
- `Enter` 提交,`Esc` 取消。
- 空名、重名、非法字符、只读资源必须由 preflight / command 返回可解释错误。
P1
- 文件型 asset 默认只选中文件名 stem,不误选扩展名。
- rename 期间保留 row 高度和缩进,不触发布局跳动。
### 6.7 删除 / 恢复 / 永久删除
P0
- doc 删除走 `tree.node.archive`
- doc 恢复走 `tree.node.restore`
- doc 永久删除走 `tree.node.purge`,必须是更强确认路径。
- asset 删除 / 恢复必须补正式 `tree.resource.archive/restore` 或等价命令,不能长期停留在 legacy `/api/media/batch`
- 多选删除先走 `tree.filetree.delete.preflight`,展示将影响的 doc/asset 集合。
P1
- 支持删除后 undo / toast action,命令结果仍以 kernel event 和 projection resync 为准。
### 6.8 剪贴板
P0
- `copy`:复制选中 doc subtree / asset resource。
- `cut`:进入 cut clipboard 状态,源 row 降噪显示;paste 成功后清空 cut 状态。
- `paste`:目标由 focused row / context target / active doc 推导,必须先走 `tree.filetree.paste.preflight`
- 多选 paste 保持原有可见顺序,并过滤父子重复选择。
P1
- 系统剪贴板只保存安全 payload,不暴露敏感内部 token。
- 跨窗口 paste 需要校验 workspace 和权限。
### 6.9 拖放
P0
- 内部拖放支持 move/copy,修饰键控制 copy。
- 外部文件拖入支持 upload target preflight。
- 非法目标即时反馈,不发持久化命令。
- doc 拖到自身或后代节点必须拒绝。
- asset-folder / asset 的可投放能力由 `capabilities` 和 preflight 决定。
P1
- 拖到可展开目标 hover 延迟后自动展开。
- 显示 before / inside / after 三种 drop indicator,而不是只有整行背景。
- 多选拖放过滤已被父节点包含的子节点。
### 6.10 Context Menu
菜单按 `rowKind/resourceKind/selection` 裁剪:
| 目标 | P0 动作 |
| --- | --- |
| root | 新建页面、上传文件、刷新、全部折叠 |
| doc | 打开、新建子页面、重命名、复制、剪切、粘贴、移动到、归档、复制链接 |
| index | 打开、复制、剪切、粘贴、Reveal 页面 |
| asset-folder | 打开、复制、剪切、粘贴、重命名、删除、移动到 |
| asset | 打开、复制、剪切、重命名、删除、下载、移动到 |
| 多选 | 打开禁用、复制、剪切、删除、移动到 |
当前缺口:
- mindmap rename 提示不支持。
- table move/download 提示不支持。
- asset 移动仍靠输入页面 ID。
这些必须进入 P0/P1 差异清单,不得被样式修复掩盖。
## 7. 资源排序与混排规则
P0 固定当前最小规则:
1. workspace root
2. doc row
3. doc 的 `index.md`
4. doc 的 direct assets / asset folders
5. child documents
排序由 Rust projection 的 `position` 与 preorder 决定。UI 只渲染。
需要新增到 projection meta 的候选规则:
- `ordering: "kernel_file_tree_preorder"`
- `folderPolicy: "mnote_composite_resource"`
- `indexPlacement: "first_child"`
- `assetPlacement: "after_index_before_child_documents"`
这样可以避免 UI 通过 hardcode 解释为什么 `index.md` 总是出现在页面下第一行。
## 8. Search / Filter / Reveal
P0
- Explorer filter 只改变可见 row,不改变 projection truth。
- filter 命中子节点时必须补全祖先链。
- active row 在 filter 后不可见时,保留 active 状态但不强制显示。
- `Reveal in Explorer` 必须展开祖先链、滚动到目标 row、设置 focus,不破坏 selection,除非调用者明确要求 select。
P1
- 支持 `maxResults` 下的 ancestor completion。
- 支持 indexing visibility,展示 stale / refreshing 状态。
当前 `KernelFileTreeProjection.meta.search.indexingVisibility` 已有雏形,后续应把它用于 UI 状态,而不是在 Explorer 自行发明刷新状态。
## 9. 分阶段任务
### P0:让当前 Explorer 行为不再含糊
- 固定 active / selection / focus 的视觉优先级。
- 补齐 file tree keyboard runtime`Arrow/Home/End/Enter/F2/Delete/Cut/Copy/Paste/Esc`
-`cut` clipboard 状态和 cut row decoration。
- 把 doc / index / asset / asset-folder 的 context menu 动作裁剪成合同。
- 移除资源移动的 `prompt("输入目标页面 ID")`,改用 tree picker / move target picker。
- asset rename/delete/restore 制定 `tree.resource.*` 正式命令或等价 bridge 命名,并保留 legacy alias。
- 写 smoke 固化当前截图中重复页面 + `index.md` 场景的 active/selection/reveal 行为。
### P1:让资源管理器达到 VSCode 常用操作预期
- inline rename 全面替代 prompt。
- root toolbar 对齐 Explorer:新建页面、上传文件、刷新、全部折叠、更多。
- 拖放支持 before / inside / after indicator 与 hover auto expand。
- 上传 target preflight 失败时在 drop target 上显示明确反馈。
- 多选 delete / move / copy 的 preflight plan 显示影响集合。
- mindmap/table 的 rename/move/download 能力从“不支持”变为可解释菜单状态或正式实现。
### P2:高级 Explorer 能力
- compact folders 只用于未来真实目录链,不用于页面/附件复合链。
- git-like / sync-like decorations,如果未来有对应数据源再加入。
- 多 workspace root。
- 大树虚拟化和增量加载。
- 跨窗口剪贴板。
## 10. 验收
### 10.1 单元 / 合同测试
必须覆盖:
- projection item 到 row model 的稳定映射。
- active / selection / focus 分离。
- `Ctrl/Cmd` 多选、`Shift` 范围选、右键保留选区。
- 可见 row 变化后的 selection normalize。
- `cut/copy/paste` payload encode/decode 与目标推导。
- drop preflight payload 构造。
- illegal drop:自身、后代、跨 workspace、只读资源。
- inline rename submit/cancel。
### 10.2 Rust 测试
必须覆盖:
- `kernel.project_view(file_tree)` 输出 `doc/index/asset-folder/asset`
- `resourceMeta``capabilities``iconHint` 稳定。
- `tree.filetree.*.preflight` 对多选和非法目标返回 plan。
- 新增 `tree.resource.rename/archive/restore` 或等价命令后,覆盖 command event 与 projection resync。
### 10.3 浏览器 smoke
必须覆盖:
- 当前截图同类数据:多个同名 `新页面`,每个页面下有 `index.md`
- 点击 `index.md` 后右侧打开对应页面,Explorer active/focus/selection 可解释。
- 右键已选区保留多选。
- `F2` inline rename`Esc` 取消,`Enter` 提交。
- `Ctrl/Cmd+X` 后 cut decoration 出现,paste 成功后消失。
- 外部文件拖入 doc / index / asset-folder 的目标差异。
- 资源移动不再弹 `prompt`
## 11. 非目标
- 不把文件树改成真实磁盘文件系统。
- 不把 `Explorer` 替代 `section` 页面树作为唯一导航。
- 不在 UI 层重新拼排序、层级、资源类型或权限。
- 不为了像 VSCode 而绕过 Page Aggregate、tree projection 或 `tree.*` 命令。
- 不把 `90-1-filetree.md` 的 Rust 生态调研当作实现模板;它只作为参考资料。
## 12. 完成判定
这份设计进入 done 的条件:
- P0 项全部有代码和 smoke 覆盖。
- file tree 的 active / selection / focus / keyboard / context menu / clipboard / DnD 有稳定 runtime contract。
- 资源 rename/delete/restore 不再只依赖 legacy `/api/media/batch` 主路径。
- 当前截图暴露的 Explorer 行为歧义能通过测试说明和验证。
- 新增能力没有制造第二套树真相。
@@ -0,0 +1,586 @@
# 4-21 [process] Local Folder / Convex 统一树源架构 v1
> 更新时间:2026-05-06
>
> 关联:
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/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`
## 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 workspace 的 source adapter,负责同步、协作、权限、分享和远程资源。
- 两者共用同一套 projection、command、preflight、runtime state 和 UI。
- 差异只允许出现在 `WorkspaceSource` 的 capability、preflight plan 和 executor 中。
核心原则:
> **维护一套树操作语义,不维护两套文件树产品。**
## 2. 为什么需要这个方案
当前 mnote 的 Explorer 视图仍然基于 Convex 虚拟用户空间,这让 VSCode Explorer 对标天然别扭:
- 没有真实文件夹 root。
- `New File / New Folder / Rename / Move / Cut / Paste / Drop` 都需要模拟文件系统语义。
- `index.md`、页面、附件、mindmap asset-folder 混成复合投影后,无法完全照搬 VSCode 文件夹操作。
如果允许用户打开一个本地文件夹,就能获得真实文件系统语义:
- 文件夹就是文件夹。
- `.md` 文件就是页面内容的本体。
- 普通文件就是资源。
- 拖入、拖动、剪切、复制、粘贴、重命名、删除都可以映射到文件系统操作。
- Git、外部编辑器、脚本、ripgrep 等外部工具自然成立。
但这不表示 Convex 失去意义。Convex 的定位应从“所有树的底座”调整为:
> **云工作区 source:负责多端、协作、权限、分享、发布、异步处理和远程资源。**
## 3. 术语
### 3.1 `WorkspaceSource`
`WorkspaceSource` 是树域的存储源适配层。
最低接口:
- `sourceKind`
- `rootUri`
- `workspaceId`
- `capabilities`
- `loadSnapshot`
- `watch`
- `preflightCommand`
- `executeCommand`
- `resolvePageAggregate`
当前建议先支持两类:
- `local_folder`
- `convex_workspace`
### 3.2 `SpaceRoot`
`SpaceRoot` 是用户在 UI 里选择的工作区入口。
示例:
- `file:///home/lix/Documents/mnote-vault`
- `convex://workspace/ws_123`
`SpaceRoot` 可以出现在同一个 Explorer 列表里,但每个 root 只能由一个 source adapter 持有。
### 3.3 `Source Capability`
capability 描述当前 source 对某项操作的真实支持程度。
示例:
- `create-file`
- `create-folder`
- `rename`
- `move`
- `copy`
- `trash`
- `restore`
- `purge`
- `upload`
- `watch`
- `share`
- `publish`
- `collaborate`
capability 不改变 UI 命令名,只控制菜单显隐、禁用原因和 preflight 结果。
## 4. 真源分层
### 4.1 存储真源
存储真源由 source 持有:
| source | 存储真源 |
| --- | --- |
| `local_folder` | 本地文件系统 |
| `convex_workspace` | Convex 数据库 + 对象存储 |
本地模式不得先导入 Convex 再渲染。那会破坏本地文件夹模式的价值。
### 4.2 语义真源
树、页面和资源语义由 Rust kernel 统一生成:
- `KernelNode`
- `KernelEdge`
- `file_tree`
- `page_tree`
- `page_aggregate`
- `tree.*` command result
source adapter 只负责读写存储,不允许 UI 直接从本地路径或 Convex record 拼树语义。
### 4.3 UI 本地态
继续服从 `4-7`
- `expanded`
- `selected`
- `focused`
- `hovered`
- `editingName`
- `clipboard`
- `dragging`
- `dropTarget`
- `contextMenuOpen`
这些不进入 source,也不进入 projection。
## 5. Local Folder Workspace
### 5.1 P0 范围
P0 只支持:
- 打开一个本地文件夹作为 workspace root。
- `file_tree` 显示该文件夹下的目录、`.md` 文件和普通文件。
- `page_tree` 只显示 `.md` 文件。
- 打开 `.md` 文件时进入文档页。
- 普通文件作为 asset 打开或预览。
- 对本地文件和目录执行 VSCode-like 操作。
暂不支持:
- 多用户本地 ACL。
- 云同步。
- 多端协作。
- 非 Markdown 正文格式的页面编辑。
- 本地文件夹自动发布到 Convex。
### 5.2 本地页面身份
不能只用文件路径作为长期 page id。
建议 P0 采用三层策略:
1. `.md` frontmatter 中存在 `mnote_id` 时使用它。
2. 不存在时在 `.mnote/page-ids.json` 记录 `relativePath -> pageId`
3. 临时未初始化文件夹可用 path-derived id,但只作为只读过渡。
原因:
- 重命名文件时页面引用不能断。
- 移动文件时历史、链接、backlink 和 AI 索引需要能追踪。
- 同名文件在不同目录下必须稳定区分。
### 5.3 Markdown 到 Page Aggregate
本地 `.md` 文件映射到 `page_aggregate`
| Page Aggregate 字段 | 本地来源 |
| --- | --- |
| `page_identity` | `mnote_id` / `.mnote/page-ids.json` |
| `page_head.title` | frontmatter `title` / 第一行 H1 / 文件名 |
| `page_body` | Markdown AST / Tiptap document adapter |
| `page_tree` | 文件夹层级派生的 md-only projection |
| `page_layout` | frontmatter / `.mnote/page-options.json` |
写回时:
- 正文保存写回 `.md`
- 标题重命名优先改文件名,必要时同步 H1/frontmatter。
- 页面设置写入 frontmatter 或 `.mnote/page-options.json`,具体由后续编辑器设计冻结。
### 5.4 本地资源
`.md` 文件作为 resource
- image / video / audio
- pdf / book
- table-like 文件
- unknown file
资源链接优先使用相对路径。
附件从页面正文引用时,应解析成 `resourceMeta`,但不能把正文引用本身变成树结构真相。
## 6. Convex Workspace
Convex 继续存在,但定位更明确:
- 云工作区。
- 权限 / 成员 / 分享。
- 评论、协作状态、公开页面。
- 远程附件存储。
- OCR、AI、索引、异步任务。
- 多设备访问。
Convex source adapter 输出同一套 kernel 输入:
- document record -> page/document node
- media asset -> asset/resource node
- mindmap/table/book/pdf -> resource node
- share/public/template 状态 -> resource meta / decoration meta
Convex 不再被要求模拟真实本地文件系统;它只需要实现同一组 tree command 语义。
## 7. 统一 Projection
### 7.1 `file_tree`
`file_tree` 表达 source 的资源管理结构。
本地 source
- root folder
- directory
- markdown file
- normal file
Convex source
- workspace root
- page/document
- `index.md` view
- asset-folder
- asset
共同字段继续使用 `4-4`
- `rowId`
- `nodeId`
- `parentNodeId`
- `nodeType`
- `projectionKind`
- `title`
- `depth`
- `position`
- `childCount`
- `expandable`
- `capabilities`
- `resourceMeta`
- `iconHint`
新增建议:
- `sourceKind`
- `sourceUri`
- `relativePath`
- `storageIdentity`
- `operationProfile`
这些应进入 `resourceMeta.extra` 或后续协议扩展,不能散落在 UI。
### 7.2 `page_tree`
`page_tree` 表达可作为知识页面打开的页面集合。
本地 source
- 只显示 `.md` 文件。
- 默认层级来自目录结构。
- 排序可先使用文件系统排序,后续支持 frontmatter `order`
Convex source
- 继续显示 document/page 层级。
- 不显示普通 asset。
关键判断:
> Wolai 页面树不是文件树的替代,而是 markdown/page resource 的语义视图。
### 7.3 `page_aggregate`
无论 source 是本地还是 Convex,文档页只消费 `page_aggregate`
前端不应该关心正文来自:
- 本地 `.md`
- Convex document content
- 未来同步副本
差异由 page aggregate loader / source adapter 吸收。
## 8. 统一 Command
UI 只发一套命令。
### 8.1 命令路由
```text
UI action
-> tree command envelope
-> Rust command router
-> source adapter preflight
-> source adapter execute
-> kernel event / projection resync
```
命令 envelope 必须携带:
- `sourceKind`
- `workspaceId`
- `rootUri`
- `targetNodeId`
- `targetResourceMeta`
- `selection`
- `operation`
### 8.2 命令语义
| UI 操作 | 统一命令 | Local executor | Convex executor |
| --- | --- | --- | --- |
| 新建页面 | `tree.node.create` | 创建 `.md` | 创建 document |
| 新建文件夹 | `tree.folder.create``tree.resource.createFolder` | 创建目录 | 按 capability 决定是否支持 |
| 重命名页面 | `tree.node.rename` | rename `.md` / 更新 title | rename document |
| 移动页面 | `tree.subtree.move` | move file / folder | move document subtree |
| 复制页面 | `tree.subtree.copy` | copy file / folder | copy document subtree |
| 删除页面 | `tree.node.archive` | 移到 `.mnote/trash` 或系统 trash | 标记 archived |
| 永久删除 | `tree.node.purge` | 删除文件 | purge document |
| 上传 / 拖入文件 | `tree.resource.upload` | copy into folder | upload object storage |
| 移动资源 | `tree.resource.move` | move file | update asset parent/document |
| 复制资源 | `tree.resource.copy` | copy file | copy asset |
| 重命名资源 | `tree.resource.rename` | rename file | rename asset |
| 删除资源 | `tree.resource.archive` | trash file | archive asset |
| 恢复资源 | `tree.resource.restore` | restore from trash | restore asset |
说明:
- `tree.folder.create` 是否新增,需要单独冻结命名。若不新增,也可以把 folder 视作 `tree.resource.create``resourceKind=folder`
- 命名必须在后续 `4-6` 的 command protocol 补稿里冻结。
## 9. 统一操作但不同 capability
用户担心“两套文件树操作”是合理的。解决方式不是把底层做成一样,而是统一 capability 解释。
### 9.1 统一 UI
以下 UI 行为必须完全共用:
- row model
- active / selection / focus
- keyboard
- inline rename
- context menu
- clipboard
- drag/drop
- reveal
- filter/search
- preflight result display
### 9.2 不同 source capability
差异只表现为:
- 菜单项隐藏。
- 菜单项禁用并显示原因。
- preflight plan 返回不同风险。
- execute adapter 调用不同存储 API。
示例:
- 本地文件夹支持 `New Folder`
- Convex 虚拟空间默认不支持真实 `New Folder`,除非引入 folder resource。
- Convex 支持 `Share / Publish`
- 本地文件夹默认不支持 `Share / Publish`,除非执行显式 publish 到 cloud。
## 10. 权限模型
### 10.1 本地权限
本地权限来自:
- 操作系统文件权限。
- 用户选择文件夹的显式授权。
- workspace trust。
- mnote 自己的安全限制,例如禁止越过 root。
如果用户想“本地建立每个用户的文件夹”,可以作为部署策略:
```text
/data/mnote-users/alice/
/data/mnote-users/bob/
```
但这只是本地 source 的 root 管理方式,不等价于 Convex 的协作权限模型。
### 10.2 Convex 权限
Convex 权限来自:
- workspace membership
- document ACL
- public/share state
- group permission
- server-side policy
这类能力本地文件夹不能自动获得。
### 10.3 不混淆
不要让本地文件夹伪装成云协作空间。
正确表达:
- local:你有这个文件夹的本机读写权。
- cloud:你有这个 workspace 的协作权限。
## 11. VSCode 源码参考
本轮已把 VSCode 源码浅克隆到:
`/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/vscode`
当前 checkout
- commit`b5c5f2f`
- sparse paths
- `src/vs/workbench/contrib/files`
- `src/vs/platform/files`
- `src/vs/base/browser/ui/tree`
- `src/vs/base/browser/ui/list`
- `src/vs/workbench/browser/parts/views`
- command / action / context key 相关基础目录
### 11.1 值得参考的点
VSCode Explorer 有几个重要设计值得直接吸收:
- `IExplorerService` 同时管理 roots、context、editable、copy/cut、refresh、select/reveal 和 bulk edit。
- context key 明确区分 folder、readonly、root、cut、focus、find provider 等状态。
- `ExplorerItem` 是资源模型,持有 `resource/name/isDirectory/isReadonly/parent/root/children`
- renderer 把 cut 状态变成 row class,并把 inline rename 做在 row 内。
- inline rename 默认只选中文件名 stem`F2` 可在 stem/all/extension 三种选择之间切换。
- DnD 会区分 native drop、Explorer 内部 drop、root reorder、readonly、self/descendant invalid、copy vs move、before/after/over indicator 和 auto expand。
- sorter 支持 default/mixed/filesFirst/type/modified/foldersNestsFiles,但排序逻辑仍在模型层,不在 UI 临时判断。
### 11.2 不能照搬的点
不能照搬:
- 真实磁盘路径作为唯一页面身份。
- compact folders 到 page/resource 复合链。
- OS 级菜单动作作为默认能力。
- VSCode 的 editor tab preview/pin 语义。
- 文件 nesting 规则到 mnote 的页面引用或附件关系。
## 12. 推荐实施顺序
### P0:统一 Source 抽象和本地只读投影
- 增加 `WorkspaceSource` 设计与 Rust trait。
- 增加 `LocalFolderSource` 只读扫描。
- 本地 source 输出 `file_tree`
- 本地 source 输出 md-only `page_tree`
- 打开 `.md` 生成只读 `page_aggregate`
- UI 可从入口选择 `convex workspace``local folder`
验收:
- 打开一个本地文件夹后,Explorer 显示真实目录和 `.md`
- 页面树只显示 `.md`
- 点击 `.md` 能进入文档页阅读态。
- Convex 现有 workspace 不受影响。
### P1:本地写操作与统一 command
- 本地新建 `.md`
- 本地新建目录。
- inline rename。
- move / copy / cut / paste。
- delete 到 `.mnote/trash` 或系统 trash。
- drop 外部文件。
- 文件 watcher 刷新 projection。
验收:
- 同一套 UI 对 local 和 Convex 发同一类 command envelope。
- source adapter 分别执行。
- preflight UI 共用。
### P2:本地 Markdown Page Aggregate 写回
- Markdown AST 与 Tiptap document 双向转换。
- 标题编辑写回文件名 / H1 / frontmatter 的规则冻结。
- page id 管理。
- backlink / reference scan。
- 本地全文索引。
### P3Local 与 Cloud 的显式桥接
- Import local folder to cloud。
- Publish local page to cloud。
- Cloud workspace export to folder。
- 可选 sync,不作为 P0/P1。
## 13. 风险
### 13.1 最大风险:双真相
如果本地文件夹先导入 Convex,再从 Convex 渲染,就会变成:
```text
file system -> Convex mirror -> projection -> UI
```
这会制造双真相,也会让 VSCode-like 操作变慢、变怪。
必须避免。
### 13.2 页面身份风险
如果 page id 只依赖路径,移动/重命名会断引用。
P0 可以先只读,P1 写操作前必须完成 page id 方案。
### 13.3 Markdown 往返风险
Tiptap 文档和 Markdown 不是无损双向转换。
P0 可以先阅读态或有限编辑;P2 再处理复杂块、附件、引用、frontmatter。
### 13.4 安全风险
本地 source 必须限制在用户选择的 root 下:
- 禁止 `../` 越界。
- symlink 默认只显示,不默认跟随写入。
- 删除 / 覆盖必须有 preflight。
- watcher 不监听 root 外路径。
## 14. 非目标
- 不让 Convex 成为本地文件夹模式的中介。
- 不维护 local file tree 和 convex file tree 两套 UI。
- 不在前端用路径和 Convex record 临时拼第二套树。
- 不在 P0 做本地/云自动同步。
- 不在 P0 承诺 Markdown 与 Tiptap 完全无损互转。
- 不用本地 per-user 文件夹替代 Convex 协作权限。
## 15. 完成判定
本文进入 done 的条件:
- `WorkspaceSource` 抽象落地。
- local 和 convex 都能通过同一 command envelope 接入 tree runtime。
- `file_tree/page_tree/page_aggregate` 对 UI 的消费路径不分叉。
- 本地文件夹 P0 只读投影和 `.md` 页面打开可验证。
- 至少一组写操作在 local 和 Convex 上共用 UI、共用命令名、只分 source executor。
- 新增能力没有绕开 Rust kernel 或制造第二套树真相。
@@ -0,0 +1,590 @@
# 4-22 [process] Local Folder / Convex 统一树源顺序执行清单 v1
> 更新时间:2026-05-06
>
> 来源:
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-20-vscode-explorer-file-tree-alignment-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/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`
## 0. 执行原则
本清单按顺序执行,不跳阶段。
总目标:
> **只维护一套 tree projection、tree command、preflight、runtime state 和 UILocal Folder 与 Convex 只作为不同 `WorkspaceSource` executor 存在。**
硬约束:
- [x] 不让本地文件夹先导入 Convex 再渲染。
- [x] 不在 UI 层用本地 path 或 Convex record 拼第二套树。
- [x] 不新增 local 专用文件树 UI。
- [x] 不绕过 Rust kernel 写页面树、文件树、排序或权限。
- [x] 不把 `expanded/selected/focus/drag/drop/contextMenu` 写进 projection。
- [x] 所有写操作先 preflight,再 execute,再 projection resync。
**Done 条件:**
- 本清单对应实现中,Local 和 Convex 都通过同一 command envelope 进入 Rust command router。
- `file_tree/page_tree/page_aggregate` 的 UI 消费路径不因 source 分叉。
- 本地文件夹与 Convex 的差异只通过 capability / preflight / executor 表达。
## 1. Phase A:冻结 Source 与协议边界
### A1. 定义 `WorkspaceSource` 合同
- [x] 在 Rust 侧定义 `WorkspaceSource` trait / interface 草案。
- [x] 字段至少覆盖 `sourceKind``rootUri``workspaceId``capabilities`
- [x] 方法至少覆盖 `loadSnapshot``watch``preflightCommand``executeCommand``resolvePageAggregate`
- [x] 明确 `local_folder``convex_workspace` 两个 source kind。
- [x] 写 contract test,验证 source kind 和 root uri 会进入 command envelope。
**Done 条件:**
- Rust 代码中存在统一 `WorkspaceSource` 抽象。
- 至少有 mock source 能输出 `file_tree/page_tree` snapshot。
- 前端没有新增 source-specific tree component。
### A2. 扩展 projection source metadata
- [x] 为 projection item 补 source metadata 方案。
- [x] 字段候选固定为 `sourceKind``sourceUri``relativePath``storageIdentity``operationProfile`
- [x] 优先放入 `resourceMeta.extra`,除非需要正式扩展 `core-protocol`
- [x] 更新 TS 类型映射。
- [x] 补 fixturelocal folder item、convex item 同时存在。
**Done 条件:**
- `file_tree` item 可以区分来自 local 还是 convex。
- UI 仍只读 projection 字段,不直接访问 source adapter。
- fixture 覆盖 `directory/md file/normal file/document/index/asset`
### A3. 统一 command envelope
- [x] 定义 `TreeCommandEnvelope` 的 source 字段。
- [x] envelope 至少包含 `sourceKind``workspaceId``rootUri``targetNodeId``targetResourceMeta``selection``operation`
- [x] 现有 `tree.node.*``tree.subtree.*``tree.resource.*` 进入统一 router。
- [x] router 按 source kind 分发到 executor。
- [x] 对 unknown source / capability missing 返回可解释错误。
**Done 条件:**
- 同一个 UI action 可以构造同一种 envelope。
- local / convex 的 executor 可以在测试中分别接收 envelope。
- 没有新增 `/api/local-filetree/*` 这类平行命令入口。
## 2. Phase BLocal Folder 只读主链
### B1. 打开本地文件夹入口
- [x] 增加选择本地文件夹的入口。
- [x] 生成 `SpaceRoot=file://...`
- [x] 记录最近打开的 local root。
- [x] 禁止 root 外路径访问。
- [x] 对无权限目录返回明确错误。
**Done 条件:**
- 用户能选择一个本地文件夹并进入 local workspace。
- 3000 `desktop:hot` 主入口不能跳到 `/tree` debug shell,也不能落入 `legacy_next_compat_disabled`
- Convex workspace 入口不受影响。
- smoke 能断言当前 source kind 为 `local_folder`
### B2. LocalFolderSource 只读扫描
- [x] 扫描目录。
- [x] 识别 directory。
- [x] 识别 `.md` 文件。
- [x] 识别普通文件。
- [x] 默认忽略 `.git``node_modules``.mnote/trash` 等高噪音目录。
- [x] 处理 symlink:P0 只显示,不默认跟随写入。
**Done 条件:**
- 本地文件夹能输出 read-only snapshot。
- snapshot 不越过 root。
- 扫描失败不会让整个 app 崩溃。
### B3. 输出 local `file_tree`
- [x] directory 映射为 folder row。
- [x] `.md` 映射为 markdown file row。
- [x] 普通文件映射为 asset row。
- [x] iconHint 覆盖 markdown/image/video/audio/pdf/book/table/unknown。
- [x] childCount、depth、parentNodeId、position 由 kernel 生成。
- [x] 排序规则先固定为 VSCode defaultfolder firstname sort。
**Done 条件:**
- `kernel.project_view(file_tree)` 可以返回 local folder projection。
- UI 使用同一 Explorer surface 渲染 local projection。
- 浏览器 smoke 可看到真实目录、`.md`、普通文件。
### B4. 输出 md-only `page_tree`
- [x] 只把 `.md` 文件纳入 page tree。
- [x] 目录只作为页面树分组/父级,不作为正文页面打开。
- [x] 页面标题优先级固定为 frontmatter `title` > 第一行 H1 > 文件名。
- [x] parent/child 来自目录层级。
- [x] 同名 md 文件必须通过 path / page id 区分。
**Done 条件:**
- 同一个 local root 下,Explorer 显示所有文件,页面树只显示 `.md`
- 点击页面树 md 节点能定位到同一个 `.md` resource。
- 不因过滤普通文件影响 page tree 层级。
### B5. `.md` 只读 Page Aggregate
- [x] 读取 `.md` 文件。
- [x] 解析 frontmatter。
- [x] 解析标题。
- [x] 生成只读 `page_aggregate`
- [x] 文档页用现有 `DocumentShell / DocumentContent` 消费。
- [x] 文件读取失败显示错误页,不清空 Explorer。
**Done 条件:**
- 点击本地 `.md` 文件能打开文档阅读态。
- 文档页不关心内容来自 local 还是 Convex。
- `page_aggregate` 字段满足 `5-5-1` 最小契约。
## 3. Phase C:页面身份与本地元数据
### C1. 本地 page id 策略
- [x] 支持 frontmatter `mnote_id`
- [x] 支持 `.mnote/page-ids.json`
- [x] 支持 path-derived 临时 id,但仅用于未初始化只读模式。
- [x] page id 生成必须稳定且不与 Convex document id 冲突。
- [x] 写测试覆盖 rename/move 后 id 保持。
**Done 条件:**
- 重命名或移动 `.md` 后,page id 不变。
- 没有 id 的文件能被安全初始化。
- 同名不同路径 `.md` 不冲突。
### C2. 本地 `.mnote` 元数据目录
- [x] 定义 `.mnote/page-ids.json`
- [x] 定义 `.mnote/page-options.json`
- [x] 定义 `.mnote/trash` 或系统 trash 策略。
- [x] `.mnote` 不出现在默认 Explorer 展示中,除非开启 debug。
- [x] 元数据写入必须原子化或有失败回滚策略。
**Done 条件:**
- `.mnote` 元数据能支持 page id 和页面设置。
- 默认用户视图不被 `.mnote` 噪音污染。
- 元数据损坏时有可解释错误和恢复建议。
## 4. Phase D:统一 Explorer P0 交互
### D1. Active / Selection / Focus 分离
- [x] 统一 row state`activeRowId``selectedRowIds``focusedRowId`
- [x] active 表示右侧打开资源。
- [x] selection 表示 Explorer 操作集合。
- [x] focus 表示键盘焦点。
- [x] local 和 convex 共用 reducer。
- [x] 多选时 selection 视觉优先于 active。
**Done 条件:**
- 截图中的 `index.md` active/focus/selection 歧义被测试覆盖。
- local `.md` 和 Convex `index.md` 使用同一视觉规则。
- selection 不回写 projection。
### D2. 键盘导航
- [x] `ArrowUp/ArrowDown` 移动 focus。
- [x] `ArrowLeft/ArrowRight` 折叠 / 展开 / 父子导航。
- [x] `Home/End` 跳首尾。
- [x] `Enter` 打开 focused row。
- [x] `F2` 进入 inline rename。
- [x] `Delete/Backspace` 触发删除 preflight。
- [x] `Ctrl/Cmd+C` copy。
- [x] `Ctrl/Cmd+X` cut。
- [x] `Ctrl/Cmd+V` paste。
- [x] `Esc` 取消 rename / menu / drag feedback。
**Done 条件:**
- 键盘能力在 runtime contract 中测试,不只散落在 React handler。
- local 和 convex 使用同一组按键浏览器 smoke。
- `cut` 不再缺失。
### D3. 选择与右键
- [x] 单击单选并设置 anchor/focus。
- [x] `Ctrl/Cmd + Click` 切换选中。
- [x] `Shift + Click` 范围选。
- [x] 右键已选区内 row 保留选区。
- [x] 右键未选 row 切换到该 row。
- [x] 空白区点击清空 selection/focus。
**Done 条件:**
- 多选 context menu 不会被右键误清空。
- local/convex 的 selection reducer 共用。
- 过滤可见行变化后 selection normalize 正确。
- 已选区内右键、未选项右键、空白区点击三条路径都必须在真实浏览器中验证。
### D4. Row grid 与 root toolbar
- [x] 固定 indent / twistie / icon / label / decoration / actions 六列。
- [x] section header 使用 `EXPLORER`
- [x] root row 明确显示 local folder 或 cloud workspace。
- [x] toolbar 至少有新建、上传/导入、刷新、全部折叠、更多。
- [x] toolbar action 由 capability 控制可用性。
**Done 条件:**
- hover action 不改变行高和缩进。
- local root 与 Convex root 在同一 Explorer 框架下展示。
- toolbar 不创建 source-specific UI 分支。
- hover / focus 显示 actions 时,row height、indent、twistie/icon/label 列位置必须有浏览器截图或 DOM 断言。
## 5. Phase E:统一 Preflight 与 Context Menu
### E1. Context menu profile
- [x] root 菜单:新建页面、新建文件夹、上传/导入、刷新、全部折叠。
- [x] md/page 菜单:打开、重命名、复制、剪切、粘贴、移动到、删除、Reveal。
- [x] folder 菜单:新建页面、新建文件夹、粘贴、重命名、删除。
- [x] asset 菜单:打开、重命名、复制、剪切、删除、下载、移动到。
- [x] 多选菜单:复制、剪切、删除、移动到。
- [x] share/publish 只在 Convex capability 存在时出现。
**Done 条件:**
- 菜单项由 rowKind/resourceKind/capability/selection 共同裁剪。
- 不支持的能力有禁用原因或隐藏规则。
- mindmap/table 的“不支持”状态被 capability 表达,而不是散落提示。
### E2. Preflight result UI
- [x] delete preflight 展示影响集合。
- [x] move preflight 展示目标和冲突。
- [x] paste preflight 展示命名冲突策略。
- [x] upload/drop preflight 展示目标是否可写。
- [x] readonly / permission denied / root escape 返回统一错误结构。
**Done 条件:**
- local 和 convex 的 preflight 使用同一 UI 组件展示。
- 失败时不发 execute。
- 危险动作不会静默执行。
## 6. Phase FLocal 写操作 P1
### F1. 新建
- [x] `tree.node.create` 在 local source 创建 `.md`
- [x] `tree.folder.create` 或等价命令创建目录。
- [x] 新建后进入 inline rename。
- [x] 命名冲突走 incremental naming 或 preflight 提示。
- [x] Convex 对 `New Folder` 按 capability 禁用,除非已有 folder resource。
**Done 条件:**
- 同一个新建入口可在 local 创建文件,在 Convex 创建 document。
- Convex 不支持 folder 时不会显示假入口。
- 新建结果通过 projection resync 出现。
### F2. Inline rename
- [x] 本地文件 / 目录 rename 写入文件系统。
- [x] `.md` rename 保持 page id。
- [x] Convex document rename 继续走 `tree.node.rename`
- [x] asset rename 收口到 `tree.resource.rename` 或等价正式命令。
- [x] `Enter` 提交,`Esc` 取消。
- [x] 文件型资源默认选中文件名 stem。
**Done 条件:**
- 不再使用 `prompt` 做重命名。
- local 和 convex 的 rename UI 完全一致。
- rename 失败不破坏 selection/focus。
### F3. Move / Copy / Cut / Paste
- [x] local move/copy 使用文件系统操作。
- [x] Convex move/copy 使用 existing tree/resource command。
- [x] cut 状态有 decoration。
- [x] paste 成功后清空 cut。
- [x] 多选时过滤父子重复选择。
- [x] 禁止 move 到自身或后代。
**Done 条件:**
- local/convex 都通过同一 clipboard payload。
- cut/copy/paste 浏览器 smoke 覆盖两个 source。
- projection resync 后 selection/focus 合理保留。
### F4. Delete / Restore / Purge
- [x] local delete 进入 `.mnote/trash` 或系统 trash,策略需冻结。
- [x] local restore 从 trash 恢复。
- [x] local purge 永久删除。
- [x] Convex delete/restore/purge 继续走 `tree.node.*` / `tree.resource.*`
- [x] asset delete/restore 从 legacy `/api/media/batch` 收口。
**Done 条件:**
- 删除不是直接 `rm` 静默执行。
- restore 在 local 和 Convex 都有统一菜单语义。
- purge 有更强确认。
### F5. Drop 外部文件
- [x] local drop 文件复制进目标目录。
- [x] Convex drop 文件上传对象存储。
- [x] drop 到 readonly 目标被拒绝。
- [x] drop 到 md 文件时目标归属规则明确。
- [x] before/inside/after indicator 覆盖本地目录和 Convex document/asset-folder。
**Done 条件:**
- 外部拖入在 local 和 convex 使用同一 drop preflight UI。
- 非法目标不执行写入。
- 文件写入后 projection 自动刷新。
- 外部文件 drop、内部拖拽、before/inside/after indicator、hover auto expand 都必须在真实浏览器中验收,不能只靠 contract test 或 jsdom。
- 至少覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法拖拽场景。
**验收记录:**
- local 外部 drop`scripts/task163-local-folder-unified-tree-browser-smoke.js` 使用真实 Chromium `DataTransfer/File`,覆盖取消 preflight 不写入、确认后复制进目录、drop 到 `.md` 时写入父目录、readonly 目标拒绝且不发 execute。
- Convex 外部 drop:同一 smoke 通过 iframe 宿主监听 `tree.filetree.external-drop`,覆盖 Convex document 与 asset-folder 两类目标;宿主侧继续由 `tree-shell-iframe-host.tsx -> Sidebar.handleResourcePaneDropFiles -> uploadFileTreeResourceAsset -> /api/media/upload` 上传对象存储。
- 对象存储链路:`resource-command-client.test.ts` 覆盖 `uploadFileTreeResourceAsset``app/api/media/upload/route.test.ts` 覆盖 `tree.resource.upload` plan、Convex upload URL、`mediaAssets:createWithStorage` 与资源 delta artifact。
- indicator:同一 smoke 覆盖本地目录 before/inside/after、hover auto expand,以及 Convex document before/inside/after 与 asset-folder inside。
## 7. Phase GWatcher / Realtime / Refresh
### G1. Local file watcher
- [x] 监听 root 下文件新增、删除、重命名、修改。
- [x] 忽略 `.git/node_modules/.mnote/trash` 等目录。
- [x] 防抖合并事件。
- [x] source 输出 tree delta 或触发 snapshot refresh。
- [x] 写操作自身产生的 watcher 事件不造成重复刷新风暴。
**Done 条件:**
- 外部编辑器新增 `.md` 后 page_tree 自动更新。
- 外部删除文件后 Explorer 自动更新。
- watcher 不越过 root。
**验收记录:**
- Rust 侧新增 `/api/tree/local-folder-watch`,由 `LocalFolderSource` 计算 root revision,只在可见文件/目录变化时变更 revision,并复用 root escape 与 ignore 规则。
- 浏览器端 local source 不再固定 2.5 秒 reload,而是轮询 revision,变化后防抖触发 snapshot refresh。
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖 `.mnote` 噪音不改变 revision、可见文件内容变更会改变 revision、root escape 防护。
- `scripts/task163-local-folder-unified-tree-browser-smoke.js` 覆盖外部新增 `.md` 后 page_tree 自动更新,以及外部新增/删除普通文件后 Explorer 自动更新。
### G2. Convex realtime parity
- [x] Convex 继续使用现有 tree stream。
- [x] preferred snapshot 选择不因 local source 破坏。
- [x] local source 和 Convex source 都能进入统一 tree event consumer。
- [x] source 切换时清理上一 source 的 watcher / stream。
**Done 条件:**
- local 和 convex 的实时刷新状态都可观测。
- 切换 workspace 不出现旧 source 事件污染新 workspace。
- 当前 Convex smoke 不回退。
**验收记录:**
- Convex 侧继续由 `useSidebarTreeStream` / tree stream server 处理 realtime`pnpm vitest run src/lib/tree-stream/use-sidebar-tree-stream.test.tsx src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/server.test.ts` 通过。
- local 侧通过同一 tree shell source metadata 和 revision watcher 触发 snapshot refresh,不引入第二套 UI。
- 真实浏览器 smoke 覆盖 local -> Convex source 切换,断言旧 local watcher 内容和 selection 不污染 Convex file tree。
## 8. Phase HMarkdown 编辑与写回 P2
### H1. Markdown AST 到编辑器文档
- [x] Markdown parse 成 editor document。
- [x] frontmatter 保留。
- [x] 基础块覆盖 paragraph / heading / list / quote / code / divider。
- [x] 附件链接解析为 resource reference。
- [x] 不支持块有降级显示。
**Done 条件:**
- 本地 `.md` 可进入可编辑态。
- 基础 Markdown 往返不丢主体内容。
- 不支持格式不会导致保存破坏原文件。
**验收记录:**
- `LocalFolderSource` 的 Markdown 读取侧已覆盖 heading 1-6、paragraph、bullet / numbered list、quote、fenced code、divider、相对附件链接 `media` reference;未知/不支持语法降级为 paragraph。
- `cargo test -p mnote-web local_markdown -- --nocapture` 覆盖基础块解析、附件链接解析、unsupported table 降级显示。
- 真实浏览器 smoke 打开 `docs/blocks.md`,断言 frontmatter title、列表、quote、code 与附件链接文本均可在文档页读出。
### H2. 编辑器文档写回 Markdown
- [x] 保存正文写回 `.md`
- [x] 标题编辑规则冻结:文件名 / H1 / frontmatter 如何同步。
- [x] 页面设置写入 frontmatter 或 `.mnote/page-options.json`
- [x] 保存失败保留编辑器状态并显示错误。
- [x] watcher 与保存事件去重。
**Done 条件:**
- 修改本地 `.md` 后刷新仍能看到新内容。
- 文件名、H1、frontmatter title 不互相打架。
- Convex 编辑链路不受本地写回影响。
**验收记录:**
- 正文保存通过 `/api/documents/save` 写回 `.md`,标题编辑通过 `/api/documents/title` 只写 frontmatter `title`,文件名变更仍由 Explorer rename 负责;三者职责分离。
- 页面设置写入 `.mnote/page-options.json`,不混入正文。
- `cargo test -p mnote-web local_ -- --nocapture` 覆盖本地正文、标题和页面设置写回;`local_markdown_save_preserves_frontmatter_and_writes_basic_blocks` 覆盖 frontmatter 保留与基础块 Markdown 输出。
- 真实浏览器 smoke 通过 document APIs 保存本地 Markdown,刷新后仍能看到新标题和正文;revision watcher 只在本地 root revision 变化后防抖刷新,避免固定刷新风暴。
## 9. Phase ILocal / Cloud 显式桥接 P3
### I1. Import / Export
- [x] local folder import 到 cloud workspace。
- [x] cloud workspace export 到 local folder。
- [x] 冲突策略明确。
- [x] page id 映射保留。
- [x] 附件路径和对象存储映射保留。
**Done 条件:**
- import/export 是显式动作,不是 local 打开时自动发生。
- 用户可预览影响集合。
- 失败可重试或回滚。
**验收记录:**
- P3 候选合同已冻结在 `4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- import/export 均被定义为 `preview -> execute` 显式动作,不能在打开 local folder、保存 local `.md`、watcher 或普通树操作中隐式发生。
- 冲突策略固定为 preview 输出冲突集合,execute 默认 incremental naming,不静默覆盖。
- page id 映射分别通过 `mnote_id` / `.mnote/page-ids.json``cloud_document_id``.mnote/cloud-export-map.json` 保留。
- 附件路径和对象存储 id 必须进入 export/import mapexecute 失败必须返回 retry cursor 和 rollback plan。
### I2. Publish / Sync 候选
- [x] local page publish 到 Convex public page。
- [x] 手动 sync 候选设计。
- [x] 不在 P0/P1 引入隐式双向同步。
- [x] 权限模型不混淆:local OS 权限不同于 Convex ACL。
**Done 条件:**
- 本地 workspace 不依赖 Convex 才能工作。
- Cloud 功能通过显式桥接启用。
- 没有自动双真相。
**验收记录:**
- `4-23` 固定 publish 为 `tree.bridge.publish.preview/execute`,只发布显式选择的单页或子树。
- sync 只进入 `tree.bridge.sync.preview` 候选,不提供默认 `sync.execute`,避免形成自动双真相。
- local OS 权限与 Convex ACL 在 preview 中必须单独展示,不互相推导。
- 当前实现中的 local folder 读写、watcher、Markdown 保存均不依赖 ConvexConvex 能力只通过显式 bridge 候选启用。
## 10. 全局验收矩阵
### 10.1 Contract tests
- [x] `WorkspaceSource` mock local / mock convex。
- [x] command envelope source routing。
- [x] projection source metadata。
- [x] capability-driven menu profile。
- [x] preflight result schema。
- [x] active / selection / focus reducer。
- [x] cut / copy / paste reducer。
**Done 条件:**
- 不启动浏览器也能验证核心协议。
- local/convex 至少各有一套同命令不同 executor 的测试。
- Contract tests 只负责协议、schema、reducer、payload 与 executor 分发,不替代拖拽、右键、多选、外部 drop、source 切换的真实浏览器验收。
### 10.2 Rust tests
- [x] local folder scan。
- [x] md-only page_tree。
- [x] file_tree folder first sorting。
- [x] page id persistence。
- [x] root escape 防护。
- [x] local write executor。
- [x] Convex executor regression。
**Done 条件:**
- Rust 测试覆盖 source adapter 主路径。
- 安全边界有失败用例。
- Convex 旧路径没有被本地 source 破坏。
### 10.3 Browser smoke
- [x] 打开 local folder。
- [x] 3000 `desktop:hot` 主入口覆盖打开本地文件夹:不触发原生 prompt,不跳 `/tree`,不落入 `legacy_next_compat_disabled`
- [x] Explorer 显示目录、md、普通文件。
- [x] 页面树只显示 md。
- [x] 打开 md 到文档页。
- [x] 同一套右键菜单在 local / convex 按 capability 裁剪。
- [x] 已选区内右键保留多选,未选项右键切换 selection 后开菜单,空白区点击清空 selection / focus。
- [x] `Ctrl/Cmd+Click``Shift+Click``Ctrl/Cmd+A` 多选链路。
- [x] 3000 主 Sidebar file tree 的 `Shift+Click` 范围多选由 `scripts/task164-desktop-hot-local-folder-main-entry-smoke.js` 验收。
- [x] inline rename。
- [x] `Ctrl/Cmd+C/X/V` 与菜单 Copy / Cut / Paste 都覆盖,cut decoration 出现,paste 成功后消失。
- [x] 内部拖拽覆盖单选、多选、move / copy 修饰键切换。
- [x] 内部拖拽覆盖 before / inside / after 三态 indicator。
- [x] 内部拖拽覆盖 hover auto expand。
- [x] 内部拖拽覆盖自拖自身、拖到后代、跨 workspace、只读目标四类非法场景,并断言不会发 execute。
- [x] 外部文件 drop 使用真实 `DataTransfer/File` 或等价 Playwright 文件拖入,覆盖 local folder、Convex document/asset-folder、readonly 目标、md 文件目标。
- [x] 外部文件 drop 覆盖 preflight UI、写入/上传结果、projection refresh。
- [x] watcher 外部修改刷新。
- [x] 切回 Convex workspace 后原有页面树正常。
- [x] source 切换后旧 watcher / stream 已清理,selection / focus / active 不被旧 source 污染。
- [x] local 与 Convex 上同一动作保持同一 UI 和同一命令名,但 executor 行为可不同。
- [x] 组合场景覆盖多同名 `新页面`、每页下 `index.md`、多选、右键、`F2``Ctrl/Cmd+X/V`、外部文件拖入、切回 Convex workspace。
**Done 条件:**
- smoke 覆盖 source 切换。
- smoke 覆盖 3000 主入口本地文件夹链路;不能只用 `/tree` debug shell 作为完成证据。
- smoke 覆盖至少一个同 UI、同命令、不同 executor 的动作。
- 截图中 Explorer root / active / selection / focus 可解释。
- 浏览器 smoke 必须是真实浏览器自动化,不接受纯 jsdom、DOM 单测或只跑 reducer 的结果。
- 拖拽、外部 drop、上下文菜单、hover 和键盘导航必须在浏览器里触发真实事件链,不接受手工只看截图的验收。
## 11. 最终 done 条件
只有同时满足以下条件,`4-20/4-21/4-22` 才能整体移动到 `done/`
- [x] `WorkspaceSource` 抽象落地。
- [x] `LocalFolderSource` 支持本地只读 `file_tree/page_tree/page_aggregate`
- [x] `ConvexWorkspaceSource` 继续通过同一 projection/command 消费路径工作。
- [x] Explorer P0 交互完成:active/selection/focus、keyboard、context menu、inline rename、cut/copy/paste、drag/drop。
- [x] 至少一组写操作在 local 和 convex 上共用 UI 和 command envelope。
- [x] 本地文件夹不经过 Convex 中介。
- [x] page id 不因本地 rename/move 断裂。
- [x] UI 没有 source-specific tree fork。
- [x] 所有危险写操作都有 preflight。
- [x] 浏览器 smoke 和 Rust/TS contract tests 通过。
- [x] 3000 `desktop:hot` 主入口 smoke 通过,覆盖本地文件夹入口、legacy fallback 拒绝和主 Sidebar `Shift+Click` 多选。
- [x] 拖拽、外部 drop、右键、多选、键盘、source 切换的关键路径在真实浏览器中逐项验收通过;这些 hard gate 未通过时,即使 contract/Rust 测试通过也不得移动到 `done/`
## 12. 明确非目标
- [x] 不在 P0 实现本地和云端自动同步。
- [x] 不在 P0 承诺 Markdown 与编辑器完全无损互转。
- [x] 不用本地 per-user 文件夹替代 Convex 协作权限。
- [x] 不把 VSCode 源码作为直接复制对象;只参考 Explorer 行为模型。
- [x] 不把 Convex 虚拟空间强行伪装成真实磁盘目录。
- [x] 不把 Local Folder 模式做成必须登录才能使用。
@@ -0,0 +1,117 @@
# 4-23 [process] Local / Cloud 显式桥接 P3 候选合同 v1
> 更新时间:2026-05-07
>
> 关联:
> - `4-21-local-folder-convex-unified-tree-source-v1.md`
> - `4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
## 1. 结论
Local / Cloud 桥接只允许作为显式动作出现,不进入 Local Folder 打开、读取、watcher、保存、拖拽或普通树操作主链。
固定候选动作:
- `tree.bridge.import.preview`
- `tree.bridge.import.execute`
- `tree.bridge.export.preview`
- `tree.bridge.export.execute`
- `tree.bridge.publish.preview`
- `tree.bridge.publish.execute`
- `tree.bridge.sync.preview`
`sync.execute` 不进入 P3 默认范围;P3 只冻结候选 preview 和手动执行边界。
## 2. Import Local Folder To Cloud
输入:
- `sourceKind=local_folder`
- `rootUri=file://...`
- `targetWorkspaceId=ws_...`
- `selection=rowIds | all`
preview 必须输出:
- 将创建的 cloud documents。
- 将上传的附件。
- 本地 `mnote_id` / `.mnote/page-ids.json` 到 Convex document id 的映射策略。
- 冲突列表:同名页面、重复 `mnote_id`、不可读文件、超限附件。
- 回滚计划:已创建 document id、已上传 storage id、可删除顺序。
execute 规则:
- 必须引用 preview id。
- 必须幂等。
- 失败时返回可重试 cursor 和 rollback plan。
- 不允许修改 local root,除非用户显式要求写入 cloud id mapping。
## 3. Cloud Workspace Export To Local Folder
输入:
- `sourceKind=convex_workspace`
- `workspaceId=ws_...`
- `targetRootUri=file://...`
- `selection=rowIds | all`
preview 必须输出:
- 将写入的 `.md` 文件路径。
- 将下载的附件路径。
- Convex document id / asset id 到本地路径的映射。
- 冲突策略:默认 incremental naming,不静默覆盖。
- 回滚计划:本次新建文件、覆盖前备份、下载失败项。
execute 规则:
- 必须引用 preview id。
- Markdown frontmatter 必须保留 `mnote_id``cloud_document_id`
- 附件路径和对象存储 id 写入 `.mnote/cloud-export-map.json`
- 失败时不删除已存在用户文件;只删除本次创建且仍未被用户修改的文件。
## 4. Publish Local Page To Cloud
输入:
- `sourceKind=local_folder`
- `rootUri=file://...`
- `documentId=local-md...`
- `targetWorkspaceId=ws_...`
- `visibility=private | workspace | public`
preview 必须输出:
- 将创建或更新的 cloud page。
- 页面正文、frontmatter、附件引用的转换计划。
- 权限差异说明:local OS 权限不等于 Convex ACL。
execute 规则:
- 只发布选中的单页或显式选择的子树。
- 不启动双向同步。
- 发布结果写入 `.mnote/publish-map.json` 时必须经用户确认。
## 5. Manual Sync Candidate
sync 只能作为候选 preview
- 比较 local revision、cloud domain event cursor 和 mapping。
- 输出三方差异:local-only、cloud-only、conflict。
- 不自动执行。
禁止项:
- 打开 local folder 时自动 import。
- 保存 local `.md` 时自动上传 Convex。
- Convex stream 事件自动改写本地文件。
- 把 OS 文件权限解释为 Convex ACL。
## 6. Done Gate
P3 进入实现前必须先满足:
- P0/P1/P2 的 local 和 convex 树操作 smoke 全绿。
- preview UI 已复用统一 preflight/result 组件。
- import/export/publish 均有 rollback plan。
- 至少一个真实浏览器 smoke 覆盖 preview,不允许直接跳 execute。