lllin000_PaperForge/docs/plans/memory-layer-test-report.md
LLLin000 e551f79a77 fix: PR 7 post-test hardening — object units, node_id, expanded tests
P0 fixes from test report review:
- object unit_id: fallback to f"{obj_type}:p{page}:{block_id}" when
  figure_id/table_id missing (prevents DB overwrite on INSERT OR REPLACE)
- object caption_key: page-qualified f"p{page}:{caption_bid}" to match
  page-qualified subtree_block_ids in find_owning_node
- object block_map: page-qualified keys for consistency with body_units
- structure node_id: changed from sec:{block_id} to sec:p{page}:{block_id}
  for stability (reduces vector ID churn from emitted_order changes)

New tests:
- Section 4b: Object units DB persistence (unit_id unique, non-empty labels,
  DB count == list count after upsert)
- Section 5b: Coverage gate (v2_tree / render_map / body_papers counts)

Layer A: 1669/1669 pass (+60 checks from 4b + 5b)
Unit tests: 153 pass, 1 skip
2026-07-06 19:34:05 +08:00

14 KiB
Raw Blame History

Memory Layer — 功能测试报告

日期: 2026-07-06
测试目标: PaperForge Memory Layer 全面改造7 个 PR的功能验证
测试范围: Layer Aartifact 一致性)+ Layer Bvector/API
测试环境:

  • Repo: github-release @ 01461330 (master)
  • Vault: D:/L/OB/Literature-hub (734 OCR papers, 22 篇有 v2 结构)
  • Embedding: SiliconFlow Qwen/Qwen3-Embedding-4B (OpenAI-compatible API)
  • Unit tests: 153 passed, 1 skip

TC-1: Render Events

内容

从持久化 render/render-map.json 验证 heading_events 和 emitted_block_events:

  • heading_events 数量 ≥ 1
  • emitted_order 全局单调递增且唯一
  • heading 在 emitted_block_events 中有对应的 emitted blockemitted_as="heading"
  • 向下兼容:return_events=False 返回 str

方法

Layer A 自动脚本从 render-map.json 读取 heading_events 和 emitted_blocks 检查 emitted_order 单调性/唯一性,以及 heading 对应的 emitted block 存在性。

效果

全部通过1609/1609 checks pass。每篇论文 733 个 headingemitted_order 递增无碰撞。

风险

  • render-map.json 只存在于 22 篇 v2 render 论文中。709 篇旧论文没有该文件。
  • backmatter heading 现在进入 heading_eventsmarkdown_level=2这是新行为旧 render-map 没有。

TC-2: Structure Tree

内容

从持久化 index/structure-tree.json 验证:

  • 嵌套深度正确child level > parent level
  • own_block_idssubtree_block_ids 中的所有 page-qualified key 在 emitted_block_events 中存在
  • own_block_ids 不与 children 的 subtree_block_ids 重叠
  • 所有 node_id 唯一block_id 跨页碰撞通过 :order{N} 后缀消除)
  • page_span 与实际 emitted blocks 的页码范围一致

方法

Layer A 自动脚本遍历树节点,对比 emitted_idspage-qualified p{page}:{block_id} 格式)。

效果

全部通过。tree 中所有 block reference 都使用 p{page}:{block_id} 格式, 消除跨页同名 block_id如 running headers block_id=0的歧义。

风险

  • bounds_map 的修复bounds_map[f"p{h['page']}:{h['block_id']}"])是在测试过程中发现的漏洞, 3 个 commit 后才解决。如果后续修改 bounds_map 逻辑,可能重新引入该问题。
  • 节点 page 字段必须存在,否则 bounds lookup 会 miss。

TC-3: Body Units

内容

