feat(check): 响应方式识别(C12)和「这条要不要投标人回答」;评测集纠错
- checklist/response_mode.py:按类别(技术 / 商务)识别招标文件的响应方式(逐条响应 / 默认接受),规则优先; 两种写法冲突按逐条,认不出按逐条并写进覆盖范围。 - check/applicability.py:无效情形(★ / 斜体下划线标注的条款除外)、日期与递交、没要求附材料的评分标准、 无需提供证明或由采购人查询的资格,不逐条找响应,写进覆盖范围;要求附证明材料按原文判断,中标后才交的不算。 - 默认接受的要求没找到响应:一般要求记 implicit_accepted、不出问题;实质性要求记 implicit_review、提示级 人工核对;要附证明材料的不免(迁移 0006)。 - match.py:修同一行单元格又是别的条目候选句时的 KeyError(每条清单项单独记候选句,句子从总表里查)。 - 评测集:注入器同一要求的所有表格行一起改;散文、附件、格式文件里也响应了的要求不挂漏响应类问题; 错位响应必须是另一个参数;复核判不成立的单元不再挂;regen.py 按固定种子重新生成。 开发集 17 份:规矩投标文件报出的问题 639 → 229,误报 21.3 → 12.4 / 100,召回 68% → 65%,致命级 20 → 21 / 32 (docs/architecture/check-eval.md §4)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,3 +77,57 @@ def test_no_value_is_a_hint_and_unverified_is_explained() -> None:
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def test_response_mode_by_category() -> None:
|
||||
from bid_doctor.checklist import response_mode
|
||||
|
||||
sents = [
|
||||
sent(1, "技术规格偏离表"),
|
||||
sent(2, "投标人应将所有偏离的条目列入上表,未列入上表的视作投标人完全接受。"),
|
||||
sent(3, "商务条款偏离表"),
|
||||
sent(4, "投标人须对商务要求逐条作出响应。"),
|
||||
]
|
||||
m = response_mode.detect(sents)
|
||||
assert (m["technical"].mode, m["technical"].status) == ("implicit", "found")
|
||||
assert (m["commercial"].mode, m["commercial"].status) == ("explicit", "found")
|
||||
both = response_mode.detect([sent(1, "须逐项作出响应。"), sent(2, "否则将认为投标人接受招标文件的要求。")])
|
||||
assert (both["technical"].mode, both["technical"].status) == ("explicit", "conflict") # 写法冲突按逐条(C12)
|
||||
none = response_mode.detect([sent(1, "本项目为货物采购。")])
|
||||
assert (none["commercial"].mode, none["commercial"].status) == ("explicit", "unknown")
|
||||
|
||||
|
||||
def test_applicability_skips_items_that_need_no_response() -> None:
|
||||
from bid_doctor.check import applicability
|
||||
from bid_doctor.checklist import response_mode
|
||||
|
||||
modes = response_mode.detect([sent(1, "未列入偏离表的技术要求视作投标人完全接受。")])
|
||||
decide = applicability.decide
|
||||
assert decide({"categories": ["invalidity"], "importance_basis": "tender_text"}, modes).skip == "condition"
|
||||
assert decide({"categories": ["invalidity"], "importance_basis": "marker"}, modes).match # ★ 条款是要求
|
||||
assert decide({"categories": ["schedule"]}, modes).skip == "schedule"
|
||||
assert decide({"categories": ["scoring"]}, modes).skip == "scoring"
|
||||
assert decide({"categories": ["scoring"], "evidence_required": "业绩合同"}, modes).match
|
||||
assert decide({"categories": ["scoring"], "title": "业绩(2 分):提供近三年类似项目合同复印件"}, modes).match
|
||||
assert (
|
||||
decide({"categories": ["scoring"], "title": "提供检测报告,中标后签订合同前提供原件"}, modes).skip == "scoring"
|
||||
)
|
||||
assert decide({"categories": ["tech_param"], "title": "支持硬件BFD,提供官网截图证明"}, modes).evidence
|
||||
assert decide({"categories": ["qualification"], "title": "未被列入信用中国失信名单"}, modes).skip == "no_proof"
|
||||
d = decide({"categories": ["tech_param"], "title": "内存"}, modes)
|
||||
assert d.match and d.mode == "implicit"
|
||||
|
||||
|
||||
def test_implicit_not_found_is_accepted_but_substantive_gets_a_hint() -> None:
|
||||
from uuid import uuid4
|
||||
|
||||
from bid_doctor.check import applicability
|
||||
|
||||
a, b = uuid4(), uuid4()
|
||||
results = {a: match.Result("not_found", "model"), b: match.Result("not_found", "model")}
|
||||
items = {a: {"importance": "normal"}, b: {"importance": "substantive", "title": "★双电源"}}
|
||||
decisions = {a: applicability.Decision(True, mode="implicit"), b: applicability.Decision(True, mode="implicit")}
|
||||
match.apply_modes(results, items, decisions)
|
||||
assert results[a].verdict == "implicit_accepted" and findings.spec(results[a], items[a]) is None
|
||||
spec = findings.spec(results[b], items[b])
|
||||
assert results[b].verdict == "implicit_review" and spec is not None and spec.severity == "hint"
|
||||
|
||||
Reference in New Issue
Block a user