Files
mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md
T
lix-2026 47e224d419 chore: align sqlite control plane architecture
- replace default Convex control-plane wording with Rust SQLite control-plane across architecture, AGENTS, Reasonix, and design docs

- retire root Convex functions source and deploy script into recycle while keeping explicit cloud/compat/sync-replica boundaries

- add control-plane migration guard/docs and keep CodeGraph refreshed after the SQLite control-plane cutover
2026-05-22 17:45:22 +08:00

359 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2-1 [done] 页面块存储与投影对齐方案 v1
> 更新时间:2026-05-16
>
> 当前状态:`DONE`。
>
> 2026-05-22 口径补充:本文是 2026-05-18 前后的阶段性 Convex-backed snapshot 对齐记录;当前默认正文真相已收口到 local-first `.md` / Page Aggregate 投影,Rust SQLite control-plane 承接默认控制面,Convex 仅保留历史迁移源、显式 cloud source / compat / sync replica 边界。
>
> 关联文档:
> - `/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/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
>
> 代码依据:
> - `/mnt/Data1T/mnote/wolai-frontend/convex/schema.ts`
> - `/mnt/Data1T/mnote/wolai-frontend/convex/documents.ts`
> - `/mnt/Data1T/mnote/wolai-frontend/convex/blocks.ts`
> - `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs`
> - `/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs`
---
## 1. 结论
页面块 AI 工具要可读、可写、可移动,必须先把三层对齐:
```text
Rust canonical block model
<-> Page Aggregate block projection
<-> Convex persisted page body snapshot
```
当前真实持久化主链仍是:
```text
documents.content
documents.content_revision
documents.content_conflict_key
```
`blocks` 表存在,但当前 schema 没有 `order_key` 字段,mutation 也没有维护父子排序和正文 snapshot 的一致性;因此第一阶段不能把 `blocks` 表描述为页面正文事实源。
第一阶段正确策略是:
> **历史阶段策略是 Rust 先持有块语义和命令计划,Convex 保存 canonical snapshot 到 `documents.content`Page Aggregate 从 snapshot 投影出稳定 block view。当前默认策略已继续演进为 local-first `.md` 正文真相 + Rust SQLite control-plane 默认控制面;Convex snapshot 只作为显式 cloud / compat source。**
---
## 2. 当前存储状态
### 2.1 `documents` 表
`wolai-frontend/convex/schema.ts``documents` 保存:
- `content: v.any()`
- `content_revision: v.optional(v.number())`
- `content_conflict_key: v.optional(v.union(v.string(), v.null()))`
- `raw_text`
- 页面标题、父级、排序、页面设置、统计等字段
`documents.ts``updateContent` / `updateDocumentContentRecord` 已经支持:
- `expectedRevision`
- `conflictDetectionKey`
- revision 自增
- conflict key 更新为 `${documentId}:${nextRevision}`
- raw text 回写
- ingest job debounce
这说明当前页面正文冲突检测已经有页面级版本基础。
### 2.2 `blocks` 表
`blocks` 表当前字段:
- `id`
- `workspace_id`
- `page_id`
- `parent_block_id`
- `type`
- `props`
- `content`
- `child_block_ids`
- `created_by`
- `created_at`
- `updated_at`
当前问题:
- schema 没有 `order_key`,但 `blocks.ts` 的 append/insertAfter 入参包含 `orderKey` 却未持久化。
- `insertAfter` 只插入新 block,不维护 sibling order。
- `replace/remove` 操作的是 `blocks` 表,不会同步 `documents.content`
- storage mapping 中旧 `insert_block/update_block/move_block/delete_block` 指向 blocks 类命令,但当前长期页面正文保存仍映射到 `documents:updateContent`
因此 `blocks` 表当前只能视为历史/辅助/实验 substrate,不是页面正文单一真源。
### 2.3 Rust command 到 Convex 的映射
`storage-convex-bridge` 当前将:
- `documents.save`
- `page.body.save`
- `blocks.patch`
- `tree.node.embed`
映射到 `documents:updateContent`
这说明当前正式页面正文写入边界仍是 `page.body.save -> documents:updateContent`
---
## 3. 对齐目标
### 3.1 短期目标
短期固定为:
```text
AI tool / editor command
-> Rust EditorCommand
-> canonical EditorBlockDocument
-> legacy content snapshot
-> page.body.save
-> documents:updateContent
-> Page Aggregate reload
```
允许:
- 底层整份 snapshot 回写。
- `documents.content` 继续是 persisted body。
- `blocks` 表保持只读参考或历史兼容。
不允许:
- AI 直接写 `documents.content` 私有数组。
- AI 直接写 `blocks` 表并期待页面正文自动变化。
- 前端临时 Tiptap JSON 成为存储事实源。
### 3.2 中期目标
中期应让 `PageAggregate.page_body` 输出:
- `content`
- `revision`
- `conflictDetectionKey`
- `blockDocument`
- `blockProjectionVersion`
- `projectionSource`
并保证:
- `blockDocument` 可由 `documents.content` 可靠生成。
- `blockDocument` 可生成 canonical `documents.content`
- 保存后 `/api/page-aggregate/:id``mnote.doc.fetch` 看到相同 block ids。
- editor session 能基于 revision 刷新。
### 3.3 长期目标
长期如果升级 `blocks` 表为写主链,必须先满足:
- `blocks` 表补齐 `order_key` 或等价排序字段。
- `blocks` 表与 `documents.content` 的关系明确:谁是事实源,谁是 materialized snapshot。
- 每个 block write 都能产出 page body revision。
- 页面读取、搜索、引用、评论、历史、实时事件都消费同一投影。
- 有迁移、回填、校验和回滚方案。
在这些完成前,不把 `blocks` 表提升为页面正文事实源。
---
## 4. Revision 与 Conflict Key
### 4.1 页面级 revision
页面正文当前使用:
```text
content_revision: number
content_conflict_key: `${documentId}:${revision}`
```
AI 写工具必须携带:
- `expectedRevision`
- `conflictDetectionKey`
- `idempotencyKey`
- `dryRun`
缺少 `expectedRevision``conflictDetectionKey` 时:
- 只允许 dry-run。
- 不允许真实写入。
### 4.2 块级 revisionRef
块级工具需要额外返回 `revisionRef`
```text
pageRev:{revision}:block:{blockId}:hash:{hash}
```
用途:
- 防止 AI 基于旧块内容写入。
- 防止移动时 anchor 已改变。
- 帮助 dry-run 返回精确 warning。
说明:
- `hash` 可以用 canonical block JSON 或文本/结构摘要计算。
- Tiptap AI Toolkit `_hash` 可作为参考,但不能替代 mnote `blockId`
- 第一阶段 `revisionRef` 不需要单独持久化,可以由 Page Aggregate projection 生成。
### 4.3 冲突粒度
第一阶段冲突粒度:
- 写入仍以页面级 revision 为硬门槛。
- 块级 `revisionRef` 作为额外预检和 warning 来源。
后续如果引入块级持久化:
- 可以把块级 revision 升级为硬冲突键。
- 但仍必须推进 page body revision,保证页面快照和实时事件一致。
---
## 5. Projection 对齐
### 5.1 Page Aggregate 输出
`PageAggregate.page_body` 应从当前:
```json
{
"content": [],
"revision": 12,
"conflictDetectionKey": "doc_1:12"
}
```
扩展为:
```json
{
"content": [],
"revision": 12,
"conflictDetectionKey": "doc_1:12",
"blockDocument": {
"documentId": "doc_1",
"rootBlockIds": [],
"blocks": []
},
"blockProjectionVersion": 1,
"projectionSource": "documents.content"
}
```
### 5.2 投影来源
投影来源必须显式标记:
- `documents.content`:从当前 persisted snapshot 生成。
- `editorDocument`:从 canonical editor document 直接生成。
- `tiptapDocument`:从 Tiptap JSON bridge 生成。
- `blocksTable`:仅在未来 blocks 表成为受控投影或主链后使用。
### 5.3 Page Aggregate 与 AI 工具一致性
`mnote.doc.fetch``mnote.block.fetch``mnote.block.replace``mnote.block.insert_after``mnote.block.move_after` 都必须以 Page Aggregate block projection 为读来源。
写入完成后必须验证三处一致:
- `/api/page-aggregate/:id`
- 当前 editor session 可见内容
- 再次 `mnote.doc.fetch` / `mnote.block.fetch`
---
## 6. Editor Session Refresh
块写工具成功后,必须解决“后端已写、前端编辑器还停在旧文档”的问题。
第一阶段可选策略:
1. 触发 page aggregate reload。
2. 向当前页面 stream/event 发 `page.body.saved` 或等价 body refresh event。
3. editor island 收到 revision 变化后 reload canonical content。
最低要求:
- 写工具返回新 revision。
- 前端能检测当前 editor session revision 已落后。
- smoke 中必须证明刷新页面后内容仍在。
不得把“Convex 已保存”当成用户可见编辑闭环。
---
## 7. 实施顺序
### Phase 1snapshot canonicalization
- [ ]`documents.content` 稳定生成 `EditorBlockDocument`
- [ ]`EditorBlockDocument` 稳定生成 canonical `documents.content`
- [ ] 为每个可编辑块生成 `blockId/type/text/attrs/children/order/path/revisionRef`
- [ ] Page Aggregate 输出 block projection。
### Phase 2read projection
- [ ] `mnote.doc.fetch scope=block/full/outline/keyword` 消费 Page Aggregate。
- [ ] `mnote.block.fetch` 消费 Page Aggregate。
- [ ] 返回 page revision、conflict key、block revision refs。
### Phase 3write through Rust command
- [ ] `mnote.block.replace` 生成 `EditorCommand::ReplaceBlock`
- [ ] `mnote.block.insert_after` 生成 `EditorCommand::InsertBlockAfter`
- [ ] Rust 将命令应用到 canonical `EditorBlockDocument`
- [ ] 生成 canonical content 后走 `page.body.save -> documents:updateContent`
### Phase 4move gate
- [ ] `mnote.block.move_after` 先只做同父级叶子块 dry-run。
- [ ] 同父级移动真实写入通过后再考虑标题子树、列表、复杂块。
### Phase 5blocks 表评估
- [ ]`order_key` 设计。
- [ ] 明确 `blocks` 表是主链还是 materialized projection。
- [ ] 设计迁移和双写校验。
- [ ] 通过后再移动长期写主链。
---
## 8. 不做清单
- [ ] 不让 AI 直接调用 `documents:updateContent`
- [ ] 不让 AI 直接调用 `blocks:*` mutation。
- [ ] 不把 `blocks` 表描述为当前页面正文事实源。
- [ ] 不把 Tiptap `UniqueID``_hash` 当作业务 block id。
- [ ] 不在没有 page revision 的情况下真实写入。
- [ ] 不在 editor session refresh 未闭环时宣布块写工具完成。
---
## 9. 完成定义
本文已归档为 `DONE`,done 边界是“页面块存储与投影关系已经冻结为当前代码可执行合同”;后续更宽执行验收继续由 `7-10` 承接。
- [x] Page Aggregate 输出稳定 block projection。
- [x] `documents.content -> EditorBlockDocument -> documents.content` 有测试覆盖。
- [x] `mnote.block.fetch` 能读取 projection 中任意可编辑块。
- [x] `mnote.block.replace``mnote.block.insert_after` 已进入真实页面最小 smoke。
- [x] 写入后 revision/conflict key 更新,刷新页面和 AI 回读一致的最小闭环已有 smoke 证据。
- [x] `blocks` 表当前定位在代码和设计中不再被误称为正文主链;当前正文主链仍是 `documents.content` / Page Aggregate block projection 过渡态。