Files
bid-doctor/evals/check/qual_defects.py
YANG JIANKUAN 08e3fe7eff
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(qualification): 资质库和资质匹配(查标第 4 步,规则集 2026.11-r4)
- 资质库(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>
2026-11-04 23:22:50 +08:00

98 lines
5.3 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.

"""从规矩的投标文件派生带「企业资质情况表」的投标文件(qualification-plan §5):q00 证书都有效、等级够;
q01 注入 D12(有效期早于投标截止日)和 D18(等级低于要求)。
cd server && ../.venv/bin/python ../evals/check/qual_defects.py <规矩的投标文件> <输出> <q00|q01>
然后 ../.venv/bin/python ../evals/check/render.py <输出>
- 资质情况表是文字表格(证书原件在后面的扫描件页上,和规矩的那份一样),插在资格证明材料那一章的开头;
- 证书名称、编号、日期都是虚构的,写法故意和词表的别名不完全一样(国标号在前、括注英文缩写、日期几种写法);
- 响应清单照抄规矩的那份(单元都是 expect=ok),rule_defects 记注入的问题;不挂标注单元,不需要复核(E1 只管单元);
- 局限:表是对着 check/qualification.py 的词表写的,分数说明不了泛化(qualification-plan §5)。
"""
from __future__ import annotations
import argparse
import json
import pathlib
from typing import Any
ROOT = pathlib.Path(__file__).resolve().parent
OUT = ROOT.parent / "out" / "check"
HEADER = ["序号", "证书名称", "证书编号", "等级", "发证机构", "有效期"]
# 招标文件 → 资质情况表的行(名称、编号、等级、发证机构、有效期),全部虚构
CERTS: dict[str, list[list[str]]] = {
"S01": [
["建筑业企业资质证书(电子与智能化工程专业承包)", "D232000001", "贰级", "某省住房和城乡建设厅(虚构)", "2024年5月21日至2029年5月20日"],
["安全生产许可证", "(苏)JZ安许证字〔2023〕000016", "—", "某省住房和城乡建设厅(虚构)", "2023年11月16日至2027年11月15日"],
["信息系统业务安全服务资质证书(CCIA)", "CCIA-2025-000231", "一级", "某网络安全行业组织(虚构)", "2025-02-01至2028-01-31"],
["信息安全服务资质认证证书(安全集成)", "CCRC-2024-ISV-000188", "一级", "某认证中心(虚构)", "有效期至2027.06.30"],
],
"S04": [
["GB/T 19001-2016/ISO 9001:2015 质量管理体系认证证书", "00125Q30001R1M", "—", "某认证有限公司(虚构)", "2025年3月10日至2028年3月9日"],
["环境管理体系认证证书(ISO 14001)", "00125E30002R1M", "—", "某认证有限公司(虚构)", "2025年3月10日至2028年3月9日"],
["职业健康安全管理体系认证证书 GB/T 45001-2020", "00125S30003R1M", "—", "某认证有限公司(虚构)", "2025年3月10日至2028年3月9日"],
],
"S07": [
["信息技术服务标准(ITSS)运行维护符合性证书", "ITSS-YW-2-320020250001", "二级", "某行业协会(虚构)", "2025年6月1日至2028年5月31日"],
["信息系统建设和服务能力等级证书", "CS3-3201-2024000077", "CS3", "某行业联合会(虚构)", "2024-12-31至2028-12-30"],
],
}
# 招标文件 → 注入的问题:(行号, 列号, 新值, 问题编号, 写法)
DEFECTS: dict[str, list[tuple[int, int, str, str, str]]] = {
"S01": [
(1, 5, "2023年11月16日至2026年11月15日", "D12", "资格条件的证书早于截止日到期"),
(2, 3, "二级", "D18", "评分项要求一级,写的是二级"),
],
"S04": [(1, 5, "2023年3月10日至2026年3月9日", "D12", "评分项的证书已过期")],
"S07": [
(0, 5, "2023年6月1日至2026年5月31日", "D12", "评分项的证书已过期"),
(1, 3, "CS2", "D18", "评分项要求 CS3 及以上,写的是 CS2"),
],
}
def table(rows: list[list[str]], vol: str) -> list[dict[str, Any]]:
return [
{"type": "para", "text": "企业资质情况表(证书复印件附后)", "vol": vol},
{
"type": "table",
"header": HEADER,
"rows": [{"cells": [str(i + 1), *r]} for i, r in enumerate(rows)],
"vol": vol,
},
]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("clean")
ap.add_argument("out")
ap.add_argument("kind", choices=["q00", "q01"])
a = ap.parse_args()
src = OUT / a.clean
spec = json.loads((src / "spec.json").read_text())
manifest = json.loads((src / "manifest.json").read_text())
tender = spec["tender"]
rows = [list(r) for r in CERTS[tender]]
defects = []
if a.kind == "q01":
for n, (row, col, value, code, variant) in enumerate(DEFECTS[tender], start=1):
rows[row][col - 1] = value
defects.append({"id": f"{code}-{n}", "type": code, "variant": variant, "t": value})
vol = spec["volumes"][0]
at = next(i for i, b in enumerate(vol["blocks"]) if b.get("type") == "heading" and b.get("form") == "evidence")
vol["blocks"][at + 1 : at + 1] = table(rows, vol["blocks"][at].get("vol") or "v1")
out = OUT / a.out
out.mkdir(parents=True, exist_ok=True)
spec["bid"] = a.out
manifest.update(bid=a.out, kind="clean" if a.kind == "q00" else "sloppy", density="qualification", rule_defects=defects)
(out / "spec.json").write_text(json.dumps(spec, ensure_ascii=False, indent=1))
(out / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=1))
print(f"{a.out}:{a.kind},资质情况表 {len(rows)} 行,注入 {[d['id'] for d in defects]}")
if __name__ == "__main__":
main()