build_body_units 验证:

  • unit_id 全局唯一(跨论文)
  • unit_kind 分类:当前 policy 只排除 reference_item/reference_heading
  • section_path 完整性(path == "/".join(json.loads(section_path_json))
  • token cap每 part len(text)//4 <= 1000(递归拆分)
  • 空 section 不产出 body unit
  • reference text 不泄漏进 body units

方法

Layer A 自动脚本从结构化 blocks + structure tree 重建 body_units 检查 token cap、section_path、role 过滤、unit_id 唯一性。

效果

  • 811 body_units20 篇论文
  • unit_kind 全部为 body(无 backmatter_bodypolicy 变更后合并)
  • token cap: 全部 ≤1000 tokens
  • reference items 未泄漏
  • 全局 unit_id 唯一

风险

  • Policy 变更_body_unit_role_kind 从 inclusion list 改为 exclusion list 现在包含 abstract_bodyauthorsfootnote 等之前排除的角色。这是意图中的行为, 但如果下游系统依赖 unit_kind=="backmatter_body",会出问题(目前无下游消费该字段)。
  • abstract_body 的分类可能不准确OCR pipeline 的误标),导致一些非 abstract 内容 被当作 abstract_body 处理—但这不影响检索质量。
  • 7TG7U4U4 的 "SI Supporting Information" 被归类为最后一个 section heading 而不是 backmatter heading其 body_units 内容包含 author bios 和 reference list portions。 这是正确的block 在 rendered fulltext 中,不是 reference zone但 section_path 不直观。

TC-4: Object Units

内容

从真实 role-index.json 验证:

  • object_kind 为 "figure" 或 "table"
  • caption_text 非空section_path 非空
  • role_index key 兼容:captionsfigure_captions 都行

方法

Layer A 自动脚本从 role-index.json + tree + blocks 重建 object_units 检查 object_kind、caption、section_path。key 兼容性单独测试(不在论文循环内)。

效果

  • 636 object units/篇22 篇论文共约 200+ units
  • 全部 object_kind 正确
  • key 兼容性通过(captionsfigure_captions 都能产出)

风险

  • P0: unit_id collision — 真实 role_index 没有 figure_id/table_id fallback 为空字符串,导致 uid = "{paper_id}:obj:"INSERT OR REPLACE 时覆盖。 已在代码中修复:使用 f"{obj_type}:p{page}:{block_id}" 作为 fallback。
  • P0: caption_key 未 page-qualifiedfind_owning_node 检查 page-qualified subtree_block_ids,但 caption_bid 只有裸 block_id始终无法匹配。 已在代码中修复caption_key = f"p{page}:{caption_bid}"
  • 修复验证: DB count == list count所有 unit_id 唯一。

TC-5: FTS No Duplicate

内容

验证 FTS5 表行数等于 body_units 中 indexable=1 的行数:

  • 顺序 upsert 多篇论文后 FTS 不重复
  • 同一论文重复 upsert 不累积
  • 全库 upsert 后总数一致

方法

Layer A 自动脚本使用 _upsert_body_units 对 10 篇论文顺序 upsert 每步检查 FTS line count。

效果

  • FTS=811, indexable body_units=811严格相等
  • 重复 upsert 不增加行数

风险

  • FTS5 external content 表在空表时 DELETE 会抛 DatabaseError,已通过 try/except 处理。
  • 如果 future schema 变更改变了 indexable 的定义,需要重新验证。

TC-6: Content Discovery

内容

验证 Content Discovery 行为:

  • 正文章节能检索到结果
  • 无匹配时返回 coverage 和 next_action不静默 fallback 到 metadata
  • DB 不存在时返回错误

方法

部分覆盖在 Layer A 脚本中(从 body_units 取高频词查询 FTS。 不静默 fallback 的逻辑在 Layer A 的 reference exclusion 测试中覆盖。 DB 不存在测试需要模拟空库。

效果

  • FTS 查询正常返回结果
  • reference 内容被正确排除
  • 空库错误路径(build_body_units 对空 inputs 返回空 list

风险

  • Content Discovery 的 next_action 精确格式未验证(依赖下游命令定义)
  • TC-6.2/6.3 的完整覆盖需要 content-discovery 命令的 e2e 测试,当前只覆盖了 FTS 部分

TC-7: Memory Builder Incremental

内容

验证 build_from_index 增量检测:

  • Full rebuild 产出 body_units 且 FTS 一致
  • 再次 run 无变化hash match
  • OCR_hash 变化触发增量
  • retrieval_policy_version 变化触发增量

方法

直接调用 build_from_index,检查返回结果和 DB 状态。

效果

  • body_units=811, FTS=811
  • 再次 run: hash_match=True, 无变化
  • RETRIEVAL_POLICY_VERSION = l4.body.v1

风险

  • TC-7.3(修改 result-hash.txt 后触发增量)未完整执行——依赖修改真实文件,目前通过 hash mismatch 间接验证。
  • TC-7.4(空 vault 不崩溃)未单独测,但 build_from_index 对空 index 返回 0 papers。

TC-8: Embed Build (Layer B)

内容

验证 embedding pipeline:

  • body_units 路径:嵌入 body_units 到 paperforge_body collection
  • Legacy 路径:未 rebuild 论文走 paperforge_fulltext 路径
  • Resume 按 body_units_hash 检测变更
  • 双 collection 状态输出
  • 无 body_units 但有 structured files 时给出提示

方法

检查 ChromaDB collection 内容,对比 body_units_hash验证 resume 逻辑。

效果

指标
body_chunk_count 811
fulltext_chunk_count 57,435
total_chunks 58,246
model Qwen/Qwen3-Embedding-4B
3 篇嵌入论文的 hash mismatch 全部检测到policy 变更后 hash 不同)

风险

  • ChromaDB query limit 陷阱col.get(limit=100) 只返回 100 条记录, 在 811 chunks 中抽样只看到 3 篇 paper测试脚本 bug实际全部 20 篇已正确嵌入。
  • Embed 慢fulltext 57435 chunks 的 full rebuild 需要大量 API 调用。 建议使用 --resume 模式增量更新。
  • API key 泄露风险:测试使用了 SiliconFlow key应在测试后轮换。

TC-9: Retrieve Merge (Layer B)

内容

验证 merge_retrieve 混合检索:

  • 同时返回 body_unitlegacy_chunk 结果
  • (source, unit_id) 去重
  • Per-paper cap每篇最多 2 条结果)
  • 空查询返回 5 条低分结果vector search 行为)
  • 无 body 向量时 fallback 到 fulltext

方法

使用 merge_retrieve(vault, query, limit=N) 执行查询, 检查 source、去重、cap。

效果

  • 混合返回VNS 查询返回 5 body + 5 legacy
  • 去重0 重复body 有 unit_idlegacy 有 chunk_index
  • Per-paper cap无 paper 超过 2 条
  • 差查询返回 5 条 ~0.55 低分结果

风险

  • Legacy chunks 没有标准 unit_id——使用 (source, paper_id, hash(chunk_text)) 作为后备唯一键。 如果 legacy chunks 在 future 被适配为有 unit_iddedup 逻辑会更可靠。
  • body 向量只有 20 篇论文的实际数据,混合查询主要体现 legacy 结果。 当 709 篇也获得 body 向量后,混合分布会更平衡。
  • 向量搜索的 limit * 2 (expand) 策略在 body 集合较小时811 vs 57k可能导致 body 结果被 legacy 结果淹没。当前 merge_retrieve 按 score 降序排序,不做 source bias。

TC-10: Backmatter & Abstract

内容

验证:

  • backmatter_body section_path 包含合理关键词(旧 policy现已 moot
  • Abstract 覆盖策略

方法

检查 body_units 中 abstract_body 块的分布。验证 paper_fts 可以搜到 abstract 内容。

效果

  • Abstract_body 现在进入 body_units(新 policy排除 ref zone其余全纳入
  • paper_fts metadata 搜索:"abstract" 匹配 35 篇论文
  • 22 篇 body_units 中 abstract_in_body_units=0 —abstract_body 块被合并在其所在 section 中, 没有独立的 "Abstract" 标题

风险

  • abstract_body OCR 分类可能不准,一些 body paragraph 被标为 abstract_block— 目前没影响,但如果未来需要精确 abstract filter 就有问题。

TC-10.2: Abstract 覆盖决策(已过时)

  • 旧决策abstract 不进 body_units由 paper_fts 覆盖
  • 新 policy 变更:_body_unit_role_kind 只排除 reference zoneabstract_body 现在进入 body_units
  • 影响无负面abstract 内容现在可被 body retrieval 命中paper_fts 仍然可用)
  • 如果未来需要恢复旧行为,需为 abstract_body 角色加 exception

TC-11: Regression

内容

验证:

  • 单元测试全部通过
  • CLI 命令不破坏
  • 跨 vault 切换正常

方法

运行 pytest、CLI help、embed status。

效果

  • 153 passed, 1 skip
  • paperforge --help / embed status --json 正常
  • unit tests 覆盖 n 至 n 层

风险

  • 测试包含 1 个 pre-existing skip非本改动引入
  • 未做跨 vault 切换的 e2e 测试(需要两个真实 vault

风险总览

风险等级 问题 影响 缓解
709 篇论文无 structure-tree.json → 无 body_units → 无 body 向量 只有 22/734 篇论文使用 body 向量检索 全量 rebuildderived_stale=Trueocr rebuild --allmemory buildembed build --resume
abstract_body 进入 body_units新 policy 行为) 行为与旧设计文档不符,但不影响检索质量 TC-10.2 已记录;如需回退需加 role exception
Legacy chunks dedup 基于 hash(chunk_text) 极少情况下两条相同文本的 legacy chunk 会被去重 body 向量逐步替换 legacy问题自愈
bounds_map 依赖 node.page 字段 future tree 结构变更可能破坏 bounds lookup 单元测试 + Layer A 自动检查
极低 FTS5 空表 DatabaseError 发生在首次 build 前 try/except pass 已处理
极低 ChromaDB get(limit=100) 抽样偏差 开发/调试时误判向量覆盖范围 使用 col.count()get(limit=col.count())

PR 7: Post-Review Hardening

Based on the test report review (2026-07-06, attached external review):

Code Fixes

  • object unit_id fallback: figure_id/table_id 缺失时用 f"{obj_type}:p{page}:{block_id}" 替代空字符串 → 解决 DB overwrite
  • object caption_key page-qualified: find_owning_node 使用 f"p{page}:{caption_bid}" 匹配 page-qualified subtree_block_ids
  • structure node_id page-qualified: node_id = f"sec:p{h['page']}:{h['block_id']}" 减少 emitted_order 变化引起的 vector ID 大面积变更
  • object block_map page-qualified: 与 body_units 一致

Added Tests

  • Object DB persistence: upsert 后 DB count == list count, unit_id 唯一性
  • Coverage gate: v2_tree / body_papers / body_vector 比例输出

Remaining Risks

  • 709 篇等待全量 rebuild 后才会有 v2 tree + body vectors
  • Content Discovery CLI e2e 未完整执行(仅 FTS 部分)
  • Legacy collection 57k chunks 可能在混合检索中淹没 body 结果

结论

Memory Layer 全面改造通过功能验证。

  • Layer A1609/1609 passartifact 一致性)
  • Layer B混合检索、resume hash 检测、去重、per-paper cap 全部正常
  • Embedding811 body chunks in 20 papers, Qwen/Qwen3-Embedding-4B, healthy
  • PR 7 Hardeningunit_id/caption_key/block_map page-qualify 修复Object DB 验证coverage gate 输出

还剩 709 篇论文等待重建(无 v2 structure tree → 无 body_units → 无 body 向量)。 如果你决定全量跑,流程是:

1. ocr rebuild --mark-stale (或用脚本设置 derived_stale=True)
2. paperforge ocr rebuild --all --parallel 4
3. paperforge memory build
4. paperforge embed build --resume (需 API key)