From 00eb0bc1d03d8d76b9fc752f637b655ba88368ba Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 11 Sep 2026 14:18:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(r4-27):=20normalize=5Fsource=20title=20?= =?UTF-8?q?=E7=A9=BA=E5=80=BC=E5=9B=9E=E5=A1=AB(=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E5=90=8D/=E5=85=83=E6=95=B0=E6=8D=AE=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E5=AD=97=E6=AE=B5,=E6=AD=A3=E6=96=87=E7=89=87=E6=AE=B5?= =?UTF-8?q?=E7=BB=9D=E4=B8=8D=E5=8F=82=E4=B8=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 消息级 sources items 的 title 为空导致前端文献列表渲染出正文片段。 回填优先级:顶层标题类字段(publication_title/article_title/journal_title/name) → metadata 标题类字段(paper_title/source_entity/name/source) → 顶层 source(断言主体实体名);全部缺失保持 null(前端有 fallback)。 paper_id/score/snippet 等既有字段不受影响。 Co-Authored-By: Claude Fable 5 --- sdk/app/services/normalize.py | 27 ++++++++++++++++++ sdk/app/services/test_normalize.py | 46 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/sdk/app/services/normalize.py b/sdk/app/services/normalize.py index fc3d40d..0fd94b6 100644 --- a/sdk/app/services/normalize.py +++ b/sdk/app/services/normalize.py @@ -20,6 +20,16 @@ def pick(obj: dict, *names: str, default=None): # --------------------------------------------------------------------------- # §8 Source 归一化(缺失字段键必须存在,值为 null) # --------------------------------------------------------------------------- +# title 回填优先级(缺陷 #27,2026-09-11):title/paper_title 缺失时, +# 依次从以下字段取可读文本(论文标题优先于实体名),全部缺失才保持 null。 +# 排除 content/evidence/text/snippet 正文字段——正文片段不是标题。 +_TITLE_FALLBACK_TOPLEVEL = ( + "publication_title", "article_title", "journal_title", "name", +) +_TITLE_FALLBACK_METADATA = ( + "paper_title", "publication_title", "article_title", "journal_title", + "source_entity", "name", "source", +) def normalize_source(item: dict[str, Any] | None) -> dict[str, Any]: """单个 source item → API.md §8 形态(所有键存在,缺失为 null)。 @@ -41,6 +51,23 @@ def normalize_source(item: dict[str, Any] | None) -> dict[str, Any]: source = pick(item, "source") # 断言主体实体名(上游语义) title = pick(item, "title", "paper_title") + # 缺陷 #27:title 为空时按固定优先级回填—— + # 顶层标题类字段 → metadata 标题类字段 → 顶层 source(断言主体实体名)。 + # 正文字段绝不参与(正文片段不是标题)。 + if not title: + for _key in _TITLE_FALLBACK_TOPLEVEL: + _candidate = pick(item, _key) + if _candidate: + title = _candidate + break + if not title and isinstance(pick(item, "metadata"), dict): + for _key in _TITLE_FALLBACK_METADATA: + _candidate = pick(item["metadata"], _key) + if _candidate: + title = _candidate + break + if not title: + title = source doi = pick(item, "doi") url = pick(item, "url", "doi_url") diff --git a/sdk/app/services/test_normalize.py b/sdk/app/services/test_normalize.py index 8723245..f51a207 100644 --- a/sdk/app/services/test_normalize.py +++ b/sdk/app/services/test_normalize.py @@ -84,6 +84,52 @@ def test_normalize_source_only_doi_derives_url(): assert src["metadata"] == {} +# --------------------------------------------------------------------------- +# §8 title 回填(缺陷 #27,2026-09-11) +# --------------------------------------------------------------------------- + +def test_normalize_source_title_fill_from_source_entity(): + """title 为空 -> 回填 source(断言主体实体名),正文片段绝不参与。""" + src = normalize_source({ + "source": "OsSPL14", + "title": "", + "evidence": "## 正文片段开头(不是标题)", + "paper_id": "P1", + }) + assert src["title"] == "OsSPL14" + # title 即 source 回填:两者相等,source_entity 不落 metadata(不重复) + assert src["metadata"].get("source_entity") is None + assert src["paper_id"] == "P1" + + +def test_normalize_source_title_fill_from_metadata(): + """title/paper_title 均空 -> metadata 标题类字段回填。""" + src = normalize_source({ + "source": "entity", + "metadata": {"paper_title": "水稻抗病基因研究进展", "source_entity": "entity"}, + }) + assert src["title"] == "水稻抗病基因研究进展" + + +def test_normalize_source_title_not_overridden(): + """title 非空 -> 不回填(既有语义不变)。""" + src = normalize_source({ + "title": "Real Title", + "source": "Entity", + "paper_title": "Other", + "metadata": {"paper_title": "Meta"}, + }) + assert src["title"] == "Real Title" + assert src["metadata"]["source_entity"] == "Entity" + + +def test_normalize_source_title_all_empty_stays_none(): + """全空 -> 保持 null(前端有 fallback)。""" + src = normalize_source({"content": "snippet only", "paper_id": "P1"}) + assert src["title"] is None + assert src["snippet"] == "snippet only" + + def test_normalize_source_alias_chains(): src = normalize_source({ "content": "snippet text",