"""查标的纯函数:检索、投标侧证据校验、问题级别(m3-plan §2)。""" from __future__ import annotations from uuid import uuid4 from bid_doctor.check import findings, match from bid_doctor.check.retrieval import Index, tokens from bid_doctor.document.pipeline.text_layer import Sentence def sent(seq: int, text: str, page: int = 1) -> Sentence: return Sentence(key=f"doc:p{page}:s{seq}", page_no=page, seq=seq, text=text, quads=[[0, 0, 1, 1]]) def test_tokens_keep_number_unit_and_model_names() -> None: t = tokens("服务器内存不低于32GB,支持10GE和硬件BFD") assert "32gb" in t and "10ge" in t and "bfd" in t and "内存" in t def test_bm25_ranks_the_responding_sentence_first() -> None: index = Index( [ sent(1, "我公司提供7×24小时售后服务。"), sent(2, "服务器内存配置为64GB,并提供产品彩页。"), sent(3, "交换机支持硬件BFD,最小发送间隔3.3毫秒。"), ] ) (top, *_) = index.search("交换机支持硬件BFD 最小报文发送间隔不大于50毫秒", 3) assert top.sentence.seq == 3 assert index.search("完全无关的词语组合", 3) == [] def bid() -> tuple[match.BidDoc, dict[str, tuple[match.BidDoc, Sentence]]]: s = sent(9, "服务器内存不低于32GB的要求:我方服务器内存配置为64GB。") d = match.BidDoc(uuid4(), 1, "技术分册", [s]) return d, {"d1p1s9": (d, s)} def test_responded_needs_verified_evidence() -> None: """模型说已响应,但证据没通过:不能判已响应,记「未找到响应」并带原因码(C3)。""" _, allowed = bid() ok = match.judge( {"verdict": "responded", "evidence": [{"sid": "d1p1s9", "quote": "我方服务器内存配置为64GB。"}]}, allowed, [] ) assert ok.verdict == "responded" and ok.evidence_status == "verified" and len(ok.evidence) == 1 no_ev = match.judge({"verdict": "responded", "evidence": []}, allowed, []) assert (no_ev.verdict, no_ev.evidence_status, no_ev.reason_code) == ("not_found", "unverified", "evidence_missing") rewritten = match.judge( {"verdict": "responded", "evidence": [{"sid": "d1p1s9", "quote": "内存为64GB,满足要求。"}]}, allowed, [] ) assert rewritten.reason_code == "quote_not_original" and rewritten.detail["claimed_pages"] == [1] other = match.judge({"verdict": "responded", "evidence": [{"sid": "d1p2s1", "quote": "x" * 10}]}, allowed, []) assert other.reason_code == "sid_unknown" missing = match.judge(None, allowed, ["技术分册"]) assert (missing.verdict, missing.reason_code) == ("undetermined", "missing_answer") def test_severity_comes_from_the_item_not_the_model() -> None: assert findings.severity({"importance": "substantive", "categories": ["tech_param", "invalidity"]}) == "fatal" assert findings.severity({"importance": "normal", "categories": ["qualification"]}) == "fatal" assert findings.severity({"importance": "important", "categories": ["tech_param"]}) == "important" assert findings.severity({"importance": "normal", "categories": ["scoring"]}) == "important" assert findings.severity({"importance": "normal", "categories": ["tech_param"]}) == "hint" def test_no_value_is_a_hint_and_unverified_is_explained() -> None: item = {"title": "服务器内存", "importance": "substantive", "categories": ["tech_param"]} nv = findings.spec(match.Result("no_value", "model", "verified"), item) assert nv is not None and nv.severity == "hint" and nv.title.startswith("未给出具体值") uv = findings.spec( match.Result("not_found", "model", "unverified", "quote_not_original", detail={"claimed_pages": [3]}), item ) assert uv is not None and uv.severity == "fatal" and "第 3 页" in uv.explanation and "没能核对" in uv.explanation assert findings.spec(match.Result("responded", "model", "verified"), item) is None