# 5-27 本地 Markdown Working Copy 冲突合同 v1

## 背景

`bugs/0524.md` 第 2 条暴露的问题不是单个上传入口错误，而是本地文件夹 watcher、正文 session、文件树资源事件和冲突 UI 之间缺少清晰边界：

- 上传附件或向文件树拖入文件后，非 Markdown 文件变化会进入正文外部变更链路。
- 新建页面后，旧的冲突提示可能被同 root 的事件流带到新页面，刷新后消失。
- 真实冲突仍然需要保留：当前 Markdown 有未保存编辑，同时磁盘上的同一个 Markdown 文件被外部修改时，必须进入冲突处理。

## Sidex 对照

Sidex/VSCode 的核心模型是 `StoredFileWorkingCopy`：

- 每个 working copy 绑定一个具体 `resource`。
- 保存时以 `lastResolvedFileStat.etag/mtime` 做 dirty write prevention。
- 只有同一个 resource 的写入出现 `FILE_MODIFIED_SINCE` 时，才进入 `inConflictMode`。
- 文件系统 watcher 的目录级事件不会直接把同目录其它文件变化升级成当前 working copy 冲突。

MNote 不需要照搬 VSCode 的实现，但应采用同一条合同：正文冲突只属于当前 Markdown 文件，不属于同 root 下任意附件、文件树资源或元数据文件。

## 当前根因

当前 `mnote-web` 的 document event channel 以 `rootUri` 共享：

- 前端订阅 `/api/local-folder/events?rootUri=...`，没有传 `documentId`。
- 后端 `build_document_events_stream` 在没有 `documentId` 时不会按 Markdown 相对路径过滤。
- watcher 对非 Markdown 资源发出的 payload 中 `documentId` 为空。
- 前端收到空 `documentId` 后，仍会对同 root 下所有 document session 设置 `externalChangePending` 并调度正文刷新。

这会把附件上传、文件树拖入、资源创建等 root 级变化误投递到正文 session，形成文件冲突误报和跨页面状态污染。

另一个独立根因是冲突 UI 的 DOM 生命周期：

- `renderSessionConflictSurface` 会把冲突面板直接插入 `.document-pane`。
- 页面切换时 `unmountEditorViewBinding` 只卸载 editor runtime 和事件监听，没有清理旧 session 的冲突面板。
- 因此旧页面已出现冲突时，新建/切换到新页面可能短暂看到旧冲突面板；刷新后整页 DOM 重建，问题消失。

## 合同

1. Markdown 正文 session 只订阅自己的 Markdown 文件事件。
2. 非 Markdown 资源事件只用于文件树/资源投影刷新，不触发正文 `externalChangePending`。
3. 真实正文冲突只在“同一个 Markdown documentId + 当前 session dirty/saving/saveTimer/recent input”时出现。
4. 同 root 下不同 Markdown 页面必须有独立 document event channel。
5. 文件树 live stream 继续承担 root 级变化刷新，不依赖正文冲突链路。

## 已执行修复切片

- 前端 document event channel key 从 `rootUri` 收窄为 `rootUri#documentId`。
- 前端订阅本地文件夹 document events 时携带 `documentId`。
- 前端收到无 `documentId` 的 change payload 时直接忽略，不再设置正文 `externalChangePending`。
- editor view unmount 时清理旧 session 的冲突面板，避免手动插入的 DOM 跨页面残留。
- 后端补充 document event filter 测试，确保资源文件路径不会匹配 Markdown 正文路径。

## 后续系统性收口

- 将 `DocumentBufferStore` 作为正文冲突唯一状态底座，前端 session 只展示 buffer state，不自行拼第二套冲突事实。
- 为资源 tab 增加独立 resource watch 合同：按 `resourcePath` 监听资源文件，而不是复用 Markdown document event。
- 为新建页面增加创建者写入抑制或 bootstrap generation，避免未来新建空文件 watcher 与首次打开时序竞争。
- 把 `task451` 的真实冲突 smoke 与上传/新建页面无冲突 smoke 合并成一组 local Markdown conflict regression。

## 验收

- 上传第一个/第二个附件后，主编辑区不显示 `mnote-editor-conflict-panel`。
- 新建页面后，旧页面冲突 UI 不带入新页面。
- 新建页面后直接向文件树拖入文件，不显示文件冲突。
- dirty 当前 Markdown 后由外部修改同一个 `.md` 文件，仍显示冲突并保留 accept disk / keep current / diff 流程。
