feat(check): 规则检查(报价形式、形式要件、响应表),表格按单元格分行
- check/rules.py(规则集 2026.11-r1,不调模型):大小写金额、分项合计、超最高限价、 多个报价、别的项目名称、没填的地方、落款日期、投标有效期、附加条件、响应表里的未响应; 每条挂投标原文,有依据的挂招标侧出处;缺限价、截止时间、有效期要求时写进覆盖范围 - 文字层的句子带上单元格框,表格按单元格上沿分行 - 按 Fable 审读意见收紧致命级:认不准的降为重要或提示 - 评测:score.py 评规则问题;P07 注入记录改为空白处前后原文,剔除注入到人员表的 D10 - 上传页的检查范围文案同步 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,83 @@ FAMILY = {
|
||||
"D15": {"qualification", "unresponded"},
|
||||
}
|
||||
FATAL_CATS = {"substantive", "qualification"}
|
||||
RULE_CODES = {
|
||||
"P01": "PRICE.OVER_CEILING_TOTAL",
|
||||
"P02": "PRICE.OVER_CEILING_LOT",
|
||||
"P03": "PRICE.OVER_CEILING_ITEM",
|
||||
"P04": "PRICE.CAPITAL_MISMATCH",
|
||||
"P05": "PRICE.SUM_MISMATCH",
|
||||
"P06": "FORM.FOREIGN_PROJECT",
|
||||
"P07": "FORM.BLANK_FIELD",
|
||||
"P08": "FORM.DATE_AFTER_DEADLINE",
|
||||
"P09": "FORM.VALIDITY_SHORT",
|
||||
"P10": "FORM.CONDITION_ADDED",
|
||||
"P11": "FORM.MISSING_DOC",
|
||||
"P12": "PRICE.MULTIPLE_QUOTES",
|
||||
"P13": "PRICE.ITEM_MISSING",
|
||||
}
|
||||
|
||||
|
||||
def squash(t: str) -> str:
|
||||
return "".join(t.split())
|
||||
|
||||
|
||||
def score_rules(manifest: dict[str, Any], res: dict[str, Any]) -> dict[str, Any]:
|
||||
"""规则问题(P 类):规则编号对上、投标侧出处里有注入的那段原文才算查出;没对上任何注入问题的规则问题算误报。
|
||||
D10(符合性条件响应表)按表级问题记在 rule_defects 里,它由响应匹配查(negative_deviation),这里一起对照。"""
|
||||
hits = list(res.get("rules") or [])
|
||||
used: set[int] = set()
|
||||
found: Counter[str] = Counter()
|
||||
total: Counter[str] = Counter()
|
||||
missed: list[str] = []
|
||||
for rd in manifest.get("rule_defects") or []:
|
||||
kind, snip = rd["type"], squash(rd.get("t") or "")
|
||||
total[kind] += 1
|
||||
if kind == "D10":
|
||||
pool = [
|
||||
(-1, {"evidence": it["evidence"]})
|
||||
for it in res["items"]
|
||||
if it["finding"] and it["finding"]["type"] == "negative_deviation"
|
||||
] + [(i, h) for i, h in enumerate(hits) if h["code"] == "FORM.RESPONSE_NEGATIVE"]
|
||||
else:
|
||||
pool = [(i, h) for i, h in enumerate(hits) if h["code"] == RULE_CODES.get(kind)]
|
||||
key = snip[:12]
|
||||
cell = squash(rd.get("variant") or "") if kind == "D10" else ""
|
||||
ok = next(
|
||||
(
|
||||
i
|
||||
for i, h in pool
|
||||
if any(
|
||||
(key and (key in squash(e["text"]) or squash(e["text"]) in snip))
|
||||
or (cell and squash(e["text"]) == cell)
|
||||
for e in h["evidence"]
|
||||
)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if ok is None:
|
||||
missed.append(f"{rd['id']}({rd.get('variant', '')}):{rd.get('t', '')[:40]}")
|
||||
continue
|
||||
found[kind] += 1
|
||||
if ok >= 0:
|
||||
used.add(ok)
|
||||
# 响应表规则报的「负偏离」对上了响应问题(D05 / D06)的注入位置:算那个单元查出(rule_units),不算规则误报
|
||||
rule_units: set[str] = set()
|
||||
for i, h in enumerate(hits):
|
||||
if h["code"] != "FORM.RESPONSE_NEGATIVE":
|
||||
continue
|
||||
texts = [squash(e["text"]) for e in h["evidence"]]
|
||||
for uid, u in manifest["units"].items():
|
||||
if u.get("expect") != "defect" or "negative_deviation" not in FAMILY.get(u.get("defect", ""), set()):
|
||||
continue
|
||||
if any(
|
||||
(w := squash(x.get("t") or "")[:12]) and any(w in t or (t and t in w) for t in texts)
|
||||
for x in u.get("where") or []
|
||||
):
|
||||
rule_units.add(uid)
|
||||
used.add(i)
|
||||
fp = [h for i, h in enumerate(hits) if i not in used]
|
||||
return {"found": found, "total": total, "missed": missed, "fp": fp, "n": len(hits), "units": rule_units}
|
||||
|
||||
|
||||
def pct(a: int, b: int) -> str:
|
||||
@@ -79,6 +156,7 @@ def main() -> None:
|
||||
parent[find(o)] = find(first)
|
||||
group = {u.id: find(u.id) for u in units}
|
||||
|
||||
rs = score_rules(manifest, res)
|
||||
per_unit: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
unmapped: list[dict[str, Any]] = []
|
||||
for it in res["items"]:
|
||||
@@ -120,6 +198,9 @@ def main() -> None:
|
||||
continue
|
||||
rec_total["查标"] += 1
|
||||
good = [it for it in found if it["finding"]["type"] in FAMILY.get(e["defect"], set())]
|
||||
if not good and any(x in rs["units"] for x in manifest["units"] if group.get(x, x) == g):
|
||||
good = [{"finding": {"type": "negative_deviation", "severity": "important"}}]
|
||||
rec["其中由响应表规则查出"] += 1
|
||||
if good:
|
||||
for k in [*keys, "端到端", "查标"]:
|
||||
rec[k] += 1
|
||||
@@ -182,7 +263,23 @@ def main() -> None:
|
||||
*[f"- {x}" for x in fp[:80]],
|
||||
*(["- ……"] if len(fp) > 80 else []),
|
||||
"",
|
||||
f"## 规则问题(第 2 步才查,这里只列出):{len(manifest['rule_defects'])} 个",
|
||||
]
|
||||
lines += [
|
||||
f"## 规则问题:注入 {sum(rs['total'].values())} 个,查出 {sum(rs['found'].values())} 个;规则报出 {rs['n']} 个,误报 {len(rs['fp'])} 个",
|
||||
"",
|
||||
"| 问题 | 查出 |",
|
||||
"|---|---|",
|
||||
*[f"| {k} | {pct(rs['found'][k], rs['total'][k])} |" for k in sorted(rs["total"])],
|
||||
"",
|
||||
*[f"- 漏:{x}" for x in rs["missed"]],
|
||||
*[
|
||||
f"- 误报:{h['code']} {h['title']}:{h['evidence'][0]['text'][:50] if h['evidence'] else ''}"
|
||||
for h in rs["fp"]
|
||||
],
|
||||
"",
|
||||
"### 规则检查没覆盖的",
|
||||
"",
|
||||
*[f"- {x}" for x in (res.get("rules_coverage") or {}).get("not_checked", [])],
|
||||
"",
|
||||
]
|
||||
out = d / "runs" / a.run
|
||||
@@ -196,6 +293,13 @@ def main() -> None:
|
||||
"other_findings": other_findings,
|
||||
"extraction_miss": len(extraction_miss),
|
||||
"include_unreviewed": a.include_unreviewed,
|
||||
"rules": {
|
||||
"found": dict(rs["found"]),
|
||||
"total": dict(rs["total"]),
|
||||
"fp": len(rs["fp"]),
|
||||
"fp_by_code": dict(Counter(h["code"] for h in rs["fp"])),
|
||||
"n": rs["n"],
|
||||
},
|
||||
}
|
||||
(out / "score.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1))
|
||||
print("\n".join(lines[:20]))
|
||||
|
||||
Reference in New Issue
Block a user