# 7-51 LightRAG post-commit hardening v1

> 创建时间：2026-06-07
>
> 状态：`done`
>
> Owner：07-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 A：Delete retry / orphan doc guard

目标：source 删除或 hash 变化后，即使 LightRAG 删除失败，也不能丢失重试 provider doc id；prune 不能隐藏仍可能参与回答的 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 id；LightRAG delete 失败时写入 `delete_retry_required`，成功提交后保持 `delete_submitted`，等 `/documents` 消失后才标记 `delete_completed` 并清空 doc id。
- 2026-06-07：当前 registry schema 先用 `lightRagStatus=delete_retry_required` 表达失败状态，不额外扩写错误 message 字段，避免扩大持久化迁移面。

## 4. Phase B：Source 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 过滤返回 references；provider 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 selftest：agent 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-07：manifest、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 C：Locator 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 D：Agent / 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 E：Reasonix / manifest drift guard

目标：在完整 7-47 capability registry 实施前，先降低 Reasonix wrapper 与 Rust manifest 的漂移风险。

Checklist：

- [x] 增加一个轻量 selftest：Rust manifest 中 `mnote.knowledge_rag.*` 三个工具必须都存在于 Reasonix wrapper 映射。
- [x] 增加一个反向 selftest：Reasonix 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-07：Reasonix wrapper selftest 会读取 Rust `hermes_tools/manifest.rs`，校验 `mnote.knowledge_rag.*` manifest tools 与 wrapper `MNOTE_TOOL_NAMES` / `REASONIX_TOOL_TO_MNOTE_TOOL` 双向一致。

## 8. Phase F：legacy 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 enrichment，local-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/`。
