Files
bid-doctor/server/src/bid_doctor/check/applicability.py
YANG JIANKUAN 808259da87
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
fix(check): 修国动真实投标文件暴露的 6 处(规则集 r5)
国动真实投标文件查标一次(check-eval §14):44 个问题里有用的 9 个,
致命、重要两级都是误报。按决策 E3 全部修(§15):
- 响应错位:「完全满足,无偏离。」这类复合肯定写法整体算肯定
- 附加条件:「不以……另行收费」是承诺
- 扫描件:文字层 v5 记每页图片占比,带一行说明的扫描页也认;提示写明扫描页范围
- 不需要这家投标人回答的:无特定资格要求、总括性资格要求、中标后才做的事、
  投标文件没声称适用的联合体和价格扣除
- 「方案应明确 A、B、C……」逐项再检索
- 同一批送出的片段都可引用(引文照样核对);出处没过校验的不沿用

国动重新检查 44 → 11(全是提示,有用的 9 个都在);开发集召回 112 → 111
(模型重跑波动),致命级 25/32 不变,规矩的投标文件问题 239 → 213。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:16:12 +08:00

112 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""每条清单项要不要在投标文件里找响应、按什么响应方式判(决策 C12,`docs/architecture/check-eval.md` §2.2 第 ② 项)。
开发集上规矩的投标文件报出的问题,近一半落在不需要投标人回答的条目上(无效情形的列举、日期、评分标准、
采购人自己查询的资格)。这些条目不逐条找响应,写进检查覆盖范围:
- 无效情形、评审规定(只有「无效」类别、不是标注出来的条款):由规则检查(第 2 步)和人工核对覆盖;
- 日期与递交:由规则检查(落款日期、有效期)和需人工核对覆盖;
- 评分标准本身:没写要附什么材料的不找;写了的按证明材料找;
- 资格条件里写明「无需提供」或由采购人查询(信用中国)的不找;
- 写明没有的要求(无特定资格要求)、总括性的「须具备招标文件规定的资格要求」、
中标或签订合同以后才做的事,不找;
- 条件性要求(联合体、残疾人福利性单位 / 监狱企业 / 本国产品的价格扣除):投标文件里有对应的文件、
说明投标人声称适用时才找(国动真实投标文件,check-eval §14)。
其余按类别取招标文件的响应方式:技术参数用技术类,其余用商务类。
"""
from __future__ import annotations
import re
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
from bid_doctor.checklist.response_mode import Mode
# 要求投标时附证明材料(读标的 evidence_required 常常是空的,按原文判断);中标后 / 签合同时才交的不算
EVIDENCE = re.compile(r"(提供|附|出具)[^。;]{0,20}(证明|报告|截图|复印件|扫描件|证书|合同|承诺|声明|社保|发票)")
# 「签订合同时提供」才算中标后;「以合同签订时间为准」(业绩条款常见)不是
AWARD = re.compile(r"(中标后|成交后|签订合同(前|时)(?!间)|合同签订(前|时)(?!间))")
NO_PROOF = re.compile(r"无需提供|不需提供|无须提供|采购人[^。;]{0,12}(查询|核查|查验)|信用中国|信用记录")
# 写明没有要求、不接受:没有要投标人提供的东西(按标题判断,标题是读标写的一句话概括)
# 「不接受联合体」不放这里:投标人真是联合体时恰恰要报,按条件性要求处理
NONE_REQUIRED = re.compile(r"无特定资格要求|特定资格要求[::]?(无|/)")
SPECIAL_PROOF = re.compile(r"特定资格要求的证明") # 招标文件写了「无特定资格要求」时不用交
UMBRELLA = re.compile(
r"(具备|满足|符合)[^。;]{0,4}(招标|磋商|采购|谈判|询价)文件[^。;]{0,4}(规定|要求)的?资格(要求|条件)"
)
AFTER_AWARD = re.compile(r"^(合同签订后|签订合同后|中标后|成交后)")
# 条件性要求:标题里的主体 → 投标文件里出现了就说明投标人声称适用
# (「残疾人福利性单位声明函」写「不属于」也算出现,照常查)
CONDITIONAL = {
"consortium": (re.compile(r"联合体"), re.compile(r"联合体(协议|牵头|各方|成员)")),
"disabled": (re.compile(r"残疾人福利"), re.compile(r"残疾人福利性单位")),
"prison": (re.compile(r"监狱|戒毒"), re.compile(r"监狱企业|戒毒企业")),
"domestic": (re.compile(r"本国产品"), re.compile(r"本国产品")),
}
SKIP_LABELS = {
"condition": "无效情形和评审规定",
"schedule": "日期与递交要求",
"scoring": "评分标准(没要求附证明材料)",
"no_proof": "资格条件(写明无需提供证明,或由采购人查询)",
"price": "报价类要求(分项、单价上限):由规则比对分项报价表",
"none_required": "写明没有的要求(无特定资格要求)",
"umbrella": "总括性的资格要求(由各条具体资格条件覆盖)",
"after_award": "中标或签订合同以后才做的事",
"not_claimed": "条件性要求(联合体、残疾人福利性单位、监狱企业、本国产品),投标文件没有声称适用",
}
@dataclass(frozen=True, slots=True)
class Context:
"""整份清单和投标文件上的事实。claims 为 None 时不知道投标文件写了什么,条件性要求照常查。"""
claims: frozenset[str] | None = None
no_special: bool = False # 招标文件写了「无特定资格要求」
def context(items: Iterable[dict[str, Any]], bid_texts: Iterable[str]) -> Context:
bid = "".join(bid_texts)
claims = frozenset(k for k, (_, claimed) in CONDITIONAL.items() if claimed.search(bid))
no_special = any(re.search(r"无特定资格要求|特定资格要求[::]?(无|/)", str(i.get("title") or "")) for i in items)
return Context(claims, no_special)
@dataclass(frozen=True, slots=True)
class Decision:
match: bool
mode: str = "explicit" # explicit / implicit
skip: str | None = None # 不找响应的原因(SKIP_LABELS 的键)
mode_status: str = "found"
evidence: bool = False # 要求投标时附证明材料:默认接受也不能免(没附就是没附)
def decide(item: dict[str, Any], modes: dict[str, Mode], ctx: Context | None = None) -> Decision:
cats = set(item.get("categories") or [])
title = str(item.get("title") or "")
text = f"{title}{item.get('requirement_text') or ''}"
if cats == {"invalidity"} and item.get("importance_basis") != "marker":
return Decision(False, skip="condition") # 「……的,投标无效」这类情形;★ / 斜体下划线标注的条款是要求,照常找
if cats == {"price"}:
return Decision(False, skip="price")
if "schedule" in cats and not cats & {"tech_param", "qualification", "scoring"}:
return Decision(False, skip="schedule")
if NONE_REQUIRED.search(title) or (ctx is not None and ctx.no_special and SPECIAL_PROOF.search(title)):
return Decision(False, skip="none_required")
if "tech_param" not in cats and UMBRELLA.search(text):
return Decision(False, skip="umbrella")
if AFTER_AWARD.search(title):
return Decision(False, skip="after_award")
if ctx is not None and ctx.claims is not None:
for key, (subject, _) in CONDITIONAL.items():
if subject.search(title) and key not in ctx.claims:
return Decision(False, skip="not_claimed")
evidence = bool(item.get("evidence_required") or EVIDENCE.search(text)) and not AWARD.search(text)
if cats <= {"scoring"} and not evidence:
return Decision(False, skip="scoring")
if "qualification" in cats and "tech_param" not in cats and NO_PROOF.search(text):
return Decision(False, skip="no_proof")
mode = modes.get("technical" if "tech_param" in cats else "commercial")
if mode is None:
return Decision(True, evidence=evidence)
return Decision(True, mode=mode.mode, mode_status=mode.status, evidence=evidence)