feat(qualification): 资质库和资质匹配(查标第 4 步,规则集 2026.11-r4)
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled

- 资质库(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>
This commit is contained in:
YANG JIANKUAN
2026-11-04 23:22:50 +08:00
parent e7fb797925
commit 08e3fe7eff
43 changed files with 2643 additions and 49 deletions

View File

@@ -0,0 +1,97 @@
"""从规矩的投标文件派生带「企业资质情况表」的投标文件(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()