feat: align local-first workspace direction

Document the VSCode-like local-first product shape, demote Convex to a control-plane role, and retire stale architecture drafts.

Add local workspace migration/export references plus smoke coverage for no-Convex managed workspace startup, local markdown title/body/options persistence, asset upload behavior, and Convex fixture export.

Verification: git diff --cached --check; node scripts/check-local-first-convex-guard.js --staged; node scripts/task444-convex-workspace-export-local-fixture-smoke.js; node scripts/task166-local-first-managed-workspace-no-convex-smoke.js; node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
This commit is contained in:
lix-2026
2026-05-19 08:11:58 +08:00
parent 68d321e297
commit cdff672aa5
67 changed files with 5242 additions and 932 deletions
@@ -1,6 +1,14 @@
# 5-5 [process] 主编辑区与树域单一真源对齐方案 v1
> 更新时间:2026-05-09
> 更新时间:2026-05-18
>
> 2026-05-18 口径补充:
> - 页面聚合的默认 source 已开始向 local-first workspace 收口;页面正文、标题、设置和上传资源不应再默认把 Convex 视作主数据层。
> - 本文中涉及 Convex 的表述只应理解为 local-first 与 Convex-backed 两种 `WorkspaceSource` 的过渡兼容背景,不应再作为默认产品形态解释。
>
> 2026-05-19 口径补充:
> - local-first 下,本地 `.md` 文件是正文真相;Page Aggregate / EditorBlockDocument / tiptap state 都是投影或工作副本。
> - `documents/save` 只能作为 compat adapter,不能继续承担长期写侧仲裁。AI 后台写入、tiptap 保存、外部编辑器修改必须统一到本地文件版本冲突模型。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
@@ -34,7 +42,7 @@
它现在更接近于:
> **`Rust snapshot 主读链 + Convex-backed + 前端本地 state 仍存在` 的混合态。**
> **`Rust snapshot 主读链 + local-first / Convex-backed 双 source 过渡 + 前端本地 state 仍存在` 的混合态。**
因此,当前看到的这些“小问题”:
@@ -79,18 +87,22 @@
- 要不要 `Tiptap`
- 要不要 `leptos-tiptap`
- 要不要继续保留 Convex
- 要不要继续保留 Convex 作为控制面和可选同步协作 source
这些结论都已经足够明确:
- `Tiptap` 继续作为浏览器输入 runtime
- `leptos-tiptap` 继续作为 Leptos 内正式 editor island 接缝
- `Convex` 继续作为当前存储 / 实时 / 协作底座
- `local_folder` 作为默认页面数据真相,Convex / 服务端退居控制面和可选同步协作 source
当前真正要收口的是:
> **页面这一层,到底由谁持有聚合语义,前端到底应该消费什么,编辑器到底应该回发什么。**
local-first 后,答案进一步收紧为:
> **正文真相由本地 Markdown 文件持有;Page Aggregate 负责把文件投影成 UI 和 editor runtime 所需结构;写侧必须围绕文件版本做仲裁,而不是继续让 `documents/save` 兼容面决定谁的 revision 有效。**
### 2.3 后续主线应固定为 Page Aggregate,而不是继续零散补洞
从长期架构看,当前主线不应再描述成:
@@ -238,6 +250,41 @@
> **Rust bridge 已经进入主链,但 page aggregate command cutover 仍未完成。**
### 3.1.5 写侧冲突不能继续藏在 `documents/save` 后面
当前读侧已优先消费 Rust `mnote.page_aggregate.v1` snapshot,但写侧仍有明显兼容痕迹:
- tiptap 保存仍通过 `/api/documents/save` 兼容入口进入 `page.body.save`
- AI 兼容工具写正文时也可能最终进入同一条保存链
- local-first 下 agent 还会直接修改 `.md` 文件
如果继续让这些写入都挤在 `documents/save` 兼容面后面,会再次出现:
- 谁拥有最新 revision
- AI 写入是否覆盖了用户未保存编辑
- tiptap autosave 是否覆盖了 agent 刚写回的文件
- Page Aggregate 读到的是旧 content、EditorBlockDocument 还是最新 `.md`
因此写侧必须改成 VSCode-like 文件版本模型:
```text
current .md file
-> fileVersion = hash + mtime + size
-> Page Aggregate snapshot 带 baseFileVersion
-> tiptap 工作副本记录 baseFileVersion 与 dirty 状态
-> AI / 外部编辑器写入触发 watcher
-> clean editor 自动 reloaddirty editor 进入 conflict state
```
这意味着 `EditorBlockDocument` 原生落库闭环不能理解成“再建一份新的正文真相”。local-first 下更合理的定位是:
- `.md` 是 canonical body
- `EditorBlockDocument` 是 runtime-native projection / cache
- `.mnote/cache/<page-id>.editor.json` 或内存缓存可用于加速和保留编辑器特有信息,但必须带 `sourceFileVersion`
- 当 source file version 不匹配时,cache 失效并重新从 `.md` 投影
长期命令面应从 `/api/documents/save` compat route 收口到 `page.body.write` / LocalFS executor,并显式接收 `expectedFileVersion`。任何不带 expected version 的正文写入都只能进入 compat / import 路径,不能作为自动保存主链。
---
## 4. 为什么这些问题会直接影响长期方向
@@ -387,11 +434,12 @@ Rust 侧需要提供一个统一的页面聚合投影,至少包含:
### 5.5 Convex 的正确定位
当前不应该先拆 Convex。
当前不应该把 Convex 当作默认页面主数据层继续扩写,也不应该无计划硬拆已有 Convex 路径
正确口径是:
- Convex 继续作为存储 / 实时 / 协作底座
- local folder 是早期产品默认页面数据真相
- Convex / 服务端继续作为账号、分享、同步、协作和远端副本控制面
- Rust 持有 canonical contract 与语义编排
- Leptos / Next 负责消费 projection 与呈现 island
@@ -401,7 +449,7 @@ Rust 侧需要提供一个统一的页面聚合投影,至少包含:
而是:
- **Rust 持有语义单一真源,Convex 持有当前持久化底座。**
- **Rust 持有语义单一真源,local-first workspace 持有默认数据真相,Convex 只作为控制面和可选同步协作 source。**
---
@@ -1,6 +1,15 @@
# 5-6 [process] Page Aggregate 单一真源对齐执行清单 v1
> 更新时间:2026-05-09
> 更新时间:2026-05-18
>
> 2026-05-18 口径补充:
> - 本清单继续有效,但 Page Aggregate 的默认落点调整为 local-first workspace;本地 `.md`、`.mnote/page-options.json` / frontmatter 和 `{mdBase}.assets/` 是早期产品默认闭环。
> - Convex-backed Page Aggregate 只作为兼容 / cloud source 路径,不再作为新增能力默认主存储目标。
>
> 2026-05-19 口径补充:
> - local-first 下 `.md` 文件是正文真相,`EditorBlockDocument` 是 runtime-native projection / cache,不是第二份正文真相。
> - 写侧必须退出 `/api/documents/save` 长期主路径,收口到带 `expectedFileVersion` 的 `page.body.write` / LocalFS executor。
> - AI 直改文件、tiptap autosave、外部编辑器修改必须共用 VSCode-like 文件版本冲突模型。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
@@ -34,13 +43,16 @@
- [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。
- [x] 当前主编辑区已具备可继续推进的基础交互能力。
- [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。
- [x] local-first 正文真相已明确为 `.md` 文件;tiptap / EditorBlockDocument / Page Aggregate 均不再被描述为独立正文真相。
### 2.2 还没有成立的事实
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
- [ ] 写侧还没有完全退出 `/api/documents/save` 兼容面;`page.body.write` / LocalFS executor 与文件版本冲突模型仍待落地。
- [ ] tiptap dirty 状态与 AI / 外部文件变更之间还没有完整 VSCode-like conflict UI。
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
- [ ] AI 写入口还没有完整对齐 page aggregate command family
- [ ] AI 写入口还没有完整对齐“授权文件引用 + 白名单目录 + 后台文件写入 + 前台同步”模型
补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter``/api/documents/title``/api/documents/options``/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。
@@ -58,6 +70,8 @@
- [ ] 不先扩一批和 page aggregate 无关的编辑器花活。
- [ ] 不先争论替换 `Tiptap`、替换 `leptos-tiptap`、移除 Convex。
补充:当前也不打算把 `documents/save` 继续扩成正文真相主入口。local-first 下它最多是 compat adapter;长期写侧必须按本地文件版本号仲裁,像 VSCode 一样处理 dirty / clean / external change 冲突。
---
## 4. Phase F:冻结 Page Aggregate Contract
@@ -87,6 +101,12 @@
补充:2026-05-16 在 `http://127.0.0.1:3000` 的 Rust `mnote-web` 主入口实跑 Page Aggregate smoke。`task110-page-title-single-truth-smoke.js` 验证标题修改后页头、Breadcrumb、Sidebar、Page Tree、File Tree 与刷新后标题一致,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/task110.stdout.json`。文档打开 smoke 验证文档 HTML 包含 `data-page-aggregate-snapshot="mnote.page_aggregate.v1"``data-page-tree-source="page_aggregate.tree.pageSubtree"`,同一临时页 `/api/page-aggregate/:id` 回读 `schema=mnote.page_aggregate.v1`,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-open-snapshot.stdout.json``page-open-snapshot.png`。字段完整性 smoke 验证 `identity/head/body/tree/stats` 全部存在,证据为 `tmp/page-aggregate-single-truth-smoke/20260516-182244/page-aggregate-fields.stdout.json`。当前 `projectionSource=documents.content`,仍符合“Rust-first 读取链、非 kernel-native 落库完成态”的过渡口径。
补充:2026-05-18 继续推进 Page Aggregate 读侧块文档优先级。`build_page_aggregate_projection_result` 现在按 `editorDocument -> blockDocument -> documents.content` 构建 `body.blockDocument`,并用 `projectionSource` 暴露真实来源;同时 `body.content` 也会按原生来源重建,避免编辑器/session 下游继续从旧 legacy content 初始化。新增 `page_aggregate_get_prefers_editor_document_over_legacy_content``page_aggregate_get_projects_body_content_from_block_document_source` 与非法原生块文档防回退测试。验证命令:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate -- --nocapture``cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate -- --nocapture`。这仍不等于 EditorBlockDocument 原生落库完成,`documents:updateContent` 兼容写入与底层 schema 收口继续留在后续阶段。
补充:2026-05-18 同步推进 `page.body.save` 写侧原生快照保留。`mnote-web` transport 不再在调用 `documents:updateContent` 前剥离 `editorDocument / tiptapDocument`,只剥离 Rust artifact 事件字段。验证命令:`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_keeps_editor_runtime_fields_for_page_body_save -- --nocapture`。真实部署时仍需确认当前 Convex substrate 的 schema/mutation 已接受并返回这两个字段。
补充:2026-05-18 继续收紧 `page.body.save` 原生快照失败语义。`editorDocument` 一旦出现在保存 payload 中就必须可解析;不可解析时返回 validation error,不再静默回退到 `tiptapDocument` 或 legacy `content`。验证命令:`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_command_plan -- --nocapture`
补充:2026-05-16 继续新增 `scripts/task-page-aggregate-body-sync-smoke.js`,验证真实页面正文编辑后 `/api/documents/save``/api/page-aggregate/:id` 回读闭环。证据为 `tmp/page-aggregate-body-sync-smoke/mp87mgz7.json``mp87mgz7.png``body.revision``0``1``body.conflictDetectionKey``tree_1778927703753_1:0``tree_1778927703753_1:1``body.blockDocument.blocks[0]` 回读到新段落文本和 `revisionRef`。配套命令 `cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_save_route_executes_page_body_save_command``cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate_get_projects_legacy_content_to_block_document` 已通过。
### 4.3 退出标准
@@ -270,11 +290,12 @@
### 8.3 退出标准
- [x] AI 写入口已经可以被明确描述为“操作 page aggregate command family”,而不是“绕过系统写编辑器”。
- [x] AI 写入口已经可以被明确描述为“必须经过授权 scope 与页面写入仲裁”,而不是“绕过系统写编辑器”。
- [ ] local-first 下,普通正文 AI 写入必须进一步落到“授权文件 patch 或带 `expectedFileVersion``page.body.write`”,不能继续以 `/api/documents/save` 兼容面作为长期验收终点。
注:历史 `/api/ai-agent/run``Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。2026-05-14 起这只保留为过渡证据;新的主线已由 Hermes 发起 mnote plugin tool call,再由 Rust runtime / kernel 返回 tool result。其后:
- `doc_insert_blocks / doc_replace_range` 继续`page.body.save` 语义落到 `/api/documents/save`,再正式回显主编辑区 island
- `doc_insert_blocks / doc_replace_range` 这类历史 AI 写入曾`page.body.save` 语义落到 `/api/documents/save`local-first 新口径下,该链路只作为 compat 证据,后续普通正文写入应改为授权文件 patch 或带 `expectedFileVersion``page.body.write`
- `slash_run(rename current page)` 会把结构化结果回接到当前页 `DocumentContent` 的同一条标题提交链,并继续广播 `emitDocumentsChanged(documentId)`,因此页头标题与树标题不再靠 AI 面板内部本地状态各自漂移。
- 当前 AI 面板已经能消费结构化 `update_page_options` 结果,并把 `pageOptionsPatch` 回接到当前页 `DocumentContent` 的同一条 `patch_page_options + page.layout.updateOptions` 提交链;同时只允许 `runtimeSupport === "wired"` 的字段进入正式写回,避免 planned / ui_only 页面设置混入主链。对应最小回归测试为 `DocumentAiAgentPanel.runtime.test.tsx``document-content.test.ts`
- 历史 `mnote-cli host` 能在命中页面设置 patch 时直接执行 `page.layout.updateOptions`,并向前端回放结构化 `tool_call/tool_result` 事件;这意味着“服务端完全没有页面设置结构化写回结果”的状态已经结束。
@@ -9,7 +9,8 @@
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
> - `/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/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
> - `/mnt/Data1T/mnote/design/old/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`(历史过渡背景)
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
@@ -1,6 +1,10 @@
# 5-9 [process] Wolai-aline 连续执行 checklist v1
> 更新时间:2026-04-30
> 更新时间:2026-05-18
>
> 2026-05-18 口径补充:
> - 本 checklist 的 Wolai 体验对标仍有效,但数据真相口径跟随 local-first workspace:本地 `.md` 是默认正文真相,Convex-backed 路径只作为兼容 / cloud source。
> - 历史条目中“写回 Convex-backed 持久化底座”的表述只代表当时 smoke 的在线路径,不再作为新增编辑能力默认目标。
>
> 本清单拆自 `5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`。它不是新的测试方案;执行口径统一服从 `/home/lix/.codex/skills/wolai-aline` 与 `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`。