chore: document architecture gaps and add dev hot reload

- add npm dev:hot wrapper using cargo-watch and page reload polling

- add mnote-web dev hot reload endpoint and coverage

- record current architecture review and tracked bug findings across realtime, tree, editor, and AI runtimes

Verification:

- node scripts/task-dev-hot-plan-test.js

- node --check scripts/dev-hot.js

- cargo test -p mnote-web dev_hot -- --nocapture
This commit is contained in:
lix-2026
2026-05-17 23:09:56 +08:00
parent ee0643041a
commit 3311bd0366
31 changed files with 1220 additions and 1 deletions
@@ -0,0 +1,34 @@
# 3-16 [process][bug] tree realtime WS 主链口径与前端 SSE 实现不一致 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
当前架构口径已描述 `/api/realtime/ws` 是树实时 push 主链,`/api/tree/events` 是 SSE fallback。但前端真实代码仍只发现 `EventSource('/api/tree/events')` 消费链路,没有发现 `/api/realtime/ws``new WebSocket` 主 consumer。
## 2. 证据
- [routes/mod.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs:170) 注册 `/api/tree/events`
- [routes/mod.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/mod.rs:172) 注册 `/api/realtime/ws`
- [use-sidebar-tree-stream.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts:137) 实际创建 `EventSource`
- [protocol.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/protocol.ts:72) 固定构造 `/api/tree/events`
## 3. 影响
- 架构文档与前端运行路径不一致。
- live cache 收口时无法判断真实主链。
- 回归测试如果只检查后端 WS route 存在,会误判前端已切换完成。
## 4. 建议修复
- 明确当前阶段:要么更新架构口径为“前端仍 SSE,WS route 已准备”,要么实现前端 WS consumer。
- 增加浏览器 smoke,断言网络中实际使用的是 WS 主链还是 SSE fallback。
- 文档与测试必须以真实前端消费路径为准。
## 5. 状态
已确认口径与前端源码不一致,尚未修复。
@@ -0,0 +1,32 @@
# 3-17 [process][bug] SSE push 模式跳过 polling safety net v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
SSE route 注释和架构口径暗示 polling 可作为 safety net,但实现中只要 `stream_delta_rx.is_some()`,循环就进入 push/heartbeat 分支并跳过 polling。
## 2. 证据
- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:35) 附近说明 SSE 事件路径。
- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:116) 附近在 `stream_delta_rx` 存在时走 heartbeat / push 分支。
## 3. 影响
- broadcast 丢失、进程重启、跨进程实例不共享 channel 时,SSE 连接可能无法靠 Convex bridgeLogs 追上。
- fallback 名义存在,但实际恢复能力不足。
- 客户端可能长期停在旧 snapshot。
## 4. 建议修复
- push 模式也应按 cursor 周期性检查 bridgeLogs,或在 heartbeat 周期中合并轻量 polling。
- 增加测试:模拟 `stream_delta_rx` 无消息但 bridgeLogs 有新事件,SSE 应产出 delta/resync。
- 明确 `pollMs` 与 push channel 同时存在时的优先级。
## 5. 状态
已确认 push 分支跳过 polling fallback,尚未修复。
@@ -0,0 +1,33 @@
# 3-18 [process][bug] WS 与 SSE delta 载荷合同分裂 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
SSE polling 可以从 bridgeLogs / domainEvents 解析 `streamDelta` 并附带 snapshot / resync 信息;WS 当前广播的是 `command_committed` 外壳,两者不是同一套 delta 合同。
## 2. 证据
- [stream_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/stream_support.rs:577) 附近解析 `streamDelta`
- [command_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/command_support.rs:102) 附近广播 command committed 事件。
- [ws.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/ws.rs:40) 处理 WS socket。
## 3. 影响
- 同一 tree command 在 WS 与 SSE 下可能给客户端不同结构。
- 前端 live cache 需要维护两套 reducer,增加状态分裂风险。
- WS 切主时,SSE 已覆盖的 snapshot/resync 语义可能丢失。
## 4. 建议修复
- 抽出统一的 tree realtime envelopeWS 与 SSE 只作为 transport。
- 对同一命令分别跑 WS / SSE 合同测试,断言事件 kind、cursor、snapshot、resync 语义一致。
- 前端 reducer 只消费统一 envelope,不感知 transport 差异。
## 5. 状态
已确认 WS / SSE 事件结构不统一,尚未修复。
@@ -0,0 +1,31 @@
# 3-19 [process][bug] Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
Reasonix ACP wrapper 调用 mnote tool 时只发送 `toolName``args``workspaceId`,没有传递 `actorId``sessionId``runId``toolCallId``traceId``dryRun``idempotencyKey` 等工具合同字段。
## 2. 证据
- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:230) 附近构造 `callMnoteTool` 请求体。
## 3. 影响
- Rust tool executor 难以稳定进行鉴权、审计、追踪和幂等。
- 写工具可能被拒绝,或在缺少幂等键时产生重复写入风险。
- Reasonix 与 Hermes 对同一 tool 的调用合同不一致。
## 4. 建议修复
- wrapper 必须从 ACP run/session 中透传 actor、session、run、toolCall、trace、dryRun、idempotency 字段。
- 写工具缺失必要字段时应 fail fast。
- 增加 Reasonix 端到端 smoke,断言请求字段完整。
## 5. 状态
已确认 wrapper 请求字段不完整,尚未修复。
@@ -0,0 +1,31 @@
# 3-20 [process][bug] ACP incoming request 只记录日志不响应 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
ACP client 收到 `session/request_permission` 等 incoming request 时当前只记录日志,没有返回协议响应。
## 2. 证据
- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:347) 附近处理 incoming request。
## 3. 影响
- ACP agent 侧可能等待到超时。
- 权限请求无法进入用户确认或自动拒绝流程。
- run 状态可能卡在进行中,SSE 输出也无法表达真实阻塞原因。
## 4. 建议修复
- 为已知 request type 实现明确响应:允许、拒绝或要求人工确认。
- 未支持的 request type 也应返回结构化 error,而不是只日志。
- 增加 ACP request/response 协议测试。
## 5. 状态
已确认 incoming request 缺少响应处理,尚未修复。
@@ -0,0 +1,32 @@
# 3-21 [process][bug] ACP run payload 被第一次 stream_events 消费后移除 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`03-rust-web`
## 1. 问题定义
ACP run 的 payload 在 `stream_events` 路径被消费后从内存表移除。刷新页面、多个 SSE 订阅、断线重连时,后续订阅可能无法再取到同一个 run 的 payload。
## 2. 证据
- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:789) 进入 `stream_events`
- ACP 分支中存在 `ACP_RUN_PAYLOADS.remove` 语义,payload 生命周期与第一次 stream 订阅绑定。
## 3. 影响
- 浏览器刷新或 SSE 重连可能导致同一 run 无法恢复。
- 多个客户端观察同一 run 时只有第一个订阅者成功。
- abort / retry / reconnect 语义不可靠。
## 4. 建议修复
- payload 应有 run 级持久生命周期,至少保留到 run completed / failed / aborted 后短 TTL。
- stream subscription 不应拥有 payload 的删除权。
- 增加 SSE 断线重连与重复订阅测试。
## 5. 状态
已确认 payload 生命周期与 stream 订阅耦合,尚未修复。
@@ -0,0 +1,37 @@
# 4-46 [process][bug] FileTree 资源行被当成页面命令对象处理 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`04-tree-domain`
## 1. 问题定义
FileTree asset row 的 `resourceMeta` 同时带 `documentId``assetId`,但 tree shell 的部分交互会优先读取 `documentId`,从而把资源行当成页面行执行删除、复制、移动、重命名等页面命令。
这直接违反 Resource Tree / File Tree / Page Tree 的分层边界。
## 2. 证据
- [bridge-runtime/src/lib.rs](/mnt/Data1T/mnote/rust/crates/bridge-runtime/src/lib.rs:8499) 附近构造 asset row resource meta。
- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2019) `getFileTreeRowDocumentId()` 优先返回资源行中的 `documentId`
- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2881) 键盘删除路径使用 documentId。
- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:2962) 剪贴板路径使用 documentId。
- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:4630) F2 重命名路径使用 documentId。
## 3. 影响
- 选中 mindmap / table / media asset 行后,可能误删或误重命名所属页面。
- 资源归属与页面结构命令混淆。
- `tree.resource.*` 的正式命令面无法成为资源操作唯一入口。
## 4. 建议修复
- FileTree row model 必须区分 `document row``resource row`,不要对 resource row 暴露页面 document command。
- Delete / F2 / copy / move 对资源行应走 `tree.resource.*`,或在未实现时显式禁用。
- 增加浏览器 smoke:资源行上 Delete、F2、Ctrl+C/X/V 不得触发页面命令。
## 5. 状态
已确认 row model 与命令分发存在泄漏,尚未修复。
@@ -0,0 +1,33 @@
# 5-15 [process][bug] PageAggregateClientState 仍在前端生成第二份 page tree 真相 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
文档页已经从 Rust `page-aggregate` 读取快照,但前端 `PageAggregateClientState` reducer 在标题或正文变化后仍会本地构建 page subtree projection,并把它作为 AI context 等链路的输入。
## 2. 证据
- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:144) 初始化 page aggregate client reducer。
- [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:179) 到 [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:197) 本地构建 subtree projection。
- [page-subtree.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-subtree.ts:246) 仍保留 TS 本地 page tree builder。
## 3. 影响
- 文档页、AI context、Rust Page Aggregate 可能看到不同 page tree。
- 前端本地派生逻辑会继续承载长期语义,违反 Page Aggregate 单一真源收口。
- 后续 Debug 时很难判断页面结构到底来自 Rust 还是 TS builder。
## 4. 建议修复
- 明确 reducer 只能作为 UI 草稿态,不得生成长期 page tree projection。
- AI context 应优先读取 Rust Page Aggregate 的稳定 projection。
- 增加测试:本地编辑后 AI context 与 Rust 回读 projection 必须一致。
## 5. 状态
已确认前端仍生成第二份 page tree,尚未收口。
@@ -0,0 +1,32 @@
# 5-16 [process][bug] Sidebar preferred snapshot 中 query 可覆盖 live stream v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
Sidebar 同时启动 query/live/initial 与 tree stream,并通过 preferred snapshot 选择最终数据源。当 `query``tree_stream` 版本相同但内容不同,当前 freshness / syncKey 逻辑可能让 query 压过 live stream。
## 2. 证据
- [app-layout-shell.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx:20) 同时使用 Sidebar data 与 tree stream。
- [use-preferred-sidebar-snapshot.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts:32) 到 [use-preferred-sidebar-snapshot.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts:52) 进行 source 选择。
## 3. 影响
- Sidebar 仍存在 Convex live / HTTP / Rust stream 多链抢真相。
- 同一版本但不同内容时,用户可能看到旧树或非 Rust stream 结果。
- WS / SSE 收口后仍可能被 query source 覆盖。
## 4. 建议修复
- 明确唯一预期来源:当 Rust stream 可用时,应由 stream 成为主真相,query 仅初始化或 fallback。
- 同版本不同内容时必须记录诊断事件,而不是静默按 freshness 选择。
- 增加单测覆盖同版本冲突。
## 5. 状态
已确认多源选择策略仍可能覆盖 live stream,尚未修复。
@@ -0,0 +1,32 @@
# 5-17 [process][bug] 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
文档页标题显示仍可能优先读取 `liveSidebarTitle`,而不是 Page Aggregate `head.title`。当 Sidebar 多源延迟或抢源时,文档页标题可能被 Sidebar snapshot 覆盖。
## 2. 证据
- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:159) 使用 live sidebar title 参与标题状态。
- [page-aggregate-client-state.ts](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/page-aggregate-client-state.ts:144) 标题逻辑仍接受外部 live title。
## 3. 影响
- 文档页标题、Sidebar 标题、Page Aggregate head 可能短暂或长期不一致。
- 标题保存后回读来源不唯一,影响面包屑、AI context、页面 chrome。
- Page Aggregate 单一真源收口被 Sidebar 侧状态反向覆盖。
## 4. 建议修复
- 文档页自身标题应以 Page Aggregate head 为主,Sidebar title 只作为导航视图投影。
- Sidebar title 与 Page Aggregate head 冲突时记录诊断并触发 resync。
- 增加 smoke:改标题后 Sidebar、Breadcrumb、文档页标题同源一致。
## 5. 状态
已确认标题来源仍有漂移风险,尚未修复。
@@ -0,0 +1,33 @@
# 5-18 [process][bug] AI 写正文后本地 Page Aggregate content 可能不刷新 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
页面 AI 写正文后,前端本地 Page Aggregate state 只更新 persisted meta,不保证同步正文快照。`editorBridge.replaceWithSnapshot` 在 editor host 内派发 runtime command,但没有直接回写 reducer content。
## 2. 证据
- [DocumentAiAgentPanel.runtime.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/DocumentAiAgentPanel.runtime.tsx:913) 附近处理 AI 写入后的前端状态。
- [document-content.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx:876) 附近接收 AI 结果。
- [leptos-tiptap-island-editor-host.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx:772) `replaceWithSnapshot` 只通过 editor runtime command 更新 host。
## 3. 影响
- 用户连续两次问 AI 时,第二次 AI context 可能仍读旧 `pageClientState.content`
- 编辑器显示与 Page Aggregate reducer 状态不一致。
- AI 写入验收如果只看编辑器 DOM,可能漏掉 context stale 问题。
## 4. 建议修复
- AI 写入成功后应触发 Page Aggregate 回读或统一 reducer content 更新。
- AI context 应从最新 Rust Page Aggregate snapshot 读取,而不是依赖可能过期的本地 reducer。
- 增加 smoke:AI 修改正文后不刷新页面连续再问一次 AI,断言第二次 context 包含新内容。
## 5. 状态
已确认 AI 写入后本地 Page Aggregate content 可能滞后,尚未修复。
@@ -0,0 +1,61 @@
# 7-16 [process][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
>
> 关联代码:
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:895-956` — `search_replace`
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:920-936` — 归一化匹配后的切片计算
## 1. 问题定义
`mnote.doc.markdown_edit``search_replace` 在 Level 2 “忽略首尾空白和全角 / 半角差异”分支中,先对 `norm_line` 调用 `find(&norm_search)`,得到的是 UTF-8 byte offset
```rust
let start = norm_line.find(&norm_search).unwrap();
let end = start + norm_search.len();
```
随后代码把 `start / end` 当成字符序号传给 `line.char_indices().nth(...)`
```rust
&line[..line.char_indices().nth(start).map(|(i, _)| i).unwrap_or(0)]
&line[line.char_indices().nth(end).map(|(i, _)| i).unwrap_or(line.len())..]
```
这在 ASCII 文本里不明显,但中文、中文标点、全角字符都是多字节。byte offset 不等于字符序号,最终替换范围会偏移。
## 2. 影响
- 页面 AI 对中文正文执行 `mnote.doc.markdown_edit` 时,归一化匹配可能替换错位置。
- 本地 `.md` 文件和在线 Convex 文档共用该工具,因此两条 AI 编辑路径都会受影响。
- 如果替换结果继续写回,用户看到的正文可能被局部破坏,而不是简单失败。
## 3. 复现思路
构造一行中文正文,让精确匹配失败但归一化匹配命中,例如带首尾空白或全角 / 半角差异的 search:
```text
原文:第一段内容
search 一段
replace:二段
```
`find()` 得到的是 byte offset;当前代码按字符序号切片后,替换边界会落到错误字符位置。
## 4. 根因
`str::find` 返回 byte index`char_indices().nth(n)``n` 是第几个字符。当前实现把两种索引体系混用。
## 5. 建议修复
- 在归一化时保留原文字符到归一化字符的 offset map。
- 或者只在同一字符串上使用 byte index,并确保切片边界来自同一个原始字符串的 `char_indices` 映射。
- 增加中文、多字节标点、全角英文 / 数字混排的 `search_replace` 单测。
## 6. 状态
已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。
@@ -0,0 +1,75 @@
# 7-17 [process][bug] mnote.doc.markdown_edit 同块多操作写回会覆盖前序修改 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
>
> 关联代码:
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:779-824` — markdown 字符串层顺序执行 operations
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:854-878` — Convex 写回重新转换成 block ops
> - `rust/crates/mnote-web/src/hermes_tools/doc.rs:976-1008` — `build_block_ops_from_markdown_edit`
## 1. 问题定义
`doc_markdown_edit` 会先把所有 search/replace 顺序应用到 `md`
```rust
match search_replace(&md, &search, &replace) {
Ok(new_md) => {
md = new_md;
applied += 1;
}
...
}
```
但在线 Convex 文档写回时没有使用这个最终 `md`。当前实现重新读取 Page Aggregate blocks,然后对每条 operation 基于原始 block 文本生成一个 replace block op
```rust
let block_text = block.get("text").and_then(Value::as_str).unwrap_or("");
let new_text = block_text.replacen(search, replace, 1);
```
如果两条 operation 命中同一个 block,第二条 operation 的 `new_text` 仍从原始 `block_text` 计算,会覆盖第一条 operation 的结果。
## 2. 影响
用户在页面 AI 中一次提出多个同段修改时,可能只保留最后一个修改。例如:
```json
[
{ "search": "A", "replace": "B" },
{ "search": "C", "replace": "D" }
]
```
`A``C` 都在同一个 block 中,markdown 层预期最终是 `B ... D`,但写回层可能生成两个 replace block op
```text
op1 content = 原始文本中 A -> B
op2 content = 原始文本中 C -> D
```
最终第二次 replace 会把 block 写成只包含 `C -> D` 的版本,前一次 `A -> B` 被丢失。
## 3. 根因
`doc_markdown_edit` 有两套编辑结果:
- `md`:顺序应用 operations 后的真实 markdown 结果。
- `block_ops`:从原始 blocks 和原始 operations 重新推导出来的写回操作。
Convex 写回使用的是 `block_ops`,而不是 `md`。这让“markdown 编辑主路径”在在线文档里退化成了不完整的 block ops 转换层。
## 4. 建议修复
- 最小修复:`build_block_ops_from_markdown_edit` 先按 block 聚合 operations,并基于当前已更新文本连续计算同块最终 content。
- 更稳妥修复:以最终 `md` 作为单一结果,走 markdown -> EditorBlockDocument / page.body.save 的正式转换链,不从原始 operations 二次推导写回结果。
- 增加同一 block 多 operation 的单测和真实 Page Aggregate 回读 smoke。
- 明确部分失败是否允许写入;默认建议任一 operation 失败时不写入,除非调用方显式允许 partial apply。
## 5. 状态
已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。
@@ -0,0 +1,35 @@
# 7-18 [process][bug] AI markdown_edit 阶段状态合同漂移 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
当前仓库的顶层协作口径仍描述 AI 侧“当前只做 Phase APhase B 退役 `local_rule` planner 仍未实施”,但 `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` 与源码已经把 `mnote.doc.markdown_edit` 推到实际运行链路。
这造成阶段合同漂移:文档与实现同时在表达“仍处 Phase A”和“Phase A / B 已开始切主”两种状态。
## 2. 影响
- 后续开发者无法判断 `page_ai_workflow` 是否应继续保留 `local_rule` 快路径。
- AI 编辑验收标准会混乱:到底验 `apply_block_ops`,还是验 `markdown_edit` 回读一致性。
- 新功能容易继续堆到过渡路径,而不是收敛到正式工具合同。
## 3. 证据
- `rust/crates/mnote-web/src/routes/page_ai_workflow.rs` 已直接调用 `doc::doc_markdown_edit`
- `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` 已记录在线 / 本地 markdown 编辑收敛方向。
- 根目录协作口径仍强调 Phase B 退役 `local_rule` planner 属于后续项。
## 4. 建议修复
- 明确当前真实阶段:`markdown_edit` 是否已经是页面 AI 简单编辑主路径。
- 若已切主,应把 `page_ai_workflow`、Hermes guidance、manifest 和 smoke 都同步到该口径。
- 若未切主,应限制 `markdown_edit` 的 runtime 使用面,并把设计稿状态回退为实验态。
## 5. 状态
已确认设计口径与源码使用状态不一致,尚未完成统一。
@@ -0,0 +1,35 @@
# 7-19 [process][bug] Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`mnote.doc.markdown_edit` 已被设计为普通页面正文编辑的主路径,但 Hermes run 的工具指导仍要求简单正文编辑优先调用 `mnote_doc_apply_block_ops`
这导致模型策略与工具主线冲突。
## 2. 证据
- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2351) 构造工具指导。
- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2357) 要求上下文足够时直接调用 `mnote_doc_apply_block_ops`
- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2360) 明确“简单小段落编辑”优先用 `apply_block_ops`
## 3. 影响
- Hermes 与 page AI fast-path 会走不同编辑工具。
- markdown 级 search/replace 的收敛设计无法成为唯一验收口径。
- `apply_block_ops` 的 revision / conflictDetectionKey 合同缺陷会被继续放大。
## 4. 建议修复
- 将普通正文 search/replace、局部段落替换、全文 markdown 替换统一引导到 `mnote_doc_markdown_edit`
- 仅在需要结构性块移动、资源块、复杂定位时才引导模型使用 `apply_block_ops``mnote.block.*`
- 同步更新 smoke:模型生成工具调用时应优先产出 `mnote_doc_markdown_edit`
## 5. 状态
已确认源码指导文本与当前设计主线不一致,尚未修复。
@@ -0,0 +1,35 @@
# 7-20 [process][bug] page_ai_workflow 绕过 Hermes tool executor / audit / toggle v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`/api/page-ai/block-edit-workflow` 仍是独立模型调用链:直接读取 Hermes profile、调用 chat/completions、解析模型 JSON,并直接调用 Rust 工具函数。
它没有通过 `/api/hermes/tools/mnote/call` 的工具执行壳,因此绕过了 tool toggle、audit、统一幂等和统一调用追踪。
## 2. 证据
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:69) 直接调用上游模型。
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) 在 route 内构造 `ToolCallInput`
- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) 直接调用 `doc::doc_markdown_edit`
## 3. 影响
- 关闭或限制 mnote tool 时,该 fast-path 仍可能写入。
- 工具调用审计与普通 Hermes run 不一致。
- 幂等、dryRun、runId、toolCallId 等字段无法统一治理。
## 4. 建议修复
- 将 fast-path 的工具写入改为调用统一 tool executor。
- fast-path 只负责 prompt / plan,不直接执行写入函数。
- 增加 smoke:当对应 tool disabled 时,`page_ai_workflow` 不得绕过限制写入。
## 5. 状态
已确认 route 级绕过统一执行壳,尚未修复。
@@ -0,0 +1,33 @@
# 7-21 [process][bug] mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`mnote.doc.markdown_edit` 的本地文件分支没有调用统一写入合同校验,也没有检查 `dryRun``idempotencyKey`,在匹配成功后直接 `fs::write`
## 2. 证据
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:714) 进入 `doc_markdown_edit` 后没有先调用 `ensure_write_contract`
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:842) 注释说明本地文件直接写回。
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:845) 对本地路径执行 `fs::write`
## 3. 影响
- `dryRun=true` 时也可能落盘,违反工具预览语义。
- 缺少幂等键会让重试请求重复写入,尤其是 ACP / Hermes 断线重试场景。
- 本地 `.md` 与在线 Convex 文档的写入安全级别不一致。
## 4. 建议修复
-`doc_markdown_edit` 写入前统一调用 `ensure_write_contract`
-`dryRun=true` 返回 diff / preview,不落盘。
- 增加本地 `.md` dryRun 单测,断言文件内容不变。
## 5. 状态
已确认源码层缺陷,尚未修复。
@@ -0,0 +1,35 @@
# 7-22 [process][bug] mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`mnote.doc.apply_block_ops` 的批量入口只校验 dryRun / idempotency 写入合同,没有调用页面 revision、conflictDetectionKey、blockRevisionRef 的写前校验。
同文件中已有 `ensure_page_write_preconditions`,但该批量入口没有使用它。
## 2. 证据
- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:385) 定义 `doc_apply_block_ops`
- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:390) 仅调用 `ensure_write_contract`
- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:774) 存在 `ensure_page_write_preconditions`,但批量入口未调用。
## 3. 影响
- AI 可能基于旧 `PageAggregate` 对正文写入,覆盖用户新编辑。
- 多轮 Hermes / ACP run 同时写入时缺少冲突保护。
- `markdown_edit` 在线分支会转调 `apply_block_ops`,因此该缺陷会影响 markdown 主路径。
## 4. 建议修复
- `doc_apply_block_ops` 真实写入前必须校验 page revision 与 conflictDetectionKey。
- 块级操作应支持并校验 blockRevisionRef 或等价版本字段。
- 增加负向测试:缺少 revision / conflictDetectionKey 时真实写入必须拒绝。
## 5. 状态
已确认批量写入入口缺少必要冲突校验,尚未修复。
@@ -0,0 +1,34 @@
# 7-23 [process][bug] mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`mnote.doc.markdown_edit` manifest 的 input schema 没有完整声明 `required` 字段,也缺少 `dryRun``idempotencyKey` 等写入合同字段。
这会让模型、ACP wrapper 和外部调用方无法从 manifest 得知真实写入要求。
## 2. 证据
- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs:413) 附近定义 `mnote.doc.markdown_edit` manifest。
- 当前 schema 更像参数提示,没有完整表达写入前置条件。
## 3. 影响
- 调用方可能不传幂等字段或 dryRun 字段。
- tool toggle / schema validation 无法提前拒绝不合规请求。
- Reasonix / ACP 等多 runtime 接入时会继续复制不完整合同。
## 4. 建议修复
-`mnote.doc.markdown_edit` 补全 JSON Schema`required`、互斥的 `operations` / `full_content`、写入合同字段。
- manifest 与 Rust runtime 校验保持一致。
- 增加 manifest snapshot 测试,防止字段再次漂移。
## 5. 状态
已确认 manifest 合同不完整,尚未修复。
@@ -0,0 +1,35 @@
# 7-24 [process][bug] 在线 markdown_edit 写回不以最终 Markdown 为真源 v1
> 发现时间:2026-05-17
>
> 状态:`[process]`
>
> 关联主线:`07-ai`
## 1. 问题定义
在线 Convex 文档的 `mnote.doc.markdown_edit` 先在 `md` 字符串上顺序应用 search/replace,但写回时没有用最终 `md` 生成新的文档真相,而是重新读取原始 blocks,并用原始 operations 反推 `apply_block_ops`
因此 markdown 层已经算出的最终结果不一定能被忠实落库。
## 2. 证据
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:811) 在 `md` 上执行 search/replace。
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:855) 写回前重新读取 aggregate / blocks。
- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:857) 使用原始 operations 构造 block ops,而不是解析最终 `md`
## 3. 影响
- 跨块替换、全文替换、标题 / 列表格式变化可能在 markdown 层成功,但无法正确转换成 block ops。
- 部分失败仍可能返回成功写入,用户看到的正文与工具返回摘要不一致。
- 该问题与 `7-17` 的同块多操作覆盖同源,但影响面更宽。
## 4. 建议修复
- 在线分支应明确选择一种真源:要么最终 `md` 可被解析并完整生成 EditorBlockDocument,要么限制 `markdown_edit` 只支持可安全映射的单块操作。
- 对 full_content、跨块 search/replace、列表/标题变更增加回读一致性测试。
- route 成功条件必须从“operationsApplied > 0”提升为“Page Aggregate 回读等于预期最终 markdown”。
## 5. 状态
已确认写回层没有使用最终 markdown 真相,尚未修复。