Files
mnote/bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md
T
lix-2026 96e03645f7 chore: 收口 review 执行清单与 runtime 验证
- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录

- 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目

- 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑

- 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
2026-05-14 05:52:08 +08:00

294 lines
24 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.
# 5-11 [done][bug] Mindmap 幽灵附件增长与树命令卡顿 v1
> 更新时间:2026-05-14
>
> 分类归属:
> - `05-editor-mainline/process`
> - 涉及边界:`04-tree-domain/tree command + file_tree projection`、`06-mindmap/runtime save + resource relation`
>
> 用户证据:
> - `/mnt/Data1T/mnote/tmp/image copy 94.png`
> - `/mnt/Data1T/mnote/tmp/image copy 93.png`
## 1. 问题定义
用户在页面中只是修改了一下 mindmap,文件树/资源树下却陆续出现多个 `mindmap-mindmap...` 附件行。截图显示同一页面 `新页面3` 下有 `index.md`,并且 mindmap 附件从 2 条增长到 4 条。
同一轮反馈还指出:删除页面和新建页面都很慢,表现为树操作后明显卡顿。
这不是单纯的图标显示问题。当前症状同时暴露两条链路风险:
1. mindmap 编辑/初始化/保存链路会多次向资产层广播同一个资源存在,且缺少“页面正文 block 与 mindmap asset 关系唯一”的硬约束。
2. tree shell 的页面新建、删除、重命名、移动仍在命令成功后强制整页刷新,和当前 live projection/SSE 机制重复,导致用户感知卡顿。
## 2. 真实现象
已观察到的用户现象:
1. 新页面下初始只有 `index.md` 和少量 mindmap 附件。
2. 用户只是编辑 mindmap,不是主动新建 mindmap。
3. 等一会儿或再次修改后,同一页面下又出现新的 `mindmap-mindmap...` 行。
4. 页面新建和删除动作响应慢,像是页面/树整体重新加载。
期望结果:
1. 一个页面内的一个 mindmap block 只对应一个稳定 `mindmapId` 和一个 file tree asset row。
2. mindmap 保存只能更新既有资源,不应创建新的 mindmap 资产行。
3. file tree projection 应按 `{documentId, blockId, assetId}` 或明确 object identity 去重。
4. 页面新建/删除成功后应优先消费 command result / tree delta 更新局部投影,不应默认整页 reload。
## 3. 初步调查证据
### 3.1 mindmap 资产广播存在多入口
`wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx` 中同一个 mindmap 资源至少有三处会触发资产刷新广播:
- 保存成功后广播 `emitAssetsChanged(docId, { id: mindmapId, asset_type: "mindmap", ... })``MindmapBlock.tsx:1499``:1534`
- 初始同步 `createOnly: true` 成功后广播:`MindmapBlock.tsx:1591``:1621`
- mindmap 实例就绪后立即广播:`MindmapBlock.tsx:1636``:1647`
另一个 legacy/compat block wrapper 也会在 mount 时执行 `createOnly` 并广播资产:`MindmapBlock.tsx:3573``:3612`
这些广播本身用 `id = mindmapId`,理论上同 ID 会被 sidebar 本地 state 去重;但只要保存/转换链路让同一视觉 mindmap 换了新的 `mindmapId`,就会生成新的资产行。
### 3.2 mindmapId 仍可能由时间戳生成
当前 `leptos-tiptap` 插入 mindmap 时使用:
- `rust/spikes/leptos-tiptap-spike/src/lib.rs:5681``:5689`
这里 `next_mindmap_id()` 生成 `mindmap_{Date.now()}`,并写入 paragraph attrs 的 `mindmapId`。如果后续转换、保存、重新挂载中丢失原 attrs,fallback 会用新的 block identity / 新插入节点创建新的 `mindmapId`,资产层就会认为这是另一个 mindmap。
相关转换锚点:
- `rust/crates/mnote-web/src/routes/web_shell.rs:818``:838`legacy block 转 TipTap 时写入 `mindmapId`
- `rust/crates/mnote-web/src/routes/web_shell.rs:983``:1009`TipTap 节点转 editor block 时若 attrs 缺失则用 blockId fallback
- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts:188``:213``mindmapReferenceProps` 会在缺少 `mindmapId` 时 fallback 到 blockId
- `wolai-frontend/src/lib/documents/tiptap-content-converter.ts:410``:418`editor block 转 TipTap 时把 mindmap props 写回 paragraph attrs
当前缺少一条回归断言:连续编辑同一个 mindmap 后,保存前后 `mindmapId` 必须保持不变,且 file tree 下同一页面 mindmap asset 数量不增长。
### 3.3 Convex mindmaps 表允许同页多 mindmap,但缺少 block 关系唯一约束
`wolai-frontend/convex/schema.ts:164``:183` 定义 mindmaps 表,并以 `(document_id, mindmap_id)` 做查询索引。注意这里是普通索引,不是唯一约束。
`wolai-frontend/convex/mindmaps.ts``put` 对同一 `(document_id, mindmap_id)` 是幂等 patch/insert,但它并不知道页面正文中的哪个 block 才是唯一来源。也就是说:
- 同一个 `mindmapId` 重复保存不会多插入。
- 如果历史或并发路径已经写出同一 `(document_id, mindmap_id)` 的多行,`put` 当前用 `.first()` 只会 patch 第一行,剩余重复行仍会被后续 list/projection 展示。
- 但如果前端生成了新的 `mindmapId`,后端会按合法新 mindmap 插入。
- file tree 会把同一 document 下所有 active mindmaps 映射为资产行。
对应映射:
- `wolai-frontend/convex/mindmaps.ts:206``:245``put` 查询 `by_doc_mindmap``.first()`,不存在则 insert
- `wolai-frontend/convex/sidebar.ts:79``:100``mindmap_id` 被映射为 `id``mindmap-{mindmap_id}.json`
- `wolai-frontend/convex/sidebar.ts:193``:220`:所有 active mindmaps 都进入 `mindmap_assets`
- `rust/crates/bridge-runtime/src/lib.rs:7569``:7647`file tree projection 从 `mindmap_assets` 构建资源行,并只按 asset id 去重
因此,当前有两个需要实测区分的分支:
1. 同一视觉对象被保存成多个不同 `mindmapId`,每个都被合法展示为一个资源。
2. 数据表中已经存在同一 `(document_id, mindmap_id)` 多行,`.first()` 更新掩盖重复行,sidebar list 把重复行全部暴露出来。
两者都会在截图中表现为同一页面下多个 `mindmap-mindmap...` 行。
### 3.4 Rust / Next API 都会把 mindmap 保存转成 tree resync
保存路径还会触发树刷新:
- Next API `POST /api/mindmap/[docId]/[mindmapId]` 构造 `mindmaps.put``mindmap.command.apply``wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts:229``:254`
- Rust API 普通保存也包装为 `mindmaps.put` 并携带 `createOnly``rust/crates/mnote-web/src/routes/mindmap_api.rs:177``:200`
- Bridge runtime 给 `mindmaps.put` 生成 `resync_required` 树事件:`rust/crates/bridge-runtime/src/lib.rs:9273``:9326`
这意味着 mindmap 每次保存都会推动资源树重新读 projection;如果底层 mindmaps 数据已经重复,保存/刷新会把重复行显性化。
### 3.5 mindmap 节点编辑 artifact 语义过宽
当前 `mindmaps.put` / `mindmap.command.apply` 已经会进入 artifact 链路,但普通导图节点编辑生成的是 tree resource 语义:
- `tree.resource.mindmap.put`
- `tree.resource.mindmap.updated`
- `streamDelta: resync_required`
这会把“导图 object 内部内容变化”提升成“资源树需要重新同步”。从 `design/10-review/05-tree.md` 的对象边界看,mindmap 是页面内 block 关联的独立 object / asset editor,不是 `index.md` 正文替身;普通节点文字、结构、样式变化应属于 mindmap object 内容事件,而不是 file tree 资源关系事件。
因此,artifact 本身仍然需要,但语义需要分层:
1. 修改 mindmap 节点、文字、样式、折叠状态:应记录 `mindmap.content.updated` / `mindmap.node.updated` 这类 object 内容事件,不应默认触发 file tree resource resync。
2. 创建、挂载、解绑、删除、移动、重命名 mindmap 资源:才应记录 `tree.resource.*` / `tree.asset.*` 事件,并影响 file tree projection。
3. `resync_required` 只能作为无法应用精确 delta 时的保守 fallback,不应成为普通节点编辑的默认输出。
当前过宽 artifact 会放大本 bug:只要底层存在 ghost mindmap 行,或同一视觉 mindmap 被重新分配了新的 `mindmapId`,普通编辑触发的 tree resync 就会把重复资源更快暴露到文件树,并带来额外刷新成本。
### 3.6 次级入口收口进展
2026-05-14 已完成 `design/10-review` 第 9 项的入口一致性收口:
- Mindmap toolbar `export` 不再映射到未受控的 `EXPORT` runtime commandUI state 继续禁用该入口。
-`/api/mindmap-ai/expand-node` Next route 从“有效请求稳定 501”改为显式 410 退役边界,并指向 AI Agent 内置 `mindmap_expand_node` 工具。
- legacy `MindmapSidebar` 中直接调用旧 route 的补完入口已从渲染层关闭。
- `mindmap.command.apply` 保存 facade 已通过 Rust 单测确认仍存在,未回退到 Page Aggregate body 保存链。
这组收口只解决“能力入口与实现不一致”的次级问题,不代表 ghost mindmap 资产增长、普通节点编辑 artifact 语义过宽、file tree 重复资源行这几个核心缺陷已经完成。
### 3.7 2026-05-14 ghost asset 止血进展
本轮已确认并修复一条会制造幽灵 mindmap asset 的高风险链路:
- 根因一:`resolveCurrentDocumentId()` 原先优先读取 `window.location.pathname`。切页或打开文件树后,旧 mindmap NodeView 的保存 / 刷新 URL 可能被当前页面路径覆盖成其他 document,导致同一个视觉 mindmap 在错误页面下创建 asset。
- 根因二:TipTap NodeView 构造时会立即 `fetchScene("initial")`。刚从其他页面切回原页面时,NodeView root 可能尚未插入带 `data-document-id` 的 DOM;旧逻辑会退回 URL document id,进而向错误页面创建同名 asset。
- `design/05-editor-mainline/reference-code/leptos-tiptap/tiptap/src/extensions/tiptap_paragraph.ts``resolveCurrentDocumentId()` 改为优先使用 NodeView anchor 所属的 `[data-document-id]`;当 anchor 存在但尚未连接到 DOM 时返回 `null`,交给已有 retry 机制等待 DOM 归位,不再退回旧 URL。
- `design/05-editor-mainline/reference-code/leptos-tiptap/tiptap/src/extensions/tiptap_paragraph.test.ts`:补充 NodeView 所属文档优先、无 anchor 时回退 URL、未连接 anchor 不回退旧 URL三类用例。
- `wolai-frontend/src/lib/documents/tiptap-content-converter.test.ts`:补充 mindmapId 多轮转换稳定性回归用例。
- `scripts/task169-mindmap-realtime-smoke.js`:增强 File Tree mindmap row 去重与 object identity 断言,等待 runtime view settle 后再做 live 对比。
已通过验证:
- `node --import tsx --test src/extensions/tiptap_paragraph.test.ts`4 个测试通过。
- `npm run typecheck`,目录:`design/05-editor-mainline/reference-code/leptos-tiptap/tiptap`
- `npm run build`,目录:`design/05-editor-mainline/reference-code/leptos-tiptap/tiptap`,并同步生成物到 leptos-tiptap reference / spike generated JS。
- `pnpm test -- src/lib/documents/tiptap-content-converter.test.ts`Vitest 实际执行 112 个测试文件、458 个测试,通过。
- `node scripts/task169-mindmap-realtime-smoke.js`,结果 `ok=true``failures=[]`;同一页面下最终 File Tree mindmap row 数量为 1`mindmapId=mindmap_1778695090474`object identity 仍指向同一 `objectKind:"mindmap"`
### 3.8 2026-05-14 普通 mindmap 编辑 artifact 语义收窄
本轮已完成普通 mindmap 内容编辑的 artifact 语义收窄:
- `rust/crates/bridge-runtime/src/lib.rs``mindmap.command.apply` 改为产出 `mindmap.content.updated` domain event 与 `noop` stream delta,不再默认产出 `tree.resource.mindmap.updated + resync_required`
- `rust/crates/bridge-runtime/src/lib.rs``mindmaps.put``createOnly` 分流;`createOnly=true` 仍表示资源创建 / 挂载,保留 `tree.resource.mindmap.put + resync_required``createOnly=false` 表示既有 mindmap blob 内容更新,改为 `mindmap.content.updated + noop`
- `rust/crates/mnote-web/src/routes/mindmap_api.rs`:新增 route 单测覆盖 `mindmap.command.apply` 响应中的 object event 与 noop delta。
- `scripts/task169-mindmap-realtime-smoke.js`:新增真实浏览器 artifact 断言,拦截普通 `mindmap.command.apply` 响应,禁止 `tree.resource.*` event 与 `resync_required` delta。
已通过验证:
- RED`cargo test -p bridge-runtime mindmaps_put_existing_content_update_uses_object_event_without_tree_resync -- --nocapture` 修改前失败,旧行为为 `resync_required`
- RED`cargo test -p mnote-web mindmap_command_apply_returns_object_artifacts_without_tree_resync -- --nocapture` 修改前失败,旧行为为 `tree.resource.mindmap.put`
- GREEN`cargo test -p bridge-runtime mindmap -- --nocapture`17 个 mindmap 相关测试通过。
- GREEN`cargo test -p mnote-web mindmap -- --nocapture`,6 个 mindmap 相关测试通过。
- 已通过:`node --check scripts/task169-mindmap-realtime-smoke.js`
- 已通过:`node scripts/task169-mindmap-realtime-smoke.js`,最新结果 `ok=true``failures=[]`6 条可解析普通 `mindmap.command.apply` 响应均为 `eventType=mindmap.content.updated``streamOp=noop`,另有 1 条 Playwright response body 读取失败被记录为 `skippedReadFailures=1`;同一页面最终仍只有 1 条 mindmap asset row`mindmapId=mindmap_1778698542703`
仍未完成:
- 本轮未执行历史幽灵副本清理;若后续需要清理 Convex 中未被正文引用的旧 mindmap 行,必须先经用户确认并保留清理前后证据。
### 3.9 2026-05-14 历史幽灵副本只读审计入口
本轮新增只读审计脚本,用于在清理前生成候选证据,但不删除任何用户数据:
- `scripts/task180-mindmap-ghost-candidate-audit.js`:支持从 `/api/sidebar`、导出的 sidebar JSON、`/api/tree/projections/file?workspaceId=<workspaceId>` 或导出的 file projection JSON 读取数据,报告同页多 active mindmap、重复 active asset id、缺失 document 的 active mindmap asset、以及 file tree 中同一 mindmap object 的重复 row。
- `scripts/task180-mindmap-ghost-candidate-audit.test.js`:覆盖候选识别逻辑,确认脚本只输出 candidate summary;同时覆盖 Rust file projection 的 `{ ok, result: { items } }` 包装形态和直接 `items` 形态。
- 已通过:`node scripts/task180-mindmap-ghost-candidate-audit.test.js`
- 已通过:`node --check scripts/task180-mindmap-ghost-candidate-audit.js``node --check scripts/task180-mindmap-ghost-candidate-audit.test.js`
- 已通过真实只读审计:`node scripts/task180-mindmap-ghost-candidate-audit.js --url 'http://127.0.0.1:3000/api/tree/projections/file?workspaceId=tree_1777430834634_3' --output tmp/task180-mindmap-ghost-candidate-audit/runtime-file-projection-result.json`。结果 `ok=true``readOnly=true``fileTreeMindmapRows=10``duplicateFileTreeMindmapRows=0``candidateCount=0`
- 已确认 endpoint 可用:`/api/tree/projections/file?workspaceId=tree_1777430834634_3` 返回 `200 OK`projection item 数量为 90。
该脚本只解决“清理前证据如何生成”的问题,不代表历史数据已经清理。真正删除 Convex 历史幽灵副本仍需用户明确授权,并应另行保留清理前后审计结果。
需要注意:file projection 输入只包含投影行,能审计 File Tree 内同一 mindmap object 的重复 row;它不包含 `mindmapAssets``documents` 全量数据,因此不能单独判断同页多 active mindmap、重复 active asset id 或 orphan asset,这三类仍需 `/api/sidebar` 或等价导出的完整数据。
## 4. 页面新建/删除慢的证据
tree shell 在多处命令成功后调用 `scheduleRefresh()`,而 `scheduleRefresh()` 的实现是 80ms 后整页 reload 或带 `renameRowId` 重新 assign
- `rust/crates/mnote-web/src/routes/tree.rs:2976``:2986`
调用点包括:
- 文件树删除:`rust/crates/mnote-web/src/routes/tree.rs:2791``:2824`
- 新建页面:`rust/crates/mnote-web/src/routes/tree.rs:4309``:4340`
- 重命名:`rust/crates/mnote-web/src/routes/tree.rs:4373``:4406`
- 移动:`rust/crates/mnote-web/src/routes/tree.rs:4448``:4502`
与此同时,主页面已经有 tree live controller 接收 `snapshot` / `delta` / `resync`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3881``:3917`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3939``:4050`
这会造成两种低效叠加:
1. 命令结果本身已经返回 `tree.node.created` / `tree.node.archived` 等 delta hint。
2. 前端仍然整页刷新,重新加载 sidebar、file tree、workspace shell、编辑器 runtime。
这解释了“新建页面和删除页面都很慢”的用户感知。
2026-05-14 已有部分 tree command live apply 进展:
- `rust/crates/mnote-web/src/transport/convex.rs` 已剥离发往 legacy Convex mutation 的 artifact-only 字段,修复 archive 命令因 validator 额外字段触发 502 的问题。
- `rust/crates/mnote-web/src/ssr/pages/layout.rs` 已补 `move_document` / `remove_document` 的主页面 DOM reducer。
- 已通过:`node scripts/task177-tree-move-archive-live-smoke.js`,覆盖 move 后页面树 / File Tree 行父节点更新、archive 后两棵树局部移除,以及文档页头稳定。
这只降低了 tree command 卡顿链路的一部分风险,不代表 mindmap ghost asset 增长已经修复,也不代表普通新建/删除的无 reload 与耗时阈值已经完成。
## 5. 当前判断
当前根因尚需实测最终确认,但静态代码已经能支持以下判断:
1. mindmap ghost asset 增长的高概率根因是 `mindmapId` 稳定性没有被端到端锁死。只要编辑/保存/重挂载过程中 attrs 丢失或重新创建 block,就会插入新 `mindmap_{timestamp}`,后端会合法保存,file tree 会合法展示。
2. 另一个可疑根因是 `mindmaps` 表没有唯一约束,`put` 只 patch `.first()`,无法清除或阻止同键重复行。
3. 资产广播入口过多会放大问题。它们让新 mindmapId 或重复数据几乎立即进入 sidebar 本地 state 和 Convex projection,用户就会看到“自己又新增了一个”。
4. file tree projection 只按 `asset.id` 去重,无法识别“同一 document + 同一正文 block 的多个 mindmapId 其实是幽灵副本”,也无法处理同 id 多行投影的上游异常。
5. 普通 mindmap 节点编辑当前仍会走 artifact,但语义已从 `tree.resource.* + resync_required` 收窄为 `mindmap.content.updated + noop`,不再默认推动 file tree projection resync。
6. 页面新建/删除慢不是 Convex 单点问题,曾由 tree shell 命令成功后强制 reload 与 Sidebar 整树刷新链路放大。该慢操作已单独归入 `04-tree-domain` 跟踪,并在 `bugs/04-tree-domain/done/4-25-tree-command-create-delete-reload-latency-v1.md` 完成 create / delete no-reload、React Sidebar 直接 create/delete 本地更新,以及 Rust-family host mutation 本地 apply;本文只保留关联证据。
7. 2026-05-14 已修复 NodeView document id 误归属导致的 ghost asset 增长高风险链路;真实 `task169` 已证明连续编辑后同一页面下只保留 1 条 mindmap asset row。
8. 2026-05-14 已收窄普通 mindmap 内容编辑 artifact 语义;剩余边界主要是历史幽灵副本清理必须显式授权,不能在本 bug 修复中擅自删除用户数据。
## 6. 建议复现与验证
建议补一条失败优先 smoke,先不要直接修代码:
1. 使用真实测试账号登录 `http://localhost:3000/auth`
2. 新建测试页面,记录 `documentId`
3. 插入一个 mindmap,记录首个 `mindmapId`
4. 连续修改 mindmap 中心主题或新增节点 3 次,每次等待保存完成。
5. 切到文件树,统计该页面下 `asset_type=mindmap` 的行数和 `data-mnote-object-identity`
6. 刷新页面后再次统计。
7. 断言同一页面下 mindmap asset 数量仍为 1,且 `mindmapId` 未变化。
8. 检查 `bridgeLogs:listWorkspaceOverview` 中连续 mindmap 节点编辑产生的 artifact,确认普通内容编辑不应默认产生 `tree.resource.* + resync_required`
9. 同一脚本计时新建页面、删除页面从点击到 DOM 稳定的耗时,并记录是否触发 `window.location.reload()` / navigation。
建议同时检查 Convex 数据:
- `mindmaps` 中同一 `document_id` 下是否出现多个 `mindmap_id`
- 新增出来的 `mindmap_id` 是否都形如 `mindmap_<timestamp>`
- 页面正文 TipTap JSON 中是否只有一个 mindmap paragraph,且 attrs.mindmapId 是否随保存变化。
## 7. 建议修复方向
### 阶段一:防止继续增长
1. mindmap block 创建时生成一次稳定 `blockId``mindmapId`,后续保存、转换、重挂载必须保留。
2. `documents.save` / TipTap converter 对 `mnoteBlockType=mindmap` 增加 contract:缺少 `mindmapId` 时不得静默新建时间戳 ID,应先从 block identity / object identity 恢复,恢复不了则报可观测错误。
3. `mindmaps.put` 或上层 command 增加可选 `blockId` / `blockAssetRelation`,对同一 `{documentId, blockId}` 已有关联 mindmap 时拒绝插入第二个 mindmapId。
4. asset 广播入口收口:保存成功、initial createOnly、实例就绪不应都伪造 asset;前端应优先消费 kernel/file tree projection 返回的 object identity。
5. 收窄普通 mindmap 节点编辑的 artifact:内容编辑记录 `mindmap.content.updated` / `mindmap.node.updated`,只有资源关系变化才记录 `tree.resource.*`
6. 普通 mindmap 节点编辑不应默认输出 `resync_required`;只有缺少精确 delta、projection 丢失或 reducer 无法应用时才触发保守 resync。
### 阶段二:清理幽灵副本
1. 提供只读诊断脚本:列出同一页面下多个 mindmapId、对应 updated_at、正文引用的 mindmapId。
2. 只有在用户确认后,才可清理未被页面正文引用的 mindmap 行。
3. 清理必须进入 bug 修复 checklist,不能在本缺陷说明阶段直接删除数据。
### 阶段三:树命令性能收口
1. `tree.node.create` 成功后用 command result / delta 局部插入节点,而不是 reload。
2. `tree.node.archive` 成功后用 `remove_document` delta 局部移除节点。
3. 只有 projection 丢失、SSE 断线或 reducer 无法应用时才走 resync/reload fallback。
4. smoke 增加“无整页 reload”断言和耗时阈值。
## 8. 流转条件
当前状态:`done`
只有满足以下条件后才能移动到 `bugs/05-editor-mainline/done/`
1. [x] 已有 smoke 能稳定覆盖 ghost asset 防回归。`task169` 增强后断言同一页面最终只保留 1 条 mindmap asset row,并检查 object identity。
2. [x] 同一 mindmap 连续编辑保存后,`mindmapId` 和 file tree object identity 保持稳定。`task169` 最新结果中 `mindmapId=mindmap_1778698542703`,最终 File Tree mindmap row 只剩同一 object identity。
3. [x] 同一页面下未主动新建多个 mindmap 时,file tree 只出现一个 mindmap asset row。`task169` 结果 `ok=true``failures=[]`
4. [x] 普通 mindmap 节点编辑产生 object 内容 artifact,不默认产生 `tree.resource.* + resync_required``task169` 最新结果已验证 6 条可解析普通 `mindmap.command.apply` 均为 `mindmap.content.updated + noop`1 条 Playwright response body 读取失败只计入采样跳过,不作为应用语义失败。
5. [x] 只有 mindmap 创建、挂载、解绑、删除、移动、重命名这类资源关系变化才影响 file tree projection。`mindmaps.put createOnly=true` 保留资源事件;`createOnly=false``mindmap.command.apply` 改为 object 内容事件。
6. [x] 新建页面和删除页面不再默认整页 reload,或至少有明确 fallback 条件。`task179` 已验证 Rust `/tree` `convex_workspace` File Tree create/delete 与 Page Tree create 对应 mode 内 navigation 为 0。
7. [x] 浏览器实测记录新建/删除耗时,并明显低于当前整页 reload 体验。`task179` 记录 `filetree.createMs=142``filetree.deleteMs=180``page.createMs=126`
8. [x] 历史幽灵副本的只读候选审计已完成,且当前真实 File Tree projection 未发现 duplicate row。本轮已新增 `task180` 只读候选审计脚本,并补齐 `/api/tree/projections/file` 只读投影输入支持;已完成真实只读审计,candidateCount=0,因此本轮不需要执行历史数据清理,也不会删除用户数据。