fix(check): 修留出集暴露的 5 处(规则集 2026.11-r3)

- 响应方式:「逐条响应」要说的是要求 / 参数 / 偏离,声明函填写说明不算;「否则视为无效响应」不算默认接受
- 偏离栏按「是不是明确满足」判断:偏离栏、响应情况栏一起看,其他短写法请人核对(最高重要);表体序号认「技-86」等
- 另报价、附加条件按结构判断(另一个不同的总价金额、保留 / 另行 / 调整报价等)
- 空白栏:「精确到年月日」不算没填
- 比对时折叠异体字(戶→户等),原文照存
- 记录决策 E2:先修再跑国动真实投标文件

开发集召回 75%、规矩投标文件问题数不变;留出集参考数字见 check-eval §12(已看过,仅参考)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 20:34:12 +08:00
parent fee1c0f842
commit 902a130c82
13 changed files with 436 additions and 70 deletions

View File

@@ -297,7 +297,7 @@ async def test_rule_findings_are_written_with_citations_and_carried(user: User)
# 「让利后总价」和投标总价不同:多半另报了一个价,但不是「备选方案」那种明确的两个报价,按重要
assert rule["PRICE.MULTIPLE_QUOTES"]["severity"] == "important" and rule["FORM.BLANK_FIELD"]["severity"] == "hint"
assert any("报价形式" in x for x in run["coverage"]["checked"])
assert run["coverage"]["rule_set_version"] == "2026.11-r2"
assert run["coverage"]["rule_set_version"] == "2026.11-r3"
await user.post(f"/findings/{cap['id']}/resolve", json={"status": "ignored", "note": "大写栏已手工更正"})
again = await run_check(user, project_id, [bid["id"]])

View File

@@ -257,3 +257,102 @@ def test_validity_in_letter_table_and_chinese_dates() -> None:
("FORM.VALIDITY_SHORT", "fatal"),
("FORM.DATE_AFTER_DEADLINE", "hint"),
}
def test_generalized_quotes_conditions_and_blanks() -> None:
"""留出集暴露的写法(check-eval §11)改成按结构判断;例句既不取自开发集,也不取自留出集。"""
d = doc(
[
*opening_table("2,000,000.00", "贰佰万元整"),
para(10, "若贵方接受分批付款,我方总价可下调为1,900,000.00元。", page=2),
para(11, "另附选配清单,选配部分合计报价800,000.00元。", page=2),
para(12, "本单位类似项目业绩合同金额2,500,000.00元。", page=2), # 业绩金额:不是另一个报价
para(13, "我方投标总价为2,000,000.00元,已含全部税费。", page=2), # 等于总价
para(14, "如遇原材料价格大幅上涨,供货价格由双方另行商定。", page=3),
para(15, "我方保留根据厂家排产情况调整交付周期的权利。", page=3),
para(16, "设备数量以届时实际供货清单为准,合同金额相应结算。", page=3),
para(17, "合同最终以双方签订的合同文本为准。", page=3), # 常见套话:不算
para(18, "日期选择控件精确到年月日。", page=4), # 说明:不算没填
para(19, "签署时间:年月日", page=4),
]
)
hits, _ = rules.run(rules.Facts(), [d], [])
got = sorted((h.code, h.evidence[0][1].seq) for h in hits)
assert got == [
("FORM.BLANK_FIELD", 19),
("FORM.CONDITION_ADDED", 14),
("FORM.CONDITION_ADDED", 15),
("FORM.CONDITION_ADDED", 16),
("PRICE.MULTIPLE_QUOTES", 10),
("PRICE.MULTIPLE_QUOTES", 11),
]
assert {h.severity for h in hits if h.code == "PRICE.MULTIPLE_QUOTES"} == {"important"}
def test_deviation_column_values_other_than_clear_ok_need_review() -> None:
"""偏离栏按「是不是明确满足」判断:「基本一致」这类说法请人核对(重要),「无偏离」不报。"""
rows = [
cell(1, "技术偏离表", 74, 76, w=200),
cell(2, "序号", 80, 100, w=20),
cell(3, "招标要求", 110, 100, w=150),
cell(4, "偏离", 300, 100),
cell(5, "技-1", 82, 124, w=8),
cell(6, "支持双电源冗余", 110, 124, w=150),
cell(7, "无偏离", 300, 124),
cell(8, "技-2", 82, 146, w=8),
cell(9, "支持热插拔风扇模块", 110, 146, w=150),
cell(10, "基本一致", 300, 146),
]
hits, _ = rules.run(rules.Facts(), [doc(rows)], [])
assert [(h.title, h.severity) for h in hits] == [("响应表里写了「基本一致」", "important")]
def test_variant_glyphs_compare_equal() -> None:
from bid_doctor.document.pipeline.text_layer import norm
assert norm("用戶 可以查看") == norm("用户可以查看")
def test_response_content_column_and_wrapped_cells_are_not_flagged() -> None:
"""有「偏离」栏时只看偏离栏:「投标响应」栏写「支持」「3年」是响应内容,不报;窄栏把「无偏离」折成两段,拼起来判断。"""
rows = [
cell(1, "序号", 80, 100, w=20),
cell(2, "招标要求", 110, 100, w=120),
cell(3, "投标响应", 240, 100),
cell(4, "偏离", 310, 100, w=30),
cell(5, "1", 82, 124, w=8),
cell(6, "质保期不少于3年", 110, 124, w=120),
cell(7, "3年", 240, 124),
cell(8, "无偏", 310, 124, w=30),
cell(9, "离", 310, 136, w=30),
cell(10, "2", 82, 160, w=8),
cell(11, "支持远程运维", 110, 160, w=120),
cell(12, "支持", 240, 160),
cell(13, "无偏离", 310, 160, w=30),
]
for c in rows:
x0, y0, x1, _ = c.quads[0]
c.cell = [
x0,
100 if y0 == 100 else 124 if y0 in (124, 136) else 160,
x1,
(124 if y0 == 100 else 160 if y0 in (124, 136) else 190),
]
hits, _ = rules.run(rules.Facts(), [doc(rows)], [])
assert hits == []
def test_long_response_mentioning_noncompliance_is_not_negative() -> None:
"""响应栏里一大段正常响应提到「不符合……时预警」,不是不满足;只有开头就写不满足才算。"""
from bid_doctor.check import deviation
def s(text: str) -> Sentence:
x = cell(1, text, 0, 0)
x.cell = [0, 0, 100, 20]
return x
long_ok = s("满足,对规划条件不符合村庄规划的用地范围进行高亮标记并预警")
assert deviation.judge([long_ok], "投标响应情况") == "ok"
assert deviation.judge([s("部分响应")], "投标响应情况") == "negative"
assert deviation.judge([s("不满足,现有版本仅支持单机部署")], "投标响应情况") == "negative"
assert deviation.judge([s("无偏"), s("离")], "偏离") == "ok"

View File

@@ -91,7 +91,9 @@ def test_response_mode_by_category() -> None:
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, "否则将认为投标人接受招标文件的要求。")])
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")
@@ -293,3 +295,54 @@ def test_award_time_wording_is_not_award_stage_evidence() -> None:
assert applicability.decide(scoring, {}).match
later = {**scoring, "requirement_text": "签订合同时提供业绩合同原件备查。"}
assert applicability.decide(later, {}).skip == "scoring"
def test_response_mode_ignores_declaration_boilerplate_and_negative_consequence() -> None:
"""响应方式识别(check-eval §11):声明函「逐一、准确填写产品名称……」不算要求逐条响应;
「否则视为无效响应」说的是不响应的后果,不算默认接受。"""
from bid_doctor.checklist import response_mode
implicit = [
sent(1, "供应商需在《声明函》中逐一、准确填写产品名称、厂名、生产厂址三项信息。"),
sent(2, "对招标文件中的所有商务、技术要求,除本表所列明的所有偏离外,均视作供应商已对之理解和响应。"),
]
modes = response_mode.detect(implicit)
assert (modes["technical"].mode, modes["technical"].status) == ("implicit", "found")
explicit = [
sent(1, "供应商需对磋商文件中的技术要求与商务要求逐项作出响应或偏离说明。"),
sent(2, "供应商须对技术要求中的实质性要求做出明确响应,否则视为无效响应。"),
]
modes = response_mode.detect(explicit)
assert (modes["technical"].mode, modes["technical"].status) == ("explicit", "found")
def test_direct_read_uses_header_column_and_flags_unclear_values() -> None:
"""偏离栏按表头认出来:写的不是明确的「无偏离 / 满足」,改判负偏离并标「请核对」。
级别最高到重要(check-eval §11)。"""
from bid_doctor.check import deviation
def c(seq: int, text: str, x0: float, y: float) -> Sentence:
s = Sentence(key=f"b:p1:s{seq}", page_no=1, seq=seq, text=text, quads=[[x0 + 2, y + 4, x0 + 90, y + 16]])
s.cell = [x0, y, x0 + 100, y + 30]
return s
head = [c(1, "序号", 0, 20), c(2, "招标要求", 100, 20), c(3, "投标响应", 200, 20), c(4, "偏离", 300, 20)]
row1 = [c(5, "1", 0, 50), c(6, "支持双电源", 100, 50), c(7, "满足,支持双电源", 200, 50), c(8, "接近要求", 300, 50)]
row2 = [
c(9, "2", 0, 80),
c(10, "支持热插拔", 100, 80),
c(11, "满足,支持热插拔", 200, 80),
c(12, "无偏离", 300, 80),
]
d = match.BidDoc(uuid4(), 1, "技术分册", [*head, *row1, *row2])
a, b = uuid4(), uuid4()
results = {
a: match.Result("responded", "ai", "verified", evidence=[(d, row1[2])]),
b: match.Result("responded", "ai", "verified", evidence=[(d, row2[2])]),
}
assert deviation.direct_read(results, [d]) == 1
assert results[a].verdict == "negative_deviation" and results[a].detail["deviation_unclear"] is True
assert results[b].verdict == "responded"
spec = findings.spec(results[a], {"title": "支持双电源", "importance": "substantive", "categories": ["tech_param"]})
assert spec is not None and spec.severity == "important" and spec.title.startswith("偏离栏需核对")