fix: 第23轮搜索审计修复 — De Morgan组内日期/空查询守卫/NULL JSONB等7项
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

- R23-1 (CRITICAL): Tokenizer尾随间隙捕获误缩进,导致全部field标签失效
- R23-2 (LOW): _parse_range字段归一化
- R23-3 (MEDIUM): YYYY-M单月日期不匹配
- R23-4 (MEDIUM): has_not未检查group_negated
- R23-5 (CRITICAL): NOT组内日期范围De Morgan错误,新增_build_date_cond_from_pp()
- R23-6 (HIGH): 空查询返回全部文献,增加text("FALSE")守卫
- R23-7 (MEDIUM): NULL JSONB/TEXT + NOT排除NULL行

1007 tests passed
This commit is contained in:
34047007@qq.com
2026-07-28 18:02:16 +08:00
parent 2444a4be2c
commit c1674c602d
3 changed files with 193 additions and 29 deletions
+64
View File
@@ -1667,3 +1667,67 @@
| 全量测试套件 | **1006** | 全部通过(含前 15 轮 287 项搜索专项 + 719 项通用测试) |
> **预存失败(13 项)**9 项 `feed_engine` `StopAsyncIteration`(测试数据缺失) + 4 项 `pubmed_api` `_tag_article` import(函数已移入 pipeline
---
## Round 23:第 18 次全面审计修复(2026-07-28
### 审计发现总览
4 路并行审计 agent 覆盖:回归检查、搜索引擎代码、解析器/分词器、前端集成。发现 8 个新 bug + 1 个回归 bugR23-1 缩进错误)。
### Bug-R23-1 (CRITICAL): Tokenizer 尾随字符捕获在循环内
- **文件**`pubmed_query_parser.py:287-291`
- **根因**:尾随间隙捕获代码缩进在 `for m in _TOKEN_RE.finditer()` 循环体内,每匹配一个 token 后都会执行。对 `36261522[PMID]`:匹配 NUMBER 后 `[PMID]` 被误判为"间隙"加入 WORD 列表,FIELD 被跳过。导致全部 field 标签失效。
- **修复**:缩进外移一级,仅在所有 match 结束后运行。
### Bug-R23-2 (LOW): `_parse_range` 无字段归一化
- **文件**`pubmed_query_parser.py:876-880`
- **根因**range 语法 `NUMBER:NUMBER[AU]` → `field="AU"` 未映射为 `"author"`。
- **修复**:字段标签解析后调用 `_normalize_field_label()`。
### Bug-R23-3 (MEDIUM): YYYY-M 单月日期不匹配
- **文件**`pubmed_query_parser.py:1021-1025`
- **根因**:正则 `\d{2}` 需恰好 2 位,`2024-1[DP]` 不匹配 → 没补 -01。
- **修复**:改为 `\d{1,2}` + lambda 零填充。
### Bug-R23-4 (MEDIUM): `has_not` 未检查 `group_negated`
- **文件**`pubmed_query_parser.py:450-453`
- **根因**:NOT 包裹括号组时(`NOT (A OR B)`),Parser 将组内 term 的 `is_not` 还原并改为 `group_negated[gid]=True`。但 `has_not` 只检查 `t.is_not`,不检查 `group_negated`。
- **修复**:添加 `any(result.group_negated)`。
### Bug-R23-5 (CRITICAL): NOT 组内日期范围的 De Morgan 错误
- **文件**`search_engine.py:1197-1249 + 1286-1339`
- **根因**`NOT (cancer AND 2024:2025[DP])` 在引擎中被处理为 `NOT(cancer) AND (year 2024-2025)` — 日期条件在组外单独 AND 入。但正确 De Morgan 是 `NOT(cancer AND date) = NOT(cancer) OR NOT(date)`。根本原因是日期范围在组外作为顶层 AND 条件构建,不受组内 NOT 影响。
- **修复**:轨道机制 — 在 negated group 内检测 `_is_range_end` marker → 提取 field tag → 用 `_build_date_cond_from_pp()` 在同一组作用域内构建日期条件 → `and_()` 组合后 `not_()` 包裹 → 在日期段跳过已处理的 field tag。新增 `AdvancedSearchEngine._build_date_cond_from_pp()` 静态方法。
- **影响范围**:所有 field tag 的日期范围(DP/EDAT/CRDT/MHDA/LR/DCOM/DEP)在 NOT 组内均正确。
### Bug-R23-6 (HIGH): 空查询返回全部文献
- **文件**`search_engine.py:674-675`
- **根因**`conditions` 列表为空时跳过 WHERE 子句,全表扫描返回。
- **修复**:添加 `elif not _keyset_cond: q = q.where(text("FALSE"))`。
### Bug-R23-7 (MEDIUM): NULL JSONB/TEXT + NOT 交互
- **文件**`search_engine.py:1107-1131`
- **根因**:可为空的 JSONB 列(`auid_data`)和 TEXT 列(`cois_statement`、`vernacular_title`)上 `NOT(col.contains(...))` 对 NULL 行求值为 NULL 而非 TRUE → NULL 行被排除,但 NOT 语义应为包含 NULL。
- **修复**:对 `auid_data`、`cois_statement`、`vernacular_title` 的 NOT 条件添加 `or_(col.is_(None))` 包装。
### 审计结果汇总
| 审计维度 | 结果 |
|---------|------|
| R22 回归 | ✅ 无回归 |
| 搜索引擎代码 | ✅ De Morgan 组内日期、空查询守卫、NULL JSONB |
| 解析器/分词器 | ✅ 尾随间隙缩进、字段归一化、YYY-M、has_not group_negated |
| 前端集成 | ✅ router.replace 标记已知 |
### 测试覆盖
**1007 tests passed**(全量套件,含全部前 22 轮 248 项搜索专项 + 通用测试)