国动真实投标文件查标一次(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>
112 lines
6.8 KiB
Python
112 lines
6.8 KiB
Python
"""每条清单项要不要在投标文件里找响应、按什么响应方式判(决策 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)
|