- 资质库(FR-M6-08):增删改、证书扫描件(PDF/JPG/PNG,企业级密钥加密、签名下载)、 late_for 按进行中项目的投标截止日提醒、审计;前端资质库页和对话框,侧栏入口 - 资质匹配(FR-M3-05):证书类别词表(16 类,产品证书、人员证书不进匹配), QUAL.EXPIRED / LEVEL_LOW / NOT_COVERED;投标文字层按清单项定级,只有资质库记录的降一级, 出处显示「依据:资质库记录」(查标页、报告模板 v2);一类证书一个问题;评分项不逐条找响应的也查 - 响应错位规则不再把资质表的证书名称格当成照抄的要求 - 评测:qual_defects.py 派生资质情况表,D12 3/3、D18 2/2、规矩的资质表 0 误报;开发集不退化 - 决策 QL1–QL7;方案和落地记录 docs/architecture/qualification-plan.md,评测 check-eval.md §13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
6.2 KiB
Python
124 lines
6.2 KiB
Python
"""由每条清单项的检查结论生成问题(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
|
||
text = (
|
||
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)
|