fix(check): 修国动真实投标文件暴露的 6 处(规则集 r5)
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled

国动真实投标文件查标一次(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>
This commit is contained in:
YANG JIANKUAN
2026-11-05 13:16:12 +08:00
parent f5edc585f1
commit 808259da87
16 changed files with 373 additions and 21 deletions

View File

@@ -5,13 +5,18 @@
- 无效情形、评审规定(只有「无效」类别、不是标注出来的条款):由规则检查(第 2 步)和人工核对覆盖;
- 日期与递交:由规则检查(落款日期、有效期)和需人工核对覆盖;
- 评分标准本身:没写要附什么材料的不找;写了的按证明材料找;
- 资格条件里写明「无需提供」或由采购人查询(信用中国)的不找。
- 资格条件里写明「无需提供」或由采购人查询(信用中国)的不找;
- 写明没有的要求(无特定资格要求)、总括性的「须具备招标文件规定的资格要求」、
中标或签订合同以后才做的事,不找;
- 条件性要求(联合体、残疾人福利性单位 / 监狱企业 / 本国产品的价格扣除):投标文件里有对应的文件、
说明投标人声称适用时才找(国动真实投标文件,check-eval §14)。
其余按类别取招标文件的响应方式:技术参数用技术类,其余用商务类。
"""
from __future__ import annotations
import re
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
@@ -22,15 +27,50 @@ 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
@@ -40,15 +80,26 @@ class Decision:
evidence: bool = False # 要求投标时附证明材料:默认接受也不能免(没附就是没附)
def decide(item: dict[str, Any], modes: dict[str, Mode]) -> Decision:
def decide(item: dict[str, Any], modes: dict[str, Mode], ctx: Context | None = None) -> Decision:
cats = set(item.get("categories") or [])
text = f"{item.get('title') or ''}{item.get('requirement_text') 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")