Files
bid-doctor/server/src/bid_doctor/check/findings.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

129 lines
6.7 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.

"""由每条清单项的检查结论生成问题(FR-M3-03、m3-plan §2.3)。
- 级别从清单项算,不让模型定(参考项目让模型定级别,模型可以把高危项降成低危);
- 说明只写事实和出处,不写整改建议(报价类问题的「建议」会写出数字,红线 6);
- 「已响应」不生成问题,结论留在 check_item_result 里。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from bid_doctor.check.match import Result
FATAL_CATEGORIES = {"qualification", "invalidity"}
REASONS = {
"evidence_missing": "没有给出投标原文",
"sid_format": "引用的句子编号写法不对",
"sid_unknown": "引用的句子不在候选范围内",
"quote_not_original": "引文和投标原文对不上",
"quote_too_short": "引文太短,无法核对",
"missing_answer": "这一条没有得到判断结果",
"model_error": "模型服务暂时不可用",
"over_budget": "超出本次检查的预算",
"response_mismatch": "响应格写的是别的内容",
}
@dataclass(frozen=True, slots=True)
class Spec:
type: str
severity: str
title: str
explanation: str
detail: dict[str, Any]
def severity(item: dict[str, Any]) -> str:
"""★ 条款、法规列明或原文写明无效的、资格条件 → 致命;▲ 参数、评分项(缺证明材料)→ 重要;其余 → 提示。"""
cats = set(item.get("categories") or [])
if item.get("importance") == "substantive" or cats & FATAL_CATEGORIES:
return "fatal"
if item.get("importance") == "important" or "scoring" in cats:
return "important"
return "hint"
def _pages(pages: list[int]) -> str:
return "、".join(f"第 {p} 页" for p in pages)
def spec(result: Result, item: dict[str, Any]) -> Spec | None:
out = _spec(result, item)
note = result.detail.get("qualification_note")
if out is None or not note:
return out
return Spec(out.type, out.severity, out.title, f"{out.explanation}{note}", out.detail)
def _spec(result: Result, item: dict[str, Any]) -> Spec | None:
if result.detail.get("superseded_by"):
return None # 资质匹配出了更具体的问题(check/qualification.py),扫描件兜底那条不再单独出
sev = severity(item)
name = str(item.get("title") or "")
base = {"importance_basis": item.get("importance_basis"), **result.detail}
scope = "、".join(result.detail.get("search_scope") or [])
if result.verdict in ("responded", "implicit_accepted"):
return None
if result.verdict == "implicit_review":
text = (
"招标文件规定未列入偏离表的视为接受,投标文件里没有找到对这条要求的明确响应。"
"这是实质性要求,请人工核对所投内容确实满足。"
)
return Spec("manual_review", "hint", f"请人工核对:{name}", text, {**base, "mode": "implicit"})
if result.verdict == "not_found" and result.evidence_status == "unverified":
claimed = result.detail.get("claimed_pages") or []
where = f"{_pages(claimed)}有响应" if claimed else "有响应"
why = REASONS.get(result.reason_code or "", result.reason_code or "")
text = f"模型认为投标文件{where},但出处没能核对({why}),按「未找到响应」处理。请人工核对。"
return Spec("unresponded", sev, f"未找到响应:{name}", text, {**base, "evidence_status": "unverified"})
if result.verdict == "not_found" and result.detail.get("mismatch"):
text = (
f"投标文件照抄了这条要求,但同一行的响应格写的是「{result.detail['mismatch'][:40]}」,"
"和这条要求对不上,像是别的条目的内容。请核对。"
)
return Spec("unresponded", sev, f"响应可能对不上:{name}", text, base)
if result.verdict == "not_found":
text = f"在投标文件({scope})中没有找到对这条要求的响应。" if scope else "没有找到对这条要求的响应。"
return Spec("unresponded", sev, f"未找到响应:{name}", text, base)
if result.verdict == "negative_deviation" and result.detail.get("deviation_unclear"):
text = (
f"这一行的偏离栏写的是「{result.detail.get('direct_read')}」,不是明确的「无偏离 / 满足」,"
"请核对是否满足这条要求。"
)
return Spec("negative_deviation", "important" if sev == "fatal" else sev, f"偏离栏需核对:{name}", text, base)
if result.verdict == "negative_deviation":
cell = result.detail.get("direct_read")
text = (
f"投标文件这一行的偏离 / 响应栏写着「{cell}」,没有找到说明。"
if cell
else "投标文件写明了偏离或不满足,没有找到说明。"
)
return Spec("negative_deviation", sev, f"负偏离未说明:{name}", text, base)
if result.verdict == "suspected_unmet":
value = result.detail.get("response_value")
text = (
f"投标文件给出的值({value})按要求值判断可能不满足。"
if value
else "投标文件给出的值按要求值判断可能不满足。"
)
return Spec("suspected_unmet", sev, f"疑似不满足:{name}", text, base)
if result.verdict == "no_value":
text = "投标文件只写了满足或响应,没有给出要求里要的具体值或材料。"
return Spec("suspected_unmet", "hint", f"未给出具体值:{name}", text, {**base, "kind": "no_value"})
if result.verdict == "scanned":
total = result.detail.get("scanned_total") or 0
where = result.detail.get("scanned_where")
# 只说扫描件在哪几页,不说「材料可能在扫描件里」:扫描件上是不是这份材料,没有 OCR 分不出来
text = (
f"投标文件文字部分没有找到这份材料。{where} 是扫描件(共 {total} 页),系统读不出上面的文字;"
"材料如果附在这些页上,请人工核对,没有附就是缺了。"
if where
else f"投标文件文字部分没有找到这份材料。投标文件里有 {total} 页扫描件,系统读不出上面的文字,"
"材料可能在扫描件页上,请人工核对。"
)
return Spec("manual_review", "hint", f"请人工核对(文字部分没找到):{name}", text, base)
why = REASONS.get(result.reason_code or "", result.reason_code or "")
return Spec("manual_review", sev, f"未判定:{name}", f"这条要求本次没有完成判断({why}),请人工核对。", base)