feat(rag): harden post-LightRAG runtime

Retire legacy OCR/media/evidence fallbacks, add local-folder event bus and Page Aggregate guards, and archive completed design checklists.

Validation: cargo test -p mnote-web -- --test-threads=1; cargo test --workspace -- --test-threads=1; git diff --check; codegraph sync .; codegraph_status.
This commit is contained in:
lix-2026
2026-06-07 10:35:21 +08:00
parent 22a92edcda
commit 9551d4c1dc
59 changed files with 4053 additions and 5249 deletions
@@ -0,0 +1,155 @@
# 7-18 Agent edit clean / dirty smoke closure checklist v1
> 创建时间:2026-06-06
>
> 状态:`done`
>
> Owner07-ai / 05-editor-mainline / 03-rust-web
>
> 父设计:`design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
## 1. 边界
本 checklist 只推进 local-first 普通 Markdown 的真实 agent 文件编辑闭环,不碰 LightRAG、Knowledge RAG、OCR、evidence index 或 OnlyOffice recipe 扩展。当前另一个 agent 正在修改 LightRAG 相关文件,本轮不得编辑:
- `rust/crates/mnote-web/src/routes/knowledge_rag.rs`
- `rust/crates/mnote-web/src/hermes_tools/knowledge_rag.rs`
- `design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md`
- `scripts/task529*``scripts/task534*`
## 2. 当前代码证据
已有基础能力:
- Page AI target 会生成 `mnote.agent_target_package.v1`,并给目标写入 `allowedFiles``policy.conflictPolicy=fail_on_dirty_or_stale`
- 发送 run 前会通过 `/api/documents/buffer-state` 检查 dirty / external modified 状态;阻塞时抛 `page_ai_target_buffer_not_writable`
- 后端会生成 `mnote.agent_run_envelope.v1`,其中 `resultPolicy.changedFiles=required``refresh=watcher_or_explicit_resync`
- `scripts/reasonix-acp-wrapper.mjs` 会把 `agentRunEnvelope` 写进模型可见 prompt,要求 agent 使用自身文件工具编辑真实文件。
- `local_agent_audit_finalize_run` 会基于前后快照生成 `changedFiles``mnote.agent_run_receipt.v1`
- 前端 `pageAiDispatchReceiptRefresh(...)` 会把 receipt 转成 `tree:local-folder-watch-batch``mnote:page-ai-tool-write-completed`
当前缺口:
- 没有一条真实浏览器 smoke 证明 agent 原生 patch 修改当前 `.md` 后,前台 tiptap 在 clean buffer 下可见更新。
- dirty buffer 的阻塞虽然已有函数,但缺少端到端 smoke 证明不会启动可写 run 或不会静默覆盖。
- readonly 写入目前仍偏后置审计:`read_only_write_rejected` 可以记录结果,但还需要前置拒绝或 tool/ACP 层明确失败。
- 审计快照在 folder context 下会退回 full snapshot;产品化前需要限制为 `allowedFiles` / changedFiles 优先,并对超限降级有可见记录。
## 3. 非目标
- 不新增 MNote 普通 Markdown 写入工具。
- 不把 `mnote.doc.markdown_edit` 恢复为 local-first fallback。
- 不实现 streaming apply、review session、GhostTextOverlay。
- 不要求真实外部大模型;smoke 可以用受控 ACP stub / Hermes test runtime 模拟 agent 原生文件 patch,但必须真实写磁盘文件。
- 不把 LightRAG retrieval、evidence citation 或 OCR sidecar 混入本闭环。
## 4. Phase Aclean buffer 真实写入回收 smoke
目标:证明 agent 原生文件编辑 -> watcher / receipt -> 当前前台 tiptap 可见更新。
Checklist
- [x] 新增 `scripts/task535-page-ai-local-agent-clean-edit-smoke.js`
- [x] smoke 创建临时 local-folder workspace 和目标 `AgentClean.md`
- [x] 浏览器打开该 Markdown,确认 `mnote-leptos-tiptap-island-editor-root` ready。
- [x] 通过 Page AI 发起本地 agent runrun payload 必须包含 `targetPackage.currentFile.relativePath``allowedFiles`
- [x] agent stub / ACP test runtime 只能在 `allowedFiles` 指向的真实 `.md` 上做最小 patch。
- [x] run completed 后必须返回或触发 `agentRunReceipt.changedFiles`,路径为目标 `.md`
- [x] 前端必须触发 `tree:local-folder-watch-batch`,并对当前文档触发 `mnote:page-ai-tool-write-completed`
- [x] tiptap 可见正文更新为 agent 写入后的内容,不需要手动 reload。
- [x] 网络断言本路径不调用 `/api/documents/save`
- [x] 工具事件断言本路径不调用 `mnote.doc.markdown_edit`
验收:
- [x] smoke 输出 `tmp/task535-page-ai-local-agent-clean-edit-smoke/result.json`,包含 run payload、changedFiles、current refresh、最终 editor text。
- [x] `result.json``ok=true``usedDocumentsSave=false``usedMarkdownEdit=false`
## 5. Phase Bdirty buffer 不静默覆盖 smoke
目标:证明用户本地未保存时,agent 写入不会绕过 buffer 冲突模型。
Checklist
- [x] 新增 `scripts/task536-page-ai-local-agent-dirty-guard-smoke.js`
- [x] 打开目标 Markdown 后在 tiptap 中输入未保存内容,制造 BufferStore dirty 状态。
- [x] Page AI target chip / run 前检查能读到 dirty 状态。
- [x] 点击发送可写 run 时,应出现明确阻塞或确认流程;当前最低验收是阻塞并返回 `page_ai_target_buffer_not_writable`
- [x] smoke 断言没有启动可写 ACP run。
- [x] smoke 断言磁盘 `.md` 没有被 agent stub 修改。
- [x] smoke 断言 editor 中未保存内容仍可见。
验收:
- [x] smoke 输出 `tmp/task536-page-ai-local-agent-dirty-guard-smoke/result.json`
- [x] `result.json``ok=true``blockedBeforeRun=true``diskChanged=false``editorDirtyTextStillVisible=true`
## 6. Phase Creadonly 前置拒绝 smoke
目标:证明 readonly target 不靠事后审计才发现写入失败。
Checklist
- [x] 新增 `scripts/task537-page-ai-local-agent-readonly-write-guard-smoke.js`
- [x] 构造只读授权或只读 targetPackage`permission=read``allowedFiles` 可读但不可写。
- [x] 发起“请修改当前文件”的 Page AI run。
- [x] 前端或后端应在启动可写 ACP run 前拒绝,错误码稳定,例如 `page_ai_target_readonly``local_agent_write_not_allowed`
- [x] 若当前实现只能后置审计,应先把 smoke 写成 RED,记录实际行为,不伪造通过。
- [x] smoke 断言磁盘文件未变化。
- [x] smoke 断言 audit 中没有把 readonly 写入说成成功;若出现 `read_only_write_rejected`,必须在 UI 可见错误里体现。
验收:
- [x] smoke 输出 `tmp/task537-page-ai-local-agent-readonly-write-guard-smoke/result.json`
- [x] `result.json``ok=true` 仅在前置拒绝落地后允许;当前若为 RED,应输出 `ok=false` 和真实行为证据。
## 7. Phase D:审计快照范围收口
目标:避免 local agent audit 因 folder context 退回扫全 root,影响大 workspace 和 run 返回速度。
Checklist
- [x] 后端 `local_agent_audit_relative_paths_from_payload(...)` 优先消费 `agentRunEnvelope.allowedFiles` / `targetPackage.allowedFiles`
- [x] folder context 不再直接强制 full snapshot;除非用户明确选择 folder-wide edit 且有上限。
- [x] 增加审计上限:文件数、总字节数、耗时;超限时进入 `auditScope=truncated`,并在 receipt 中可见。
- [x] 单测覆盖 allowedFiles 优先、folder context 不扫全 root、超限截断。
验收:
- [x] `cargo test -p mnote-web local_agent_audit -- --test-threads=1` 或等价 targeted tests 通过。
- [x] clean smoke 中 audit snapshot 只包含目标文件及必要 changed file。
## 8. Phase E:文档与 manifest 退役口径
目标:锁死 local-first 普通 Markdown 不走旧 MNote 写入工具。
Checklist
- [x] Page AI / Hermes guidance 对 local-folder 普通 Markdown 明确优先 agent 原生 patch/diff。
- [x] `mnote.doc.markdown_edit` 在 manifest 中只标注为 online/cloud/compat 历史工具或结构校验辅助。
- [x] `reasonix-acp-wrapper.mjs` selftest 断言 prompt 包含“不使用 MNote doc/page write tools for ordinary local Markdown edits”。
- [x] smoke 断言 local-first agent edit 不调用 `/api/documents/save``mnote.doc.markdown_edit``mnote.page.save`
验收:
- [x] `7-18-local-first-agent-file-editing-control-plane-v1.md` Phase B/C 可勾选。
- [x] 本 checklist 可移动到 `design/07-ai/done/`,父设计 `7-18` 仅剩跨 workspace / 多 target 的产品确认项。
补充证据:
- 2026-06-07`task539-local-agent-audit-scope-contract.js` 复跑 `task535` 并断言 `targetPackage.allowedFiles=["AgentClean.md"]``runTargetSnapshot.frozenAt` 存在、未调用 `/api/documents/save` / `mnote.doc.markdown_edit` / `mnote.page.save`;同时运行 `cargo test -p mnote-web local_agent_audit -- --test-threads=1`,覆盖 `auditScope.scope=allowed_files``fileCount=1`
## 9. 推荐执行顺序
1. 先写 `task535` clean smoke,允许 RED,固定真实 run payload / receipt / 前台刷新证据。
2. 再写 `task536` dirty guard smoke,优先验证当前已有 dirty blocker。
3. 再写 `task537` readonly guard smoke,若当前只能后置审计则保持 RED。
4. 最后做审计快照范围收口和 manifest/guidance 口径收紧。
## 10. 归档条件
- clean buffer smoke 真实通过。
- dirty buffer smoke 证明不会静默覆盖。
- readonly write smoke 证明前置拒绝或 UI 明确失败。
- local-first 普通 Markdown agent edit 不调用旧 MNote 写入工具。
- LightRAG 相关 diff 不在本 checklist 中被修改或作为验收前置。
@@ -0,0 +1,186 @@
# 7-51 LightRAG post-commit hardening v1
> 创建时间:2026-06-07
>
> 状态:`done`
>
> Owner07-ai / knowledge-rag / 03-rust-web / plugin-ui
>
> 来源:`design/10-review/done/20-post-lightrag-runtime-hardening-checklist-v1.md` P0 审计
>
> 上位依据:
> - `design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md`
> - `design/10-review/done/20-post-lightrag-runtime-hardening-checklist-v1.md`
## 1. 第一结论
7-50 作为 LightRAG 资料库问答主线可以保持 `done`,但 post-commit audit 发现若干需要单独硬化的边界:
- watcher stale sync 在 best-effort 删除失败时会吞掉错误,并且已经清空 registry 中的 `lightRagDocId`,后续难以重试删除 provider orphan doc。
- `sourcePaths` 当前是 provider 检索后过滤 MNote mapped references,不是 provider 预过滤;HTTP API 仍返回未过滤 `raw`,存在调用方误用 raw 的风险。
- sidecar locator 在 quote 未命中时会 fallback 到 `first_positioned_block`,可能生成错误 page/bbox,却表现为非 degraded locator。
- `mnote-knowledge-rag` skill 文案对 degraded citation 过强,容易让 agent 误以为不能使用返回的降级 resourceTab 链接。
- Reasonix wrapper 仍手写工具表,能力注册漂移问题还没有真正解决。
本稿不恢复旧 evidence / LiteParse / local OCR 路径,不开发第二套 RAG,只硬化 7-50 已选定的 LightRAG provider 边界。
## 2. 非目标
- 不重建 `mnote.evidence.*` / `mnote.index.*` agent 工具。
- 不把 source scope 伪装成 LightRAG provider 原生过滤,除非实际实现了 provider 级约束。
- 不清空用户资料库、不删除用户原始 source、不自动大规模 reindex。
- 不把 LightRAG sidecar / chunk / graph 当作 MNote 正文真相。
- 不在本稿内实施完整 capability registry;只补必要漂移测试或文案约束,完整 registry 仍按 `7-47` / `20 P4` 推进。
## 3. Phase ADelete retry / orphan doc guard
目标:source 删除或 hash 变化后,即使 LightRAG 删除失败,也不能丢失重试 provider doc idprune 不能隐藏仍可能参与回答的 orphan doc。
Checklist
- [x] 修改 `sync_registry_source_state(...)`source missing / hash changed 时不要立即清空 `lightRagDocId`;应进入 `stale=true` + `lightRagStatus=delete_submitted``delete_retry_required`
- [x] `sync_registry_with_documents(...)` 调用 LightRAG delete 失败时,把失败写入 registry entry 的可见状态,而不是吞掉后只保留 stale。
- [x] 只有确认 LightRAG `/documents` 不再包含该 doc 后,才清空 `lightRagDocId` 并标记 `delete_completed`
- [x] `prune_registry` 不清理仍带 `lightRagDocId` 且 delete 未确认完成的 stale entry。
- [x] `mapped_references(...)` 对 missing registry 的 provider reference 需要区分:
- provider orphan / unknown source:默认不作为有效 MNote citation 返回,或明确返回 degraded + unmapped 分类,但不得进入 scoped answer citation。
- registry 命中但 stale / deleted:继续过滤。
- [x]`delete-source` route 保持 `delete_file=false`,并保留“不删除用户原始 source”的断言。
验证:
- [x] 单测:LightRAG delete 失败时 registry 保留 doc id 和 retry 状态。
- [x] 单测:delete completed 后才清 doc id。
- [x] 单测:prune 不删除 `delete_submitted` / `delete_retry_required` entry。
- [x] 单测:missing registry provider reference 不会作为正常 citation 返回。
- [x] `cargo test -p mnote-web knowledge_rag -- --nocapture`
实施记录:
- 2026-06-07`sync_registry_source_state(...)` 在 source missing / hash changed 时保留 `lightRagDocId`,并把状态置为 `delete_submitted`
- 2026-06-07`sync_registry_with_documents(...)` 会重试 `delete_submitted` / `delete_retry_required` doc idLightRAG delete 失败时写入 `delete_retry_required`,成功提交后保持 `delete_submitted`,等 `/documents` 消失后才标记 `delete_completed` 并清空 doc id。
- 2026-06-07:当前 registry schema 先用 `lightRagStatus=delete_retry_required` 表达失败状态,不额外扩写错误 message 字段,避免扩大持久化迁移面。
## 4. Phase BSource scope 语义收紧
目标:避免调用方误以为 `sourcePaths` 已限制 LightRAG provider 检索范围。
Checklist
- [x]`/api/knowledge-rag/query` 返回中新增 `sourceScopeMode`,当前值明确为 `post_filter_mapped_references`
- [x]`sourcePaths` 非空,默认不向普通 agent / Page AI 暴露未过滤 `raw` chunks;至少 compact tool output 继续不暴露 raw chunks。
- [x] HTTP API 如仍保留 `raw`,必须标注 `rawScopeFiltered=false` 或等价字段,提醒 raw 未受 source scope 过滤。
- [x] `mnote.knowledge_rag.query` manifest / Reasonix wrapper / skill 文案从“限制检索来源”改为“按 MNote source 过滤返回 referencesprovider raw 可能仍为全局检索结果”。
- [x] Page AI final answer 只允许引用 filtered `references/citations`,不得引用 raw LightRAG chunks。
验证:
- [x] 单测:`sourcePaths=[alpha]``references` 只含 alpha。
- [x] 运行态 smoke`sourceScopeMode=post_filter_mapped_references``rawScopeFiltered=false`
- [x] 单测或 wrapper selftestagent compact result 不含 raw chunks。
- [x] `task538-knowledge-rag-source-scope-api-smoke.js` 更新并通过。
- [x] `task534-knowledge-rag-source-management-scope-smoke.js` UI source management 部分已修复并通过。
实施记录:
- 2026-06-07`/api/knowledge-rag/query` 返回 `sourceScopeMode=post_filter_mapped_references``rawScopeFiltered=false`
- 2026-06-07`mnote.knowledge_rag.query` compact agent result 继续只暴露 filtered `references/citations`,不暴露 `raw.chunks`
- 2026-06-07manifest、Reasonix wrapper、`skills/mnote-knowledge-rag/SKILL.md` 已统一 sourcePaths post-filter 口径;新增 `task538-knowledge-rag-source-scope-api-smoke.js` 覆盖 API scope metadata、filtered references、`delete-source` 不删原始 source。
- 2026-06-07`task534` 修正为打开资料库设置后切到“全部”来源,避免默认“需处理”过滤隐藏已索引 source;在 3300 最新构建通过。
## 5. Phase CLocator strict match / degradation
目标:没有真实匹配到 quote / chunk 对应 sidecar block 时,不生成看似精确的 page/bbox。
Checklist
- [x] 修改 `find_lightrag_sidecar_block(...)`:移除或限制 `first_positioned_block` fallback;只有 quote 与 block content 达到明确匹配阈值时才返回 page/bbox locator。
- [x] 若只找到 source resource 但没有精确 block match,返回 clickable degraded resourceTab citation,但 `locatorDegraded=true`
- [x] `citationMarkdown` 对 degraded resourceTab 使用稳定文案,例如 `来源定位降级:<file>`,不包含伪造页码。
- [x] 对 PDF / image / DOCX fallback citation 分别保留打开能力,但不伪造 bbox。
- [x] 记录 LightRAG chunk 缺 `refs` 的当前边界;未来若 provider 暴露 refs,再改为 refs 优先、文本匹配 fallback。
验证:
- [x] 单测:quote 不匹配 sidecar block 时不返回 page/bbox locator。
- [x] 单测:quote 匹配 sidecar block 时仍返回 page/bbox locator。
- [x] 单测:known resource + no locator 返回 clickable degraded citation。
- [x] `task529-knowledge-rag-citation-resource-tab-smoke.js` 更新为当前 fixture 可重复的正例,并通过。
实施记录:
- 2026-06-07`find_lightrag_sidecar_block(...)` 已移除 `first_positioned_block` fallback;未匹配 quote 时返回降级 resourceTab citation,不生成 page / bbox。
- 2026-06-07:当前 LightRAG `/query/data` chunks 未提供可稳定回跳 MNote sidecar block 的 provider `refs`;MNote 暂以文本匹配做严格定位,未匹配时只给 degraded resourceTab。
- 2026-06-07`task529` 改为当前 image source 降级 citation 正例,验证 `open_reference` 与 citation URL 能打开 image resource tab,且不携带伪造 page locator。
## 6. Phase DAgent / UI degraded citation 口径统一
目标:agent 可以使用 MNote 返回的 degraded citation,但不能编造页码、bbox 或 provider 内部路径。
Checklist
- [x] 更新 `skills/mnote-knowledge-rag/SKILL.md``locatorDegraded=true` 时,可以引用返回的 `citationMarkdown`,但必须说明来源定位降级;禁止编造 page / bbox。
- [x] 更新 Reasonix system prompt 中 knowledge-rag 能力说明,保持同一口径。
- [x] 更新 Page AI final answer smoke,增加 degraded citation 场景:最终回答可以含降级 citation,但不能说成 p.N / bbox。
- [x] 更新 dashboard smoke,不再依赖固定 `Completed (5)` / `Fail (4)` / 固定 doc id;改为验证 Documents / Graph / Retrieval 入口与当前 documents summary 可见。
验证:
- [x] `MNOTE_REASONIX_ACP_SELFTEST=1 node scripts/reasonix-acp-wrapper.mjs`
- [x] `node scripts/task530-knowledge-rag-page-ai-final-answer-smoke.js`
- [x] `node scripts/task531-lightrag-dashboard-ui-smoke.js`
## 7. Phase EReasonix / manifest drift guard
目标:在完整 7-47 capability registry 实施前,先降低 Reasonix wrapper 与 Rust manifest 的漂移风险。
Checklist
- [x] 增加一个轻量 selftestRust manifest 中 `mnote.knowledge_rag.*` 三个工具必须都存在于 Reasonix wrapper 映射。
- [x] 增加一个反向 selftestReasonix wrapper 暴露的 `mnote_knowledge_rag_*` 必须能映射到 Rust manifest tool name。
- [x] 保留手写工具表作为短期现实,但在 7-47 中继续推进 manifest 动态注册。
- [x] 若发现旧 `mnote.evidence.*` / `mnote.index.*` 函数被 grep 误判,给 manifest 源码附近加注释:dead code 仅保留历史对照,不在 manifest vector 中注册。
验证:
- [x] `MNOTE_REASONIX_ACP_SELFTEST=1 node scripts/reasonix-acp-wrapper.mjs`
- [x] `cargo test -p mnote-web hermes_client_tools_uses_manifest_and_profile_disabled_state -- --test-threads=1`
实施记录:
- 2026-06-07Reasonix wrapper selftest 会读取 Rust `hermes_tools/manifest.rs`,校验 `mnote.knowledge_rag.*` manifest tools 与 wrapper `MNOTE_TOOL_NAMES` / `REASONIX_TOOL_TO_MNOTE_TOOL` 双向一致。
## 8. Phase Flegacy OCR / Convex media visible runtime 退役跟进
目标:LightRAG 成为资料库主线后,继续清掉仍可能被浏览器 runtime 或 worker 误读为 active fallback 的旧 OCR / Convex Files 链路。
Checklist
- [x] `/api/local-folder/events` 不再转发 `local_ocr.job.updated`resource tab 不再依赖 local OCR SSE 或 `synthetic_resource_write` 刷新 filetree。
- [x] UI preference 不再接受 `localOcr.*` 写入;历史 effective preference 强制为 `false` / retired。
- [x] FileTree 右键入口不再保留 `local-ocr` action alias;资料索引只走 `knowledge-rag-index``/api/knowledge-rag/ingest`
- [x] 浏览器 runtime 不再调用 `/api/media/sign` / `/api/media/upload`;旧 Convex Files 前端链路只标记 retired guard。
- [x] `AppState` 移除 `local_ocr_job_tx` / `local_ocr_active_jobs`,旧 local OCR helper 不再广播 runtime event。
- [x] local search incremental refresh 遇到 OCR sidecar 时只清理旧 evidence sqlite 投影,不再重建 OCR evidence。
- [x] `routes/local_ocr.rs` 从历史 HTTP job / MinerU runtime 实现收缩为历史 OCR sidecar 识别、frontmatter 解析和索引读取 helper;旧 ignored HTTP route tests 已移除。
- [x] resource tab 后台任务抽屉、toolbar 与 CSS 从 `mnote-local-ocr-*` 重命名为 `mnote-knowledge-rag-*`Knowledge RAG ingest/delete 不再挂旧 OCR DOM / function 名。
- [x] Page AI 目标包删除空实现的 OCR sidecar context enrichmentlocal-first agent 只携带当前文件/selection/allowed roots,不再保留 `ocrContext` / `ocrRootRelativePath` 注入点。
- [x] Sidebar 旧 `open-ocr-settings` / `toggle-ocr-tasks` / `data-local-ocr-*` 委托移除;历史 `.ocr/*.ocr.md` sidecar 仅以 `retired-ocr-sidecar:*` 资源身份打开,不再伪装为 active local OCR。
验证:
- [x] `cargo test -p mnote-web local_ocr -- --test-threads=1`
- [x] `cargo test -p mnote-web local_search_ocr_sidecar_is_hidden_after_lightrag_retirement -- --test-threads=1`
- [x] `cargo test -p mnote-web -- --test-threads=1`
- [x] `cargo test --workspace -- --test-threads=1`
- [x] `git diff --check`
- [x] `codegraph sync .` / `codegraph_status`
## 9. 归档条件
- delete retry / orphan doc guard 有单测覆盖,prune 不会隐藏未确认删除的 provider doc。
- `sourcePaths` 的 post-filter 语义在 API、skill、wrapper 和 tests 中一致。
- locator 不再用不匹配的 first positioned block 生成非 degraded page/bbox。
- degraded citation 的 agent / UI 文案统一。
- `task529` / `task530` / `task531` 不依赖已漂移的固定历史 LightRAG documents 数量或 doc id。
- 7-50 保持 `done`,本稿完成后移动到 `design/07-ai/done/`