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

@@ -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"