From db1b1f6f9fea6e96ed7b156b49963fab92c583cc Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Wed, 4 Nov 2026 16:44:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(check):=20=E8=A7=84=E5=88=99=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=EF=BC=88=E6=8A=A5=E4=BB=B7=E5=BD=A2=E5=BC=8F=E3=80=81?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=E8=A6=81=E4=BB=B6=E3=80=81=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E8=A1=A8=EF=BC=89=EF=BC=8C=E8=A1=A8=E6=A0=BC=E6=8C=89=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=A0=BC=E5=88=86=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check/rules.py(规则集 2026.11-r1,不调模型):大小写金额、分项合计、超最高限价、 多个报价、别的项目名称、没填的地方、落款日期、投标有效期、附加条件、响应表里的未响应; 每条挂投标原文,有依据的挂招标侧出处;缺限价、截止时间、有效期要求时写进覆盖范围 - 文字层的句子带上单元格框,表格按单元格上沿分行 - 按 Fable 审读意见收紧致命级:认不准的降为重要或提示 - 评测:score.py 评规则问题;P07 注入记录改为空白处前后原文,剔除注入到人员表的 D10 - 上传页的检查范围文案同步 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- apps/web/src/pages/BidCheckPage.vue | 4 +- docs/architecture/check-eval.md | 37 +- docs/architecture/m3-plan.md | 4 +- evals/check/mutate.py | 13 +- evals/check/report.py | 20 + evals/check/run_check.py | 101 +- evals/check/score.py | 106 +- server/src/bid_doctor/check/match.py | 1 + server/src/bid_doctor/check/rules.py | 1083 +++++++++++++++++ server/src/bid_doctor/check/runner.py | 115 +- server/src/bid_doctor/check/service.py | 9 +- .../document/pipeline/text_layer.py | 9 +- server/tests/fixtures/bid-mini-forms.pdf | Bin 0 -> 305742 bytes server/tests/fixtures/make_bid_fixtures.py | 15 +- server/tests/test_check_flow.py | 33 + server/tests/test_check_rules.py | 259 ++++ 17 files changed, 1792 insertions(+), 20 deletions(-) create mode 100644 server/src/bid_doctor/check/rules.py create mode 100644 server/tests/fixtures/bid-mini-forms.pdf create mode 100644 server/tests/test_check_rules.py diff --git a/AGENTS.md b/AGENTS.md index 30e1661..0b8bc4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -213,4 +213,5 @@ samples/ - 2026-11-04:查标第一轮改进:候选句带同一行的单元格、提示词 check.match v2。开发集 17 份上召回 30% → 50%,带标志词的问题 2% → 54%,误报略降,输入 token +44%;漏响应和响应对象错位变差(`docs/architecture/check-eval.md` §3)。评测集修了待修项、加了 3 份低密度投标文件,第三批复核 55 条、用户抽查 11 条全部同意。 - 2026-11-04:查标第二轮:评测集纠错(漏响应、错位的「变差」多是生成器只改了一处);新增响应方式识别(C12,规则优先)和「这条要不要投标人回答」的判断。开发集 17 份上规矩投标文件报出的问题 639 → 229,误报 21.3 → 12.4 / 100,召回 68% → 65%,致命级 20 → 21 / 32;证明材料缺失几乎查不出(扫描件)。第四批复核 45 条,用户抽查 9 条全部同意(四批共 65 条)。见 `docs/architecture/check-eval.md` §4。 - 2026-11-04:查标第三轮:文件要在(按标题找文件名,不调模型)、扫描件兜底(C10,没找到的证明材料改为提示级「可能在扫描件里,请人工核对」)、响应方式存进清单并在清单页显示、可改(C12 的人工确认点,迁移 0007、0008)。开发集上规矩投标文件的致命问题 120 → 51,致命级召回不变(22 / 32);证明材料缺失仍查不出,要靠 OCR / 多模态(S5)。见 `docs/architecture/check-eval.md` §5。 -- 下一步:查标第 2 步的投标侧结构识别和规则检查(报价形式、形式要件)。 +- 2026-11-04:查标第四轮:规则检查(`check/rules.py`,规则集 2026.11-r1,不调模型)——大小写金额、分项合计、超限价、多个报价、别的项目名称、没填的地方、落款日期、投标有效期、附加条件、响应表里的未响应;文字层的句子带上单元格框,表格按单元格分行。请 Fable 从真实投标文件角度审读,按「认不准的不给致命」改了定级和取值范围。开发集 17 份上规则问题 57 / 57 查出、规矩投标文件 0 误报,但规则是看着生成器和开发词库写的,这个分数说明不了泛化,要等留出集和国动真实投标文件。见 `docs/architecture/check-eval.md` §6。 +- 下一步:查标第 2 步剩下的(需人工核对的条目和 `manual-checks` 接口、「报价」类清单项和分项上限 C7),然后第 3 步的 D09 / D06 提示词 v3。 diff --git a/apps/web/src/pages/BidCheckPage.vue b/apps/web/src/pages/BidCheckPage.vue index 3972220..452ffeb 100644 --- a/apps/web/src/pages/BidCheckPage.vue +++ b/apps/web/src/pages/BidCheckPage.vue @@ -194,12 +194,14 @@ async function start() {

这一版还不查

    -
  • 报价形式(三层限价、合计、大小写)、形式要件、资质有效期:规则检查还没有上线
  • +
  • 分项 / 单价上限、分项和采购需求逐项对照、格式文件固定文字被改、声明函、资质有效期:还没有上线
  • 签章真伪(以后也只看有无,不鉴定真伪)
  • 份数、密封、CA 加密、递交方式(文件里看不出来)
  • 报价是否合理、该报多少,评分高低
  • diff --git a/docs/architecture/check-eval.md b/docs/architecture/check-eval.md index 902e880..6e92deb 100644 --- a/docs/architecture/check-eval.md +++ b/docs/architecture/check-eval.md @@ -144,7 +144,40 @@ **读法**:问题的个数没变,变的是级别:规矩的投标文件上的致命问题少了一半多(120 → 51),多出来的是「请到扫描件里核对」的提示,致命级召回不变。总召回掉了 4 个点,主要是批次变化带来的波动(文件要在的条目不再进模型,同一批里的组合变了),以及几条漏响应被「标题里出现了文件名」误判为已响应——要盯着。评测集里证明材料全是扫描件页,所以证明材料缺失(D02)照旧查不出、扫描件兜底也分不出缺没缺;真正查出来要靠 OCR / 多模态(S5)。 -## 6. 下一步 +## 6. 第四轮:规则检查(报价形式、形式要件)(2026-11-04) -1. 第 2 步(投标侧结构识别、规则、C12 响应方式抽取)和第 3 步(整行上下文、偏离表直取、presence 判断)按 §2.2 的顺序改,每改一轮在开发集 14 份上重跑对比。 +**改了什么**(`check/rules.py`,规则集 `2026.11-r1`,不调模型): +- **投标侧结构**:按坐标把句子分成行、按表头归栏,认三种表——开标一览表(「投标总报价」那一栏)、分项报价表(数量 / 单价 / 合价三栏,可以跨页接续)、响应表(有「响应情况 / 偏离情况」一栏);再加落款日期、格式文件的填写位、页眉。 +- **规则**:大写和小写金额(P04)、分项算术和合计(P05)、超最高限价(P01 / P02,按所投采购包取限价)、多个报价(P12)、页眉和填写位里的别的项目名称 / 编号 / 采购人(P06)、没填的地方(P07)、落款日期晚于截止或写错(P08)、投标有效期短于要求(P09)、附加条件(P10)、响应表里写「未完全响应 / 负偏离」或空着(D10)。响应匹配已经对同一句报了负偏离的,规则不重复报。 +- 每条问题挂投标原文;有依据的挂招标侧出处(限价、截止时间、项目名称来自项目基本信息,有效期要求来自清单项)。缺项目侧的值(限价、截止时间、有效期要求)或认不出开标一览表时不报,写进覆盖范围。 +- 说明只写读到的数字和依据,不写「应改为多少」(红线 6)。 + +**顾问审读后的修改**(请 Fable 从真实政府采购投标文件的角度审读,只给代码不给结论)。它指出的主要风险是真实版式上的致命误报,按「宁可让人核对,不让规矩的投标文件挂致命」改了: +- **表格分行改按单元格**:文字层原来就用 pdfplumber 找出了单元格框,但没存到句子上;现在句子带上单元格框(可选字段,旧的文字层没有就按句子重叠分行),同一行按单元格上沿分。垂直居中的短格、折成几句的长格、纵向合并的格不再串行或错位。 +- **开标一览表认得更严**:总价那一栏下面恰好一个金额才算总价,分项表、多年度报价表的金额栏叫「投标报价」也不会被当成「多个总价」;不同位置写的总价不一致改为重要级的「投标总价前后写的不一致」,不再按致命的「两个报价」报。 +- **另报价按把握分级**:「备选方案 / 方案二 / 第二套」致命;「优惠价 / 让利后」重要;金额等于投标总价(开标一览表或分项合计)的不报;否定看整个分句。 +- **分项表**:「硬件小计」这类分组小计不进明细;同一栏读到两个数的行(无框线表格的合并格)不核对;下一页重复表头接着算同一张表;说明写「相差多少元」。 +- **响应表**:一条记录从序号行到下一个序号行;「空着」一律按重要报(可能是版面没认准);认「否」(「是否响应 / 满足」栏)、「不响应(见说明)」这类写法。 +- **投标有效期**:只在投标函、开标一览表那一页上的按致命报,别处按提示;复述的招标原文不算;「本报价有效期」(原厂报价单)不算;要求有几条时取最少的天数;认表格写法(「投标有效期」一格、旁边或下面一格写天数)。 +- **别的项目名称**:页眉要像项目名称(以「项目 / 工程 / 采购 / 标段」结尾或带编号),章节页眉不算;招标文件里出现过的名称、编号(代理机构编号、简称)不算;「致:贵单位」不算。 +- **附加条件**:复述判断先去掉编号、用前后 20 字;「不影响合同价款」这类不算;技术分册里的按提示报。 +- **大写金额**:前后两句都找、只认写着「小写 / ¥ / 报价 / 合计」的句子,任何一处相等就不报。 +- 落款日期认汉字数字(「二〇二六年十一月十七日」)和「日 期:」;说明里去掉「……的投标无效」这类绝对说法,改为「通常会导致投标无效,请核对」。复查对齐的目标改为原文前缀 + 摘要,前缀相同的两句不会互相吞掉。 +- 每类都加了开发集以外的反例单元测试(`test_check_rules.py`)。 + +**评测集修正**:P07 的注入记录改成空白处前后的原文(原来记段落开头,空白在同段后一句时对不上);S07-A-sloppy 的 D10 注入在人员表里(不是响应表),剔除,生成器改为只往表头有「响应」的表里注入。score.py 加了规则问题的评分:规则编号对上、投标侧出处里有注入的原文才算查出。 + +| 指标(17 份) | 结果 | +|---|---| +| 规则问题查出 | 57 / 57(P04 8、P05 7、P06 7、P07 10、P08 7、P09 1、P10 7、P12 7、D10 3) | +| 规则问题误报 | 0(规矩的投标文件 7 份上 0) | +| 规则问题的级别 | 致命 8(备选方案类另报价 6、投标函上的有效期 1、符合性响应表 1)、重要 32、提示 17 | +| 响应匹配 | 不变(召回 92 / 150,致命级 22 / 32) | + +**读法:这个满分基本说明不了泛化。** 规则是在知道生成器怎么注入、见过开发词库措辞的情况下写的,投标文件版式也都出自同一个生成器。附加条件(P10)和多个报价(P12)的开发词库措辞直接落在规则的写法里,最需要留出集检验。为此做了两件事:规则按招投标文件的通用写法归纳(单元测试和 `bid-mini-forms.pdf` 都用开发词库以外的措辞);评测集里没有的情形(超限价、跨页分项表、万元单位)只有单元测试。真实投标文件的合并单元格、扫描页、各地模板差异,要等国动真实投标文件来看。P01 / P02 / P03 / P11 / P13–P18 评测集里还没有注入。 + +## 7. 下一步 + +1. 第 2 步剩下的:需人工核对的条目(份数、密封、递交方式)和 `manual-checks` 接口;「报价」类清单项和分项 / 单价上限(C7、P03);评测集补注入 P01、P02、P13。 +2. 第 3 步:D09(响应对象错位)、D06(负偏离未说明)的提示词 v3,token 预算、复查复用、重复条目分组。 3. 留出集(S03、S06、S09,风格 B + C,封存的词库)和真实国动投标文件,在第 3 步冻结代码后只跑一次。 diff --git a/docs/architecture/m3-plan.md b/docs/architecture/m3-plan.md index 1ceb841..ed159eb 100644 --- a/docs/architecture/m3-plan.md +++ b/docs/architecture/m3-plan.md @@ -10,7 +10,9 @@ |---|---|---| | 第 0 步 评测集 | 试跑完成,继续扩充 | 规范 v0.2、留出集划分已定,留出集词库和风格 C 已封存;生成、注入、渲染、离线查标、评分工具已写;开发集 7 份招标文件、14 份投标文件第一版跑完,结果和发现见 `check-eval.md` §2 | | 第 1 步 链路打通 | ✅ 2026-11-04 | 见下方「第 1 步落地记录」 | -| 第 2~6 步 | 未开始 | | +| 第 2 步 规则 | 进行中 | 响应方式(C12)、文件要在、扫描件兜底、规则检查 `rules.py`(报价形式、形式要件、响应表)已上;需人工核对的条目、「报价」类清单项和分项上限(C7)还没做。见 `check-eval.md` §4–§6 | +| 第 3 步 AI 响应匹配 | 进行中 | 整行上下文、提示词 v2;D09 / D06、token 预算、复查复用还没做 | +| 第 4~6 步 | 未开始 | | **第 1 步落地记录**(和本文设计不一致、或留到后面的,记在这里): diff --git a/evals/check/mutate.py b/evals/check/mutate.py index 2c12211..c379aca 100644 --- a/evals/check/mutate.py +++ b/evals/check/mutate.py @@ -334,6 +334,8 @@ class Mutator: def compliance(self) -> None: for t in self.tables(fixed=True): + if not any("响应" in str(h) for h in t.get("header") or []): + continue # 只有响应表(表头有「响应情况」栏)才注入;人员表、业绩表不是 rows = [r for r in t["rows"] if self.free(r.get("responds") or [])] if not rows: continue @@ -388,7 +390,7 @@ class Mutator: b = self.rng.choice(paras) blank = self.phrase("P07", "blanks") b["text"] = b["text"].replace(bidder, blank, 1) - self.rule("P07", "投标人名称空", b["text"][:24]) + self.rule("P07", "投标人名称空", blank_context(b["text"], blank)) def p08(self) -> None: signs = [b for b in self.blocks if b.get("type") == "sign"] @@ -423,6 +425,13 @@ class Mutator: self.rule("P12", text, text) +def blank_context(text: str, blank: str) -> str: + """空白处前后的原文(不跨句号),评分时拿它对照问题的投标侧出处。""" + i = text.index(blank) + start = max(i - 10, max(text.rfind(x, 0, i) for x in "。;") + 1) + return text[start : i + len(blank) + 10] + + def table_mode(b: dict[str, Any]) -> str: """表格的响应方式:档案里表单写了 mode 的以它为准(如 S02 评分要的技术规格表是 presence,不是逐条响应的机制)。""" return str(b.get("mode") or ("implicit" if b.get("only_deviations") else "explicit")) @@ -473,7 +482,7 @@ def main() -> None: for f in OUT.glob(f"{tender}-*/manifest.json"): for uid, e in json.loads(f.read_text())["units"].items(): if (e.get("review") or {}).get("ok") is False: - m.rejected.add(uid) # noqa: S311 评测数据,不用于安全 + m.rejected.add(uid) if not a.manifest_only: if a.density == "high": m.d01_block() diff --git a/evals/check/report.py b/evals/check/report.py index fa635a9..95b643e 100644 --- a/evals/check/report.py +++ b/evals/check/report.py @@ -30,6 +30,10 @@ def main() -> None: traps: Counter[str] = Counter() tokens = [0, 0] findings_clean = items_clean = 0 + r_found: Counter[str] = Counter() + r_total: Counter[str] = Counter() + r_fp: Counter[str] = Counter() + r_fp_clean = r_clean = 0 for bid in a.bids: d = OUT / bid m = json.loads((d / "manifest.json").read_text()) @@ -43,6 +47,13 @@ def main() -> None: if "查标" in s["recall"]: by_style[m["style"]] += s["recall"]["查标"][0] by_style_tot[m["style"]] += s["recall"]["查标"][1] + rules = s.get("rules") or {} + r_found.update(rules.get("found") or {}) + r_total.update(rules.get("total") or {}) + r_fp.update(rules.get("fp_by_code") or {}) + if m["kind"] == "clean": + r_fp_clean += rules.get("fp", 0) + r_clean += 1 if m["kind"] == "clean": fp += s["fp"] ok += s["ok_units"] @@ -68,6 +79,15 @@ def main() -> None: f"- 规矩的投标文件里,不需要响应(na / unknown)的要求上报了问题 {other} 个,对不上标注单元的问题 {unmapped} 个", f"- 干扰项误报:{dict(traps)}", f"- token:输入 {tokens[0]:,},输出 {tokens[1]:,}({len(a.bids)} 次检查)", + "", + f"## 规则问题:查出 {pct(sum(r_found.values()), sum(r_total.values()))};误报 {sum(r_fp.values())} 个" + f"(其中规矩的投标文件 {r_clean} 份上 {r_fp_clean} 个)", + "", + "| 问题 | 查出 |", + "|---|---|", + *[f"| {k} | {pct(r_found[k], r_total[k])} |" for k in sorted(r_total)], + "", + f"- 误报按规则:{dict(r_fp)}", ] print("\n".join(lines)) diff --git a/evals/check/run_check.py b/evals/check/run_check.py index d8029d9..7a609e6 100644 --- a/evals/check/run_check.py +++ b/evals/check/run_check.py @@ -11,11 +11,13 @@ from __future__ import annotations import argparse import asyncio +import datetime as dt import hashlib import json import pathlib import re import sys +from decimal import Decimal from typing import Any from uuid import UUID, uuid4 @@ -23,8 +25,9 @@ from bid_doctor.ai import prompts from bid_doctor.ai import service as ai_service from bid_doctor.ai.providers.base import near_limit from bid_doctor.ai.service import OutputIncomplete, provider -from bid_doctor.check import applicability, findings, match +from bid_doctor.check import applicability, findings, match, rules from bid_doctor.checklist import response_mode +from bid_doctor.checklist.extract import amount_yuan from bid_doctor.core.config import get_settings from bid_doctor.core.db import TenantContext from bid_doctor.document.pipeline import text_layer @@ -102,6 +105,9 @@ def checklist(tender: str, run: str) -> dict[UUID, dict[str, Any]]: text = "".join(by_sid[k].text for k in r.get("sentences") or [] if k in by_sid) raw = (r.get("raw") or {}).get("tech_param") or {} spec = {"tech_param": {k: raw[k] for k in ("param", "op", "value", "unit") if raw.get(k)}} if raw else {} + sched = (r.get("raw") or {}).get("schedule") or {} + if sched: + spec["schedule"] = {k: sched[k] for k in ("date_kind", "at") if sched.get(k)} out[UUID(int=i + 1)] = { "title": r.get("title"), "requirement_text": text or None, @@ -114,6 +120,53 @@ def checklist(tender: str, run: str) -> dict[UUID, dict[str, Any]]: return out +def facts(tender: str, run: str, items: dict[UUID, dict[str, Any]]) -> rules.Facts: + """项目侧的值:读标评测抽出的项目基本信息(和产品里一样来自抽取);所投的包按招标档案(相当于用户选包)。""" + base = EVALS / "out" / "runs" / run / tender + info = json.loads((base / "info.json").read_text()) + f = {k: v.get("value") for k, v in info["fields"].items()} + profile = json.loads((OUT / "tenders" / f"{tender}.json").read_text())["project"] + deadline = next( + ( + dt.date.fromisoformat(s["at"][:10]) + for r in json.loads((base / "items.json").read_text()) + if r.get("zone") == "formal" + and (s := (r.get("raw") or {}).get("schedule") or {}).get("date_kind") == "bid_deadline" + and s.get("at") + ), + None, + ) + lots = info.get("lots") or [] + ceiling, scope, note = None, "total", None + if lots: + lot = next( + (x for x in lots if str(x["lot_no"]).lstrip("0") == str(profile.get("bidding_lot", "")).lstrip("0")), None + ) + if lot is None and len(lots) == 1: + lot = lots[0] + if lot is not None: + c = amount_yuan((lot.get("ceiling") or {}).get("value"), (lot.get("ceiling") or {}).get("unit")) + ceiling, scope = (Decimal(c) if c else None), "lot" if len(lots) > 1 else "total" + else: + note = "所投采购包不明确,报价没有和限价比对" + else: + c = amount_yuan(f.get("ceiling_total"), info["fields"].get("ceiling_total", {}).get("unit")) + ceiling = Decimal(c) if c else None + days, item = rules.required_validity(items) + return rules.Facts( + name=f.get("name"), + code=f.get("code"), + purchaser=f.get("purchaser"), + agency=f.get("agency"), + deadline=deadline, + ceiling=ceiling, + ceiling_scope=scope, + ceiling_note=note, + validity_days=days, + validity_item=item, + ) + + async def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("bid") @@ -122,6 +175,7 @@ async def main() -> None: ap.add_argument("--baseline", action="store_true", help="只检索、不调模型的基线") ap.add_argument("--v1", action="store_true", help="第 1 步的做法:提示词 v1、不带同一行的单元格(对比基线)") ap.add_argument("--no-modes", action="store_true", help="不按响应方式和「要不要回答」筛选(C12 之前的做法)") + ap.add_argument("--rules-only", action="store_true", help="只重跑规则检查,写回已有运行的 results.json") args = ap.parse_args() d = OUT / args.bid manifest = json.loads((d / "manifest.json").read_text()) @@ -136,8 +190,39 @@ async def main() -> None: pdf.stem, text_layer.body_sentences(pages), [p.page_no for p in pages if p.text_source == "empty"], + [s for p in pages for s in p.sentences if s.location == "header_footer"], ) ) + hits, rules_cov = rules.run( + facts(manifest["tender"], args.checklist, items), docs, gold.sentences(manifest["tender"]) + ) + + def rule_rows(covered: set[tuple[str, int, str]]) -> list[dict[str, Any]]: + return [ + { + "code": h.code, + "type": h.type, + "severity": h.severity, + "title": h.title, + "explanation": h.explanation, + "evidence": [{"volume": doc.label, "p": s.page_no, "text": s.text} for doc, s in h.evidence], + } + for h in rules.drop_covered(hits, covered) + ] + + if args.rules_only: + prev = d / "runs" / args.name / "results.json" + data = json.loads(prev.read_text()) + covered = { + (e["volume"], e["p"], "".join(e["text"].split())) + for it in data["items"] + if it["verdict"] == "negative_deviation" + for e in it["evidence"] + } + data["rules"], data["rules_coverage"] = rule_rows(covered), rules_cov + prev.write_text(json.dumps(data, ensure_ascii=False, indent=1)) + print(f"{args.bid}:规则问题 {len(data['rules'])} 个") + return ai_service.run_json = baseline_run_json if args.baseline else cached_run_json # type: ignore[assignment] if args.v1: match.PROMPT = ("check.match", 1) @@ -196,7 +281,19 @@ async def main() -> None: out = d / "runs" / args.name out.mkdir(parents=True, exist_ok=True) summary = {"items": len(rows), "usage": usage, "verify_failures": stats.verify_failures, "splits": stats.splits} - (out / "results.json").write_text(json.dumps({"summary": summary, "items": rows}, ensure_ascii=False, indent=1)) + covered = { + (doc.label, s.page_no, "".join(s.text.split())) + for res in results.values() + if res.verdict == "negative_deviation" + for doc, s in res.evidence + } + (out / "results.json").write_text( + json.dumps( + {"summary": summary, "items": rows, "rules": rule_rows(covered), "rules_coverage": rules_cov}, + ensure_ascii=False, + indent=1, + ) + ) findings_n = sum(r["finding"] is not None for r in rows) print( f"{args.bid}:清单 {len(rows)} 条,问题 {findings_n} 个,模型调用 {usage['calls']} 次(缓存 {usage['cached']})," diff --git a/evals/check/score.py b/evals/check/score.py index 06e3710..c0ced67 100644 --- a/evals/check/score.py +++ b/evals/check/score.py @@ -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])) diff --git a/server/src/bid_doctor/check/match.py b/server/src/bid_doctor/check/match.py index 3e0f5f2..aae7b9a 100644 --- a/server/src/bid_doctor/check/match.py +++ b/server/src/bid_doctor/check/match.py @@ -53,6 +53,7 @@ class BidDoc: label: str sentences: list[Sentence] scanned_pages: list[int] = field(default_factory=list[int]) # 没有文字层的页(扫描件),C10 + header_footer: list[Sentence] = field(default_factory=list[Sentence]) # 页眉页脚(规则检查看页眉里的项目名称) @dataclass(slots=True) diff --git a/server/src/bid_doctor/check/rules.py b/server/src/bid_doctor/check/rules.py new file mode 100644 index 0000000..8cd6bf3 --- /dev/null +++ b/server/src/bid_doctor/check/rules.py @@ -0,0 +1,1083 @@ +"""规则检查:报价形式和形式要件(m3-plan §2.2,评测规范 GUIDELINES §3.2 的 P 类问题),不调模型。 + +- 投标侧结构只认最常见的几种:开标一览表(总报价那一栏)、分项报价表(数量 / 单价 / 总价三栏,可以跨页)、 + 响应表(有「响应情况 / 偏离情况」一栏)、落款日期、格式文件里的填写位、页眉; +- 表格分行:有框线的表格按单元格的上沿(文字层带的单元格框),其余按句子上下重叠; +- 每条结论都挂投标原文的句子(红线 2);说明只写读到的数字和依据,不写「应改为多少」(红线 6); +- 致命级只给版面上把握大的情形(超限价、备选方案、投标函里的有效期、符合性响应表里写了不响应); + 认不准的降为重要或提示,宁可让人核对,不让规矩的投标文件挂致命问题; +- 认不出结构、缺项目侧的值(限价、截止时间、有效期要求)时不报问题,写进检查覆盖范围(m3-plan §2.5); +- 写法按招投标文件的通用格式归纳,不按评测集的措辞写(评测集的开发 / 留出词库分开,见 check-eval.md)。 +""" + +from __future__ import annotations + +import hashlib +import re +from collections import Counter +from dataclasses import dataclass, field +from datetime import date +from decimal import Decimal, InvalidOperation +from typing import Any +from uuid import UUID + +from bid_doctor.check.match import BidDoc +from bid_doctor.document.pipeline.text_layer import Sentence, norm + +LABELS = { + "PRICE.OVER_CEILING_TOTAL": "报价和最高限价比对", + "PRICE.OVER_CEILING_LOT": "报价和所投采购包的最高限价比对", + "PRICE.CAPITAL_MISMATCH": "大写金额和小写金额核对", + "PRICE.SUM_MISMATCH": "分项报价合计核对", + "PRICE.TOTAL_INCONSISTENT": "投标总价前后一致", + "PRICE.MULTIPLE_QUOTES": "只能有一个报价", + "FORM.FOREIGN_PROJECT": "别的项目名称、编号、采购人", + "FORM.BLANK_FIELD": "格式文件里没填的地方", + "FORM.DATE_AFTER_DEADLINE": "落款日期", + "FORM.VALIDITY_SHORT": "投标有效期", + "FORM.CONDITION_ADDED": "附加条件", + "FORM.RESPONSE_NEGATIVE": "响应表里的未响应、负偏离", +} +RULES_NOT_YET = ( + "分项报价表和采购需求清单逐项对照、分项 / 单价上限、格式文件固定文字被改、" + "授权代表和签字人一致、声明函填报、签章有无、低价说明、跨页的响应表" +) + + +@dataclass(frozen=True, slots=True) +class Facts: + """项目侧(招标文件、项目基本信息)的已知值。refs:依据 → 招标侧出处编号(产品里是 citation id,评测里为空)。""" + + name: str | None = None + code: str | None = None + purchaser: str | None = None + agency: str | None = None + deadline: date | None = None + ceiling: Decimal | None = None + ceiling_scope: str = "total" # total:项目限价;lot:所投采购包的限价;multi:投了几个包 + ceiling_note: str | None = None # 没有可比的限价时的原因(写进覆盖范围) + validity_days: int | None = None + validity_item: UUID | None = None + refs: dict[str, list[UUID]] = field(default_factory=dict[str, list[UUID]]) + + +@dataclass(slots=True) +class Hit: + code: str + type: str # price_form / formality / negative_deviation + severity: str + title: str + explanation: str + evidence: list[tuple[BidDoc, Sentence]] + target: str # 复查对齐用:match_key = rule:: + basis: str | None = None # Facts.refs 的键:招标侧出处 + item: UUID | None = None # 对应的清单项(有的话) + detail: dict[str, Any] = field(default_factory=dict[str, Any]) + + +def target(text: str) -> str: + """复查对齐用的目标:原文前 30 字 + 全文摘要(前缀相同的两句不会互相吞掉,原文没改就能对上)。""" + t = norm(text) + return f"{t[:30]}#{hashlib.sha256(t.encode()).hexdigest()[:8]}" + + +# ---------- 金额、数字 ---------- +MONEY = re.compile(r"(? Decimal | None: + """大写金额 → 数值(元)。写法不成形的返回 None。""" + total, section, num, frac = Decimal(0), 0, 0, Decimal(0) + for ch in s: + if ch in DIGIT: + num = DIGIT[ch] + elif ch in SMALL: + section += (num or 1) * SMALL[ch] + num = 0 + elif ch == "万": + total += Decimal((section + num) * 10_000) + section = num = 0 + elif ch == "亿": + total = (total + section + num) * 100_000_000 + section = num = 0 + elif ch in "元圆": + total += section + num + section = num = 0 + elif ch == "角": + frac += Decimal(num) / 10 + num = 0 + elif ch == "分": + frac += Decimal(num) / 100 + num = 0 + if section or num: + return None + return total + frac + + +def money(text: str, *, plain: bool = False) -> list[Decimal]: + """文本里像金额的数:带千分位、两位小数或「元 / 万元」的(plain=True 时整数也算,表格的金额栏用)。""" + out = [] + for m in MONEY.finditer(text): + raw, unit = m.group(1), m.group(2) + if not (plain or unit or "," in raw or re.search(r"\.\d\d$", raw)): + continue + try: + v = Decimal(raw.replace(",", "")) + except InvalidOperation: + continue + out.append(v * 10_000 if unit == "万元" else v) + return out + + +def cn_int(s: str) -> int | None: + """「六十」「一百二十」「90」→ 整数。""" + if s.isdigit(): + return int(s) + total, num = 0, 0 + for ch in s: + if ch in CN: + num = CN[ch] + elif ch == "十": + total += (num or 1) * 10 + num = 0 + elif ch == "百": + total += (num or 1) * 100 + num = 0 + else: + return None + return total + num or None + + +def quote(text: str, n: int = 60) -> str: + """说明里引用的原文:截断,去掉句末标点(后面接自己的句号)。""" + return text[:n].rstrip("。;;,, ") + + +def yuan(v: Decimal) -> str: + return f"{v:,.2f} 元" + + +# ---------- 版面:页、行、单元格 ---------- +def _box(s: Sentence) -> tuple[float, float, float, float]: + return ( + min(q[0] for q in s.quads), + min(q[1] for q in s.quads), + max(q[2] for q in s.quads), + max(q[3] for q in s.quads), + ) + + +def pages_of(doc: BidDoc) -> dict[int, list[Sentence]]: + out: dict[int, list[Sentence]] = {} + for s in doc.sentences: + out.setdefault(s.page_no, []).append(s) + return out + + +@dataclass(slots=True) +class _Row: + top: float + bottom: float + in_cell: bool + items: list[Sentence] + + +def rows_of(page: list[Sentence]) -> list[list[Sentence]]: + """把一页的句子分成行,行内从左到右。有框线的表格按单元格的上沿分行:同一行的格子上沿相同, + 垂直居中的短格、折成几句的长格、纵向合并的格(只算进它的第一行)都不会串行;其余按上下范围重叠。""" + rows: list[_Row] = [] + + def top_of(s: Sentence) -> float: + return s.cell[1] if s.cell else _box(s)[1] + + for s in sorted((x for x in page if x.quads), key=top_of): + if s.cell: + r = next((r for r in rows if r.in_cell and abs(r.top - s.cell[1]) < 2), None) + if r: + r.items.append(s) + else: + rows.append(_Row(s.cell[1], s.cell[1], True, [s])) + continue + _, top, _, bottom = _box(s) + last = rows[-1] if rows else None + if last and not last.in_cell and top < last.bottom - 1: + last.bottom = max(last.bottom, bottom) + last.items.append(s) + else: + rows.append(_Row(top, bottom, False, [s])) + return [sorted(r.items, key=lambda x: _box(x)[0]) for r in sorted(rows, key=lambda r: r.top)] + + +def row_index(doc: BidDoc) -> dict[int, list[Sentence]]: + """句子 → 它所在的行(按句子对象)。""" + return {id(c): row for page in pages_of(doc).values() for row in rows_of(page) for c in row} + + +def _col(cell: Sentence, cols: dict[str, tuple[float, float]]) -> str | None: + """单元格落在哪一栏:和表头单元格左右范围重叠最多的那一栏;都不重叠(窄数字居中、表头字靠左)取中心最近的。 + cols 要包含表头的每一格,否则别的栏的数字会被归到最近的已知栏。""" + x0, x1 = _span(cell) + best, score = None, 0.0 + for name, (c0, c1) in cols.items(): + overlap = min(x1, c1) - max(x0, c0) + if overlap > score: + best, score = name, overlap + if best is None and cols: + mid = (x0 + x1) / 2 + best = min(cols, key=lambda n: abs((cols[n][0] + cols[n][1]) / 2 - mid)) + return best + + +def _span(c: Sentence) -> tuple[float, float]: + return (c.cell[0], c.cell[2]) if c.cell else (_box(c)[0], _box(c)[2]) + + +# ---------- 分项报价表、开标一览表 ---------- +@dataclass(slots=True) +class ItemRow: + cells: list[Sentence] + qty: Decimal | None + unit_price: Decimal | None + line: Decimal | None + ambiguous: bool = False # 同一栏读到两个数(无框线表格里纵向合并的格子把几行并成了一行):不核对 + + +@dataclass(slots=True) +class PriceTable: + doc: BidDoc + rows: list[ItemRow] + total: tuple[Sentence, Decimal] | None # 合计行 + + +@dataclass(frozen=True, slots=True) +class Total: + sentence: Sentence + value: Decimal + scaled: bool # 按表头的「万元」折算过 + + +HEAD_QTY = re.compile(r"^数量") +HEAD_UNIT = re.compile(r"单价") +HEAD_LINE = re.compile(r"(合价|总价|小计|金额)") +TOTAL_ROW = re.compile(r"^(合计|总计|总价|报价合计|投标总价)") +SUBTOTAL = re.compile(r"(小计|合计|总计)") +TOTAL_HEAD = re.compile(r"(投标总?报价|总报价|投标总价|报价合计|响应报价|磋商报价|投标报价)") + + +def _header(row: list[Sentence]) -> dict[str, tuple[float, float]] | None: + cols: dict[str, tuple[float, float]] = {} + for n, c in enumerate(row): + t = norm(c.text) + if HEAD_QTY.search(t) and "qty" not in cols: + cols["qty"] = _span(c) + elif HEAD_UNIT.search(t) and "unit" not in cols: + cols["unit"] = _span(c) + elif HEAD_LINE.search(t) and "line" not in cols: + cols["line"] = _span(c) + else: + cols[f"c{n}"] = _span(c) # 序号、名称、单位等栏:也要参与归栏 + return cols if {"qty", "unit", "line"} <= cols.keys() else None + + +def _scale(row: list[Sentence]) -> int: + return 10_000 if any("万元" in c.text for c in row) else 1 + + +def price_tables(doc: BidDoc) -> list[PriceTable]: + """分项报价表:表头有数量、单价、总价三栏;表体逐行读到合计行或表结束,可以接续到下一页 + (下一页重复表头的,接着算同一张表)。「硬件小计」这类分组小计不算进明细。""" + tables: list[PriceTable] = [] + cur: tuple[PriceTable, dict[str, tuple[float, float]], int] | None = None + for _, page in sorted(pages_of(doc).items()): + for i, row in enumerate(rows_of(page)): + cols = _header(row) + if cols: + if cur is not None and i <= 1 and cur[0].rows and cur[0].total is None: + cur = (cur[0], cols, _scale(row)) # 跨页重复的表头 + else: + cur = (PriceTable(doc, [], None), cols, _scale(row)) + tables.append(cur[0]) + continue + if cur is None: + continue + table, cols, scale = cur + text = norm("".join(c.text for c in row)) + words = norm("".join(c.text for c in row if not money(c.text, plain=True))) + if TOTAL_ROW.search(text): + amounts = [ + (c, v * scale) for c in row if _col(c, cols) == "line" for v in money(c.text, plain=True)[:1] + ] + amounts = amounts or [(c, v) for c in row for v in money(c.text)[:1]] + if amounts: + table.total = amounts[0] + cur = None + continue + if SUBTOTAL.search(words): + continue # 分组小计 + found: dict[str, list[Decimal]] = {} + for c in row: + name = _col(c, cols) + nums = money(c.text, plain=True) + if name and nums and len(norm(c.text)) <= 20: + found.setdefault(name, []).append(nums[0] * (scale if name != "qty" else 1)) + if "unit" not in found and "line" not in found: + if table.rows and all(_span(c)[1] <= cols["qty"][0] for c in row): + continue # 名称栏折行的续行 + cur = None # 表格结束(下一节的标题、正文) + continue + ambiguous = any(len(found.get(k, [])) > 1 for k in ("qty", "unit", "line")) + table.rows.append( + ItemRow( + row, + (found.get("qty") or [None])[0], + (found.get("unit") or [None])[0], + (found.get("line") or [None])[0], + ambiguous, + ) + ) + return [t for t in tables if t.rows] + + +def bid_totals(doc: BidDoc) -> list[Total]: + """开标一览表(报价一览表)里写的投标总价:表头写「投标总报价 / 投标报价」(不是大写栏)的那一栏, + 下面恰好只有一个金额才算(几个金额的是分项表、多年度报价表,不是总价);以及「投标总价:xxx 元」这样的句子。""" + out: list[Total] = [] + for _, page in sorted(pages_of(doc).items()): + rows = rows_of(page) + for i, row in enumerate(rows): + heads = [ + n + for n, c in enumerate(row) + if TOTAL_HEAD.search(norm(c.text)) and "大写" not in c.text and len(c.text) <= 30 + ] + if heads and len(row) > 1 and not _header(row): + cols = {f"c{n}": _span(c) for n, c in enumerate(row)} + want = {f"c{n}" for n in heads} + scale = _scale([row[n] for n in heads]) + vals: list[tuple[Sentence, Decimal]] = [] + for nxt in rows[i + 1 : i + 6]: + if _header(nxt) or TOTAL_HEAD.search(norm("".join(c.text for c in nxt))): + break + for c in nxt: + if _col(c, cols) in want and not UPPER.search(c.text): + vals += [(c, v * scale) for v in money(c.text, plain=True)[:1] if v * scale >= 100] + if len(vals) == 1: + out.append(Total(vals[0][0], vals[0][1], scale != 1)) + continue + for c in row: + m = re.search(r"(投标总?报价|总报价|投标总价|报价合计)(?:[((]?小写[))]?)?[::]\s*(.+)", norm(c.text)) + if m: + out += [Total(c, v, False) for v in money(m.group(2))[:1]] + return out + + +# ---------- 检查 ---------- +def check_prices( + facts: Facts, docs: list[BidDoc], tables: list[PriceTable], totals: list[Total], hits: list[Hit], notes: list[str] +) -> None: + by_doc = {id(s): d for d in docs for s in d.sentences} + if not tables and not totals: + notes.append("没有认出开标一览表和分项报价表,报价金额没有核对") + values = sorted({t.value for t in totals}) + + # 分项表:数量 × 单价 = 总价;各项总价相加 = 合计行(或开标一览表唯一的总价) + for n, t in enumerate(tables, start=1): + wrong: list[str] = [] + ev: list[tuple[BidDoc, Sentence]] = [] + for r in t.rows: + if r.ambiguous or r.qty is None or r.unit_price is None or r.line is None: + continue + tol = r.qty * Decimal("0.005") + Decimal("0.01") # 单价四舍五入到分,总价按未舍入的单价算 + diff = r.qty * r.unit_price - r.line + if abs(diff) > tol: + name = next((c.text for c in r.cells if not money(c.text, plain=True)), "") + wrong.append( + f"「{name[:20]}」数量 {r.qty:,} × 单价 {yuan(r.unit_price)}," + f"和这一行的总价 {yuan(r.line)} 相差 {yuan(abs(diff))}" + ) + ev += [(t.doc, c) for c in r.cells if money(c.text, plain=True)] + lines = [r.line for r in t.rows if r.line is not None] + target_value = t.total[1] if t.total else (values[0] if len(tables) == 1 and len(values) == 1 else None) + if lines and target_value is not None and not any(r.ambiguous for r in t.rows): + s = sum(lines, Decimal(0)) + if abs(s - target_value) > Decimal("0.01") * len(lines) + Decimal("0.005"): + where = "合计行" if t.total else "开标一览表的投标总价" + wrong.append(f"各项总价相加是 {yuan(s)},{where}写的是 {yuan(target_value)}") + ref = t.total[0] if t.total else next(x.sentence for x in totals if x.value == target_value) + ev.append((by_doc[id(ref)], ref)) + if wrong: + hits.append( + Hit( + "PRICE.SUM_MISMATCH", + "price_form", + "important", + "分项报价的数字对不上", + ";".join(wrong[:4]) + (";……" if len(wrong) > 4 else "") + "。请核对分项报价表和开标一览表。", + ev[:8], + f"table{n}", + detail={"mismatches": wrong}, + ) + ) + # 分项合计和开标一览表的总价不一致(分项表没有合计行时上面已经比过) + for t in tables: + if t.total and values and all(abs(v - t.total[1]) > Decimal("0.005") for v in values): + first = totals[0] + hits.append( + Hit( + "PRICE.SUM_MISMATCH", + "price_form", + "important", + "开标一览表的总价和分项报价合计不一致", + f"开标一览表写的是 {yuan(first.value)},分项报价表合计是 {yuan(t.total[1])}。", + [(by_doc[id(first.sentence)], first.sentence), (t.doc, t.total[0])], + "summary", + ) + ) + break + + # 不同位置写的投标总价不一致(投标函和开标一览表、两个分册):不判断哪个是「另一个报价」,请人核对 + if len(values) > 1 and facts.ceiling_scope != "multi": + hits.append( + Hit( + "PRICE.TOTAL_INCONSISTENT", + "price_form", + "important", + "投标总价前后写的不一致", + "投标文件不同位置写的投标总价是 " + "、".join(yuan(v) for v in values) + ",请核对。", + [(by_doc[id(x.sentence)], x.sentence) for x in totals][:4], + "totals", + ) + ) + + # 限价 + if facts.ceiling is None: + notes.append(facts.ceiling_note or "项目信息里没有最高限价,报价没有和限价比对") + elif totals: + over = [x for x in totals if x.value > facts.ceiling] + if over: + x = max(over, key=lambda t: t.value) + lot = facts.ceiling_scope == "lot" + hits.append( + Hit( + "PRICE.OVER_CEILING_LOT" if lot else "PRICE.OVER_CEILING_TOTAL", + "price_form", + "fatal", + "投标报价超过最高限价", + f"开标一览表的投标总价是 {yuan(x.value)}{'(按表头的「万元」折算)' if x.scaled else ''}," + f"招标文件的{'所投采购包' if lot else ''}最高限价是 {yuan(facts.ceiling)}。" + "报价超过最高限价通常会导致投标无效,请核对。", + [(by_doc[id(x.sentence)], x.sentence)], + "total", + basis="ceiling", + detail={"bid_total": str(x.value), "ceiling": str(facts.ceiling), "scaled": x.scaled}, + ) + ) + + # 大写和小写金额 + for d in docs: + page = pages_of(d) + rows = row_index(d) + for s in d.sentences: + for m in UPPER.finditer(s.text): + v = upper_amount(m.group(0)) + if v is None: + continue + near = _lower_near(s, m, page.get(s.page_no, []), rows) + if not near or any(v in (x, x * 10_000) for x, _ in near): + continue + x, src = near[0] + hits.append( + Hit( + "PRICE.CAPITAL_MISMATCH", + "price_form", + "important", + "大写金额和小写金额不一致", + f"大写「{m.group(0)}」折合 {yuan(v)},旁边的小写金额是 {yuan(x)}。", + [(d, s), *([(d, src)] if src is not s else [])], + target(m.group(0) + s.text), + detail={"upper": m.group(0), "upper_value": str(v), "lower": str(x)}, + ) + ) + + +LOWER_MARK = re.compile(r"小写|[¥¥]|报价|总价|合计|金额|价款") + + +def _lower_near( + s: Sentence, m: re.Match[str], page: list[Sentence], rows: dict[int, list[Sentence]] +) -> list[tuple[Decimal, Sentence]]: + """大写金额旁边的小写金额,按把握排序:同一句(「合计:1,000.00 元(壹仟元整)」)→ 同一行的单元格 → + 同一页前后两句里写着「小写 / ¥ / 报价 / 合计」的。任何一处相等就算一致(调用方判断)。""" + out = [(v, s) for v in money(s.text[: m.start()] + s.text[m.end() :])] + row = rows.get(id(s), []) + out += [(v, c) for c in row if c is not s for v in money(c.text)] + i = next((n for n, x in enumerate(page) if x is s), -1) + for off in (1, 2): + for j in (i - off, i + off): + near = 0 <= j < len(page) and i >= 0 and not any(page[j] is c for c in row) + if near and LOWER_MARK.search(page[j].text): + out += [(v, page[j]) for v in money(page[j].text)] + return out + + +ALT = re.compile( + r"(备选|替代|选择性|可选)(方案)?(报价|价格|总价)|优惠后?的?(报价|价格|总价|价)|折扣后?的?(报价|价格|总价|价)|" + r"方案[一二三四五1-5][^,。;]{0,6}(报价|总价|价格)|第[二三][种套个份]?(报价|方案)|(让利|下浮|降价)后?的?(报价|价格|总价)" +) +ALT_FATAL = re.compile(r"备选|替代|选择性|可选|方案[二三四五2-5]|第[二三]") +NEG = re.compile(r"不|无|未|拒绝") +RESP_PREFIX = re.compile(r"^(完全响应|已响应|响应|满足|无偏离|正偏离|同意|接受)[,,::。、\s]*") +NUMBERING = re.compile(r"^([((]?[0-9一二三四五六七八九十]{1,3}[)).、.]|[0-9]+(\.[0-9]+)+)\s*") + + +class Tender: + """招标文件全文(去空白):投标文件照抄的招标原文(逐条响应表里「满足,」后面复述的要求)不当作投标人自己的话。""" + + def __init__(self, sentences: list[Sentence]) -> None: + self.text = "".join(norm(s.text) for s in sentences) + + def restated(self, text: str) -> bool: + core = NUMBERING.sub("", RESP_PREFIX.sub("", NUMBERING.sub("", norm(text)))) + if len(core) < 10: + return False + return core[:20] in self.text or core[-20:] in self.text + + def has(self, value: str) -> bool: + v = norm(value) + return len(v) >= 6 and v in self.text + + +def check_quotes( + docs: list[BidDoc], tender: Tender, tables: list[PriceTable], totals: list[Total], hits: list[Hit] +) -> None: + """另外给的报价:「备选方案报价」「方案二总价」后面跟着金额(致命);「优惠价 / 让利后」跟着的金额和投标总价 + 不一样的(重要:多半是另报了一个价,也可能只是说明)。金额等于投标总价(开标一览表或分项合计)的、否定句、 + 招标文件原话都不算。""" + values = {t.value for t in totals} | {t.total[1] for t in tables if t.total} + for d in docs: + for s in d.sentences: + m = ALT.search(s.text) + if not m: + continue + amounts = money(s.text[m.end() :]) + clause = re.split(r"[,。;,;]", s.text[: m.start()])[-1] + if not amounts or NEG.search(clause) or tender.restated(s.text) or set(amounts) & values: + continue + fatal = bool(ALT_FATAL.search(m.group(0))) + hits.append( + Hit( + "PRICE.MULTIPLE_QUOTES", + "price_form", + "fatal" if fatal else "important", + "投标文件里另外给了一个报价" if fatal else "投标文件里有另一个价格", + f"「{quote(s.text)}」。招标文件允许备选方案的除外,一份投标只能有一个报价,请核对。", + [(d, s)], + target(s.text), + ) + ) + + +PAGE_NO = re.compile(r"^[-—\s第]*\d+[-—\s页]*(/\s*\d+|共\d+页)?[-—\s]*$") +CODE = re.compile(r"[A-Za-z]{2,}[A-Za-z0-9]*(?:[--—_][A-Za-z0-9]+){2,}") +ANNOT = re.compile( + r"([^,。;::、()()]{2,60})" + r"[((](项目名称|项目编号|采购编号|招标编号|采购项目编号|采购人名称|采购人)[))]" +) +CODE_LABEL = re.compile(r"(项目编号|采购编号|招标编号|采购项目编号)(?:为|是)?[::]?\s*(" + CODE.pattern + ")") +TO = re.compile(r"^致[::]\s*([^((,。]{2,40})") +LEAD = re.compile(r"^(根据|参加|就|对|按|在|为|关于|贵方|贵单位|贵中心|贵公司)+") +GENERIC_OWNER = re.compile(r"^(贵方|贵单位|贵中心|贵公司|采购人|招标人|采购代理机构|代理机构|招标代理机构|采购单位)?$") +HEADER_TAIL = re.compile(r"((投标|响应|磋商)文件|[((][^))]*[))]|[-—\s])+$") +PROJECT_END = re.compile(r"(项目|工程|采购|标段|招标|建设)$") +CHAPTER = re.compile(r"^第[一二三四五六七八九十\d]+[章节部分篇]|方案|部分|章节") +BIDDER = re.compile(r"(投标人|供应商|响应人|投标单位)(名称)?(([^)]{0,8}))?[::]\s*([^((,。::]{4,40})") + + +def _bigrams(s: str) -> set[str]: + return {s[i : i + 2] for i in range(len(s) - 1)} + + +def same_name(a: str, b: str) -> bool: + """名称是不是同一个:互相包含,或字对重合度够高(简称、括注、「项目」两字有无都算同一个)。""" + a, b = norm(a).strip("“”\"'"), norm(b).strip("“”\"'") + if not a or not b: + return False + if a in b or b in a: + return True + x, y = _bigrams(a), _bigrams(b) + return bool(x and y) and 2 * len(x & y) / (len(x) + len(y)) >= 0.6 + + +def bidder_name(docs: list[BidDoc]) -> str | None: + """投标人名称:落款「投标人(供应商)名称:xxx」里出现最多的那个。""" + c: Counter[str] = Counter() + for d in docs: + for s in d.sentences: + if m := BIDDER.search(s.text): + c[norm(m.group(4))] += 1 + return c.most_common(1)[0][0] if c else None + + +def check_foreign(facts: Facts, docs: list[BidDoc], tender: Tender, hits: list[Hit], notes: list[str]) -> None: + """别的项目留下来的名称、编号、采购人:页眉、格式文件里「xxx(项目名称)」这类填写位、「致:xxx」。 + 业绩表里的项目名称是投标人自己的业绩,不在填写位里,不查;招标文件里出现过的名称、编号(代理机构编号、 + 项目简称)不算别的项目。""" + if not (facts.name or facts.code): + notes.append("项目信息里没有项目名称和编号,没有检查是否混入别的项目") + return + bidder = bidder_name(docs) + owners = [x for x in (facts.purchaser, facts.agency) if x] + + def ours(text: str) -> bool: + t = LEAD.sub("", norm(text)) + if facts.code and norm(facts.code).lower() in t.lower(): + return True + if facts.name and same_name(t, facts.name): + return True + return bool(bidder and bidder in t) or tender.has(t) + + for d in docs: + seen: set[str] = set() + for s in d.header_footer: + t = norm(s.text) + core = HEADER_TAIL.sub("", t) + looks_like_project = bool(PROJECT_END.search(core) or CODE.search(t)) and not CHAPTER.search(core) + if t in seen or len(core) < 6 or PAGE_NO.match(t) or not looks_like_project or ours(core): + continue + seen.add(t) + pages = [x.page_no for x in d.header_footer if norm(x.text) == t] + hits.append( + Hit( + "FORM.FOREIGN_PROJECT", + "formality", + "important", + "页眉里是别的项目名称", + f"{d.label}的页眉写着「{s.text[:50]}」(共 {len(pages)} 页)," + f"和本项目「{facts.name or facts.code}」不一致。" + "可能是沿用了别的投标文件,请核对。", + [(d, s)], + f"header:{target(t)}", + basis="name", + detail={"pages": pages[:50]}, + ) + ) + for s in d.sentences: + for kind, value in _filled(s.text): + if kind == "code": + bad = bool(facts.code) and norm(facts.code).lower() not in norm(value).lower() + bad = bad and not tender.has(value) + elif kind == "name": + bad = bool(facts.name) and not ours(value) + else: + v = LEAD.sub("", norm(value)) + bad = bool(owners) and not GENERIC_OWNER.match(v) and not tender.has(v) + bad = bad and not any(same_name(v, o) for o in owners) + if not bad: + continue + label = {"code": "项目编号", "name": "项目名称", "purchaser": "采购人"}[kind] + expect = {"code": facts.code, "name": facts.name, "purchaser": "、".join(owners)}[kind] + hits.append( + Hit( + "FORM.FOREIGN_PROJECT", + "formality", + "important", + f"{label}和本项目不一致", + f"这里填的{label}是「{value[:40]}」,本项目是「{expect}」。请核对是不是沿用了别的投标文件。", + [(d, s)], + f"{kind}:{target(value)}", + basis=kind, + ) + ) + + +def _filled(text: str) -> list[tuple[str, str]]: + out: list[tuple[str, str]] = [] + for m in ANNOT.finditer(text): + value, label = LEAD.sub("", m.group(1)).strip(), m.group(2) + if not value or re.fullmatch(r"[\s__]*", value): + continue + if "编号" in label: + codes = CODE.findall(value) + if codes: + out.append(("code", codes[-1])) + elif label == "项目名称": + out.append(("name", value)) + else: + out.append(("purchaser", value)) + for m in CODE_LABEL.finditer(text): + if ("code", m.group(2)) not in out: + out.append(("code", m.group(2))) + if m := TO.match(norm(text)): + out.append(("purchaser", m.group(1))) + return out + + +BLANKS = ( + (re.compile(r"[__]{3,}"), "下划线处没有填"), + (re.compile(r"(? None: + """格式文件里该填没填的地方(提示级)。「(盖单位章)」「(签字)」这类括注本来就留着,不算。""" + n = 0 + for d in docs: + rows = row_index(d) + for s in d.sentences: + why = next((w for p, w in BLANKS if p.search(s.text)), None) + if why is None: + continue + row = rows.get(id(s), []) + if why == "冒号后面没有填" and row and row[-1] is not s: + continue # 表格里「名称:」后面一格填了 + n += 1 + if n > BLANK_CAP: + continue + hits.append( + Hit( + "FORM.BLANK_FIELD", + "formality", + "hint", + "可能有没填的地方", + f"「{quote(s.text)}」:{why}。请核对是否需要填写。", + [(d, s)], + target(s.text), + ) + ) + if n > BLANK_CAP: + notes.append(f"疑似没填的地方共 {n} 处,只列出前 {BLANK_CAP} 处") + + +DATE = re.compile(r"(\d{4})\s*[年.\-/]\s*(\d{1,2})\s*[月.\-/]\s*(\d{1,2})\s*日?") +CN_DATE = re.compile( + r"([〇零○一二三四五六七八九]{4})\s*年\s*([一二三四五六七八九十]{1,3})\s*月\s*([一二三四五六七八九十]{1,3})\s*日" +) +SIGN_LABEL = re.compile(r"日\s*期\s*[::]\s*") +SIGN_PREFIX = ("签署", "落款", "投标", "签字", "填报", "编制", "响应", "授权") + + +def _cn_year(s: str) -> int: + return int("".join(str(CN.get(c, 0)) for c in s.replace("○", "〇"))) + + +def _sign_dates(text: str) -> list[tuple[str, date | None]]: + """落款日期:「日期:2026年11月17日」「日 期:二〇二六年十一月十七日」(前面紧挨着的是「签署 / 落款 / 投标」 + 或不是汉字;「发证日期」「截止日期」不算)。写错的日期返回 None。""" + out: list[tuple[str, date | None]] = [] + for m in SIGN_LABEL.finditer(text): + before = text[: m.start()] + if before and "一" <= before[-1] <= "鿿" and not before.endswith(SIGN_PREFIX): + continue + if dm := DATE.match(text, m.end()): + y, mo, da = (int(x) for x in dm.groups()) + elif dm := CN_DATE.match(text, m.end()): + y, mo, da = _cn_year(dm.group(1)), cn_int(dm.group(2)) or 0, cn_int(dm.group(3)) or 0 + else: + continue + try: + out.append((dm.group(0), date(y, mo, da))) + except ValueError: + out.append((dm.group(0), None)) + return out + + +def check_dates(facts: Facts, docs: list[BidDoc], hits: list[Hit], notes: list[str]) -> None: + if facts.deadline is None: + notes.append("项目信息里没有投标截止时间,落款日期没有比对") + for d in docs: + found: dict[str, tuple[date | None, list[Sentence]]] = {} + for s in d.sentences: + for raw, value in _sign_dates(s.text): + if value is None or (facts.deadline and value > facts.deadline): + found.setdefault(raw, (value, []))[1].append(s) + for raw, (value, sents) in found.items(): + if value is None: + title, text = "落款日期写法有误", f"落款日期写的是「{raw}」,不是一个有效的日期。" + else: + title = "落款日期晚于投标截止时间" + text = f"落款日期写的是「{raw}」,投标截止时间是 {facts.deadline:%Y年%m月%d日}。" + hits.append( + Hit( + "FORM.DATE_AFTER_DEADLINE", + "formality", + "hint", + title, + text + (f"共 {len(sents)} 处。" if len(sents) > 1 else "") + "请核对。", + [(d, s) for s in sents[:5]], + f"{d.no}:{norm(raw)}", + basis="deadline" if value else None, + ) + ) + + +NUM = r"([0-9]+|[零一二两三四五六七八九十百]+)\s*(?:[((]\s*([0-9]+)\s*[))])?" +UNIT = r"(个月|个?日历[天日]|个?工作日|日|天)" +VALID = re.compile(r"(投标|响应|磋商|谈判)(文件)?有效期[^。;,,内]{0,30}?" + NUM + r"\s*[((]?" + UNIT) +VALID_IN = re.compile(NUM + r"\s*[((]?" + UNIT + r"[))]?\s*内[^。;]{0,4}(投标|响应)?有效") +VALID_HEAD = re.compile(r"^(投标|响应|磋商|谈判)(文件)?有效期[((]?[^))]{0,6}[))]?$") +DAYS = re.compile(r"^" + NUM + r"\s*[((]?" + UNIT) +LETTER = re.compile(r"(投标|响应|磋商|谈判)函|开标一览表|报价一览表|响应一览表") + + +def _days(n: str, paren: str | None, unit: str) -> int | None: + v = int(paren) if paren else cn_int(n) + return None if not v else v * 30 if "月" in unit else v + + +def validity_days(text: str) -> list[int]: + """投标有效期写了多少天(「九十(90)天」取括号里的数字;「三个月」按 90 天)。""" + out = [v for m in VALID.finditer(text) if (v := _days(m.group(3), m.group(4), m.group(5)))] + out += [v for m in VALID_IN.finditer(text) if (v := _days(m.group(1), m.group(2), m.group(3)))] + return out + + +def required_validity(items: dict[UUID, dict[str, Any]]) -> tuple[int | None, UUID | None]: + """清单里的投标有效期要求(日期类、date_kind = validity 的条目)。有几条时取天数最少的: + 只在投标文件比所有写法都短时才报,不拿最严的一条去卡。""" + best: tuple[int | None, UUID | None] = (None, None) + for item_id, p in items.items(): + if ((p.get("spec") or {}).get("schedule") or {}).get("date_kind") != "validity": + continue + days = validity_days(f"{p.get('title') or ''}。{p.get('requirement_text') or ''}") + if days and (best[0] is None or min(days) < best[0]): + best = (min(days), item_id) + return best + + +def _table_validity(doc: BidDoc) -> list[tuple[Sentence, int]]: + """表格里的写法:「投标有效期」一格,右边一格或下一行同一栏写「90 天」。""" + out: list[tuple[Sentence, int]] = [] + for _, page in sorted(pages_of(doc).items()): + rows = rows_of(page) + for i, row in enumerate(rows): + for n, c in enumerate(row): + if not VALID_HEAD.match(norm(c.text)): + continue + x0, x1 = _span(c) + below = rows[i + 1] if i + 1 < len(rows) else [] + cands = [*row[n + 1 : n + 2], *(x for x in below if min(x1, _span(x)[1]) > max(x0, _span(x)[0]))] + for x in cands: + if (m := DAYS.match(norm(x.text))) and (v := _days(m.group(1), m.group(2), m.group(3))): + out.append((x, v)) + break + return out + + +def check_validity(facts: Facts, docs: list[BidDoc], tender: Tender, hits: list[Hit], notes: list[str]) -> None: + """投标有效期短于要求:投标函、开标一览表那一页上的按致命报(招标文件通常把它列为无效情形); + 别处的(技术分册、附件里的一句话)按提示报。复述的招标原文不算。""" + if facts.validity_days is None: + notes.append("清单里没有投标有效期的天数要求,投标有效期没有比对") + return + for d in docs: + letter_pages = {no for no, page in pages_of(d).items() if LETTER.search(norm("".join(s.text for s in page)))} + found = [(s, v) for s in d.sentences if not tender.restated(s.text) for v in validity_days(s.text)] + for s, v in [*found, *_table_validity(d)]: + if v >= facts.validity_days: + continue + fatal = s.page_no in letter_pages + hits.append( + Hit( + "FORM.VALIDITY_SHORT", + "formality", + "fatal" if fatal else "hint", + "投标有效期短于招标文件要求", + f"投标文件写的投标有效期是 {v} 天,招标文件要求 {facts.validity_days} 天。" + + ("招标文件通常把投标有效期不满足要求列为无效情形,请核对。" if fatal else "请核对。"), + [(d, s)], + target(s.text), + basis="validity", + item=facts.validity_item, + detail={"bid_days": v, "required_days": facts.validity_days}, + ) + ) + + +CONDITIONS = ( + re.compile( + r"(报价|价格|总价|单价|投标价)[^。;]{0,8}(不|未)(包含|包括|含)[^。;]{0,8}(税|运|安装|调试|培训|运输|保险|费)" + ), + re.compile(r"按(实际|实际发生|实际需求|实际使用)[^。;,]{0,10}(结算|计算|计价|收费|计费)"), + re.compile(r"(费用|费)[^。;,]{0,6}由(采购人|甲方|贵方|用户|业主)[^。;,]{0,4}(承担|负责|支付|另付)"), + re.compile(r"另行(计算|计取|收取|收费|结算|报价|协商|商定|商议|约定|确定)|另议|另计"), + re.compile(r"以(实际|最终|现场|实际发生)[^。;,]{0,12}为准"), + re.compile(r"(价格|报价|单价|总价)[^。;]{0,10}(随|根据|按照?)[^。;]{0,12}(调整|浮动|变动)"), + re.compile(r"仅供参考"), + re.compile(r"(须|需|需要|要求)[^。;]{0,6}(预付|预付款|首付款)"), +) +COMMERCIAL = re.compile(r"报价|价格|价款|总价|单价|费用|税|付款|支付|结算|数量|工程量|到货|交货|工期|质保|合同") +NOT_CONDITION = re.compile( + r"(不|无|未|免|不再|无需|不得|决不|不会)[^。;,]{0,4}(另行|另计|另议)|无效|否则|不予|不接受|免费|不影响|不增加" +) + + +def check_conditions(docs: list[BidDoc], tender: Tender, hits: list[Hit]) -> None: + """附加条件:报价、付款、数量、交货上的保留或改动(「报价不含税」「另行协商」「以实际……为准」)。 + 「不另行收费」「不影响合同价款」这类承诺、招标文件原话(复述的要求)不算;技术分册里的按提示报。""" + for d in docs: + technical = "技术" in d.label + for s in d.sentences: + t = s.text + if len(norm(t)) < 10 or not COMMERCIAL.search(t) or NOT_CONDITION.search(t) or tender.restated(t): + continue + m = next((m for p in CONDITIONS if (m := p.search(t))), None) + if m is None: + continue + hits.append( + Hit( + "FORM.CONDITION_ADDED", + "formality", + "hint" if technical else "important", + "可能附加了条件", + f"「{quote(t)}」。附有采购人不能接受的条件可能导致投标无效," + "请核对这句话是不是对报价、付款或交货附加了条件。", + [(d, s)], + target(t), + detail={"phrase": m.group(0)}, + ) + ) + + +RESP_HEAD = re.compile(r"(响应情况|是否响应|响应结论|偏离情况|是否偏离|偏离/响应|响应/偏离|是否满足|符合或偏离)") +NEG_RESP = re.compile( + r"^(未完全响应|不完全响应|部分响应|未响应|不响应|负偏离|有偏离|不满足|部分满足|未满足|不符合|部分符合)" + r"([((].*)?[。;]?$" +) +YES_NO_HEAD = re.compile(r"是否(响应|满足|符合)") +DEVIATE_HEAD = re.compile(r"是否偏离") +FATAL_TABLE = re.compile(r"(符合性|实质性|资格)") +SERIAL = re.compile(r"\d{1,3}") + + +def _negative(cell: str, head: str) -> bool: + t = norm(cell) + if NEG_RESP.match(t): + return True + if YES_NO_HEAD.search(head) and t in ("否", "×"): + return True + return bool(DEVIATE_HEAD.search(head)) and t == "是" + + +def check_response_tables(docs: list[BidDoc], hits: list[Hit]) -> None: + """响应表(表头有「响应情况 / 偏离情况」一栏):这一栏写了「未完全响应」「负偏离」「否」之类,或者空着。 + 一条记录从序号行开始,到下一个序号行为止(垂直居中的格子、折行可能落在没有序号的行里)。 + 表名、表头写「符合性 / 实质性 / 资格」的写了不响应按致命报,其余按重要报;「空着」可能是版面没认准,按重要报。 + 能对上清单项的,响应匹配也会报,写入前按投标侧句子去重(drop_covered)。""" + for d in docs: + for _, page in sorted(pages_of(d).items()): + rows = rows_of(page) + for i, row in enumerate(rows): + at = next((n for n, c in enumerate(row) if RESP_HEAD.search(norm(c.text)) and len(c.text) <= 40), None) + if at is None or len(row) < 3: + continue + head = norm(row[at].text) + cols = {f"c{n}": _span(c) for n, c in enumerate(row)} + want = f"c{at}" + context = norm("".join(c.text for r in rows[max(0, i - 2) : i + 1] for c in r)) + fatal = bool(FATAL_TABLE.search(context)) + records: list[list[Sentence]] = [] + for r in rows[i + 1 :]: + numbered = len(r) >= 2 and SERIAL.fullmatch(norm(r[0].text)) is not None + if not numbered and RESP_HEAD.search(norm("".join(c.text for c in r))): + break # 下一张表的表头(表体每行都以序号开头,要求栏里也可能写着「……的响应情况」) + if numbered: + records.append(list(r)) + elif len(r) == 1 and len(norm(r[0].text)) > 40: + break # 表格结束(正文段落) + elif records: + records[-1] += r + for rec in records: + resp = [c for c in rec if _col(c, cols) == want] + others = [c for c in rec[1:] if not any(c is x for x in resp)] + if not others: + continue + req = others[0].text + bad = next((c for c in resp if _negative(c.text, head)), None) + if resp and bad is None: + continue + if bad is not None: + hits.append( + Hit( + "FORM.RESPONSE_NEGATIVE", + "negative_deviation", + "fatal" if fatal else "important", + f"响应表里写了「{norm(bad.text)}」", + f"「{req[:40]}」这一行写的是「{norm(bad.text)}」。" + + ( + "表名写着符合性 / 实质性审查,这类要求不满足通常会导致投标无效,请核对。" + if fatal + else "请核对。" + ), + [(d, c) for c in rec], + f"{d.no}:{target(req)}", + ) + ) + else: + hits.append( + Hit( + "FORM.RESPONSE_NEGATIVE", + "formality", + "important", + "响应表里有一行没有填响应情况", + f"「{req[:40]}」这一行没有读到响应情况(可能空着,也可能是版面没认准),请核对。", + [(d, c) for c in rec], + f"{d.no}:{target(req)}", + ) + ) + + +def drop_covered(hits: list[Hit], covered: set[tuple[str, int, str]]) -> list[Hit]: + """响应匹配已经按清单项报了「负偏离」的句子,规则不再重复报。covered:(分册名称, 页码, 句子原文)。""" + return [ + h + for h in hits + if h.code != "FORM.RESPONSE_NEGATIVE" + or not any((d.label, x.page_no, norm(x.text)) in covered for d, x in h.evidence) + ] + + +def run(facts: Facts, docs: list[BidDoc], tender_sentences: list[Sentence]) -> tuple[list[Hit], dict[str, Any]]: + """跑全部规则,返回问题和覆盖范围说明(checked / not_checked)。""" + tender = Tender(tender_sentences) + tables = [t for d in docs for t in price_tables(d)] + totals = [x for d in docs for x in bid_totals(d)] + hits: list[Hit] = [] + notes: list[str] = [] + check_prices(facts, docs, tables, totals, hits, notes) + check_quotes(docs, tender, tables, totals, hits) + check_foreign(facts, docs, tender, hits, notes) + check_blanks(docs, hits, notes) + check_dates(facts, docs, hits, notes) + check_validity(facts, docs, tender, hits, notes) + check_conditions(docs, tender, hits) + check_response_tables(docs, hits) + out: dict[tuple[str, str], Hit] = {} + for h in hits: + out.setdefault((h.code, h.target), h) # 同一规则同一目标只留一条(match_key 唯一) + coverage = { + "checked": "报价形式(大小写、分项合计、限价、多个报价)和形式要件(别的项目名称、没填的地方、落款日期、" + "投标有效期、附加条件、响应表里的未响应):按规则检查", + "not_checked": [*notes, f"{RULES_NOT_YET}:还没有上线"], + } + return list(out.values()), coverage diff --git a/server/src/bid_doctor/check/runner.py b/server/src/bid_doctor/check/runner.py index 3b738b8..846ad40 100644 --- a/server/src/bid_doctor/check/runner.py +++ b/server/src/bid_doctor/check/runner.py @@ -3,15 +3,17 @@ - 和解析任务同一套做法(架构 §3.4):阶段之间检查取消标记;结局由错误类别决定扣不扣点,失败、取消都退回。 - 健全性检查:投标文件几乎读不出文字时按输入问题失败,不扣点、不生成问题(参考项目的同类闸门)。 - 认不出、查不了、没查的都写进检查覆盖范围(coverage),不静默跳过(m3-plan §2.5)。 -TODO(第 2 步):规则检查(报价形式、形式要件、需人工核对);投标侧结构识别(偏离表、开标一览表、格式文件)。 +- 规则检查(报价形式、形式要件)在 rules.py,项目侧的值(限价、截止时间、有效期要求)带招标侧出处,见 _facts。 """ from __future__ import annotations import logging from datetime import UTC, datetime +from decimal import Decimal from typing import Any from uuid import UUID +from zoneinfo import ZoneInfo import httpx from sqlalchemy import delete, func, select @@ -20,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from bid_doctor.ai.models import AiCall from bid_doctor.ai.providers.base import TransientError from bid_doctor.billing import service as billing -from bid_doctor.check import applicability, findings, match +from bid_doctor.check import applicability, findings, match, rules from bid_doctor.check.models import CheckItemResult, CheckRun, CheckRunDocument, Finding, FindingCitation from bid_doctor.check.service import RULE_SET_VERSION, open_fatal from bid_doctor.checklist import edit as checklist_edit @@ -35,13 +37,16 @@ from bid_doctor.document import service as documents from bid_doctor.document.citations import document_label from bid_doctor.document.models import SourceDocument from bid_doctor.document.pipeline import normalize, text_layer +from bid_doctor.document.pipeline.text_layer import norm from bid_doctor.notification.models import Notification from bid_doctor.project import service as projects +from bid_doctor.project.models import Lot, Project, ProjectField log = logging.getLogger(__name__) PROGRESS = {"prepare_bid": 5, "match": 30, "rules": 85, "verify": 90, "carry_over": 93, "summarize": 96} MIN_BID_CHARS = 200 # 所有分册加起来读不出这么多字:按「投标文件几乎读不出文字」失败 REGIMES_VERIFIED = {"gov_procurement"} # 招标投标体系没有样本,规则未经验证(m3-plan §2.4) +LOCAL_TZ = ZoneInfo("Asia/Shanghai") # 投标截止时间按北京时间取日期 class Cancelled(Exception): @@ -126,6 +131,7 @@ async def _stages(ctx: TenantContext, run_id: UUID) -> None: label, text_layer.body_sentences(pages), [p.page_no for p in pages if p.text_source == "empty"], + [x for p in pages for x in p.sentences if x.location == "header_footer"], ) ) pages_info.append( @@ -163,6 +169,7 @@ async def _stages(ctx: TenantContext, run_id: UUID) -> None: [text_layer.Page.from_json(pg) for pg in await documents.text_layer(s, ctx, d)] ) ] + facts = await _facts(s, ctx, r, snapshot) # 招标文件按类别的响应方式(C12):用清单里存的(人工改过的为准);解析早于这项功能的项目临时识别 modes = response_mode.from_stored(stored_modes) if stored_modes else response_mode.detect(tender_sents) bidding = set(r.lot_ids) @@ -191,7 +198,15 @@ async def _stages(ctx: TenantContext, run_id: UUID) -> None: match.apply_modes(results, items, decisions) match.scanned_fallback(results, items, decisions, bid_docs) - await _stage(ctx, run_id, "rules") # 第 2 步:报价形式、形式要件、需人工核对 + await _stage(ctx, run_id, "rules") + hits, rule_cov = rules.run(facts, bid_docs, tender_sents) + covered = { + (d.label, x.page_no, norm(x.text)) + for res in results.values() + if res.verdict == "negative_deviation" + for d, x in res.evidence + } + hits = rules.drop_covered(hits, covered) await _stage(ctx, run_id, "verify") # 前两道校验在匹配时逐条做完;这里只是进度节点 await _stage(ctx, run_id, "carry_over") async with tenant_tx(ctx) as s: @@ -201,6 +216,7 @@ async def _stages(ctx: TenantContext, run_id: UUID) -> None: d.id: d for d in (await s.execute(select(SourceDocument).where(SourceDocument.id.in_(doc_ids)))).scalars() } await _write(s, ctx, r, items, results, by_doc) + await _write_rules(s, ctx, r, hits, facts, by_doc) await _carry_over(s, ctx, r, set(items)) _stage_nowait(r, "summarize") tokens = ( @@ -211,7 +227,9 @@ async def _stages(ctx: TenantContext, run_id: UUID) -> None: ) ).one() r.counts = await _counts(s, run_id) - r.coverage = _coverage(r, pages_info, items, results, pending, other_lot, stats, tokens, skipped, modes) + r.coverage = _coverage( + r, pages_info, items, results, pending, other_lot, stats, tokens, skipped, modes, rule_cov + ) await _publish(ctx, r, "running", "summarize", PROGRESS["summarize"]) @@ -219,6 +237,90 @@ def _stage_nowait(r: CheckRun, stage: str) -> None: r.stage, r.progress = stage, PROGRESS[stage] +async def _facts(s: AsyncSession, ctx: TenantContext, r: CheckRun, snapshot: dict[UUID, dict[str, Any]]) -> rules.Facts: + """规则检查要的项目侧值:项目基本信息(人工改过的为准)、所投采购包的限价、清单里的投标有效期要求,各带招标侧出处。""" + p = (await s.execute(select(Project).where(Project.id == r.project_id))).scalar_one() + fields = { + f.field_key: f for f in (await s.execute(select(ProjectField).where(ProjectField.project_id == p.id))).scalars() + } + + def value(key: str) -> Any: + f = fields.get(key) + return None if f is None else f.human_value if f.human_value is not None else f.ai_value + + refs = {k: [f.ai_citation_id] for k, f in fields.items() if f.ai_citation_id and f.human_value is None} + lots = [x for x in (await s.execute(select(Lot).where(Lot.project_id == p.id))).scalars() if not x.is_default] + bidding = [x for x in lots if x.id in set(r.lot_ids)] if r.lot_ids else lots + ceiling, scope, note = None, "total", None + if len(bidding) > 1: + scope, note = "multi", "投了多个采购包,报价按包和限价比对还没有上线" + elif len(bidding) == 1: + ceiling, scope = bidding[0].ceiling_amount, "lot" if len(lots) > 1 else "total" + refs.pop("ceiling_total", None) # 包的限价没有单独的出处;说明里写明取自所投采购包 + elif value("ceiling_total"): + ceiling = Decimal(str(value("ceiling_total"))) + days, item = rules.required_validity(snapshot) + if item is not None: + refs["validity"] = [UUID(c) for c in snapshot[item].get("citation_ids") or []] + refs["deadline"] = refs.pop("deadline_at", []) + refs["ceiling"] = refs.pop("ceiling_total", []) + return rules.Facts( + name=value("name") or p.name, + code=value("code") or p.code, + purchaser=value("purchaser") or p.purchaser, + agency=value("agency") or p.agency, + deadline=p.deadline_at.astimezone(LOCAL_TZ).date() if p.deadline_at else None, + ceiling=ceiling, + ceiling_scope=scope, + ceiling_note=note, + validity_days=days, + validity_item=item, + refs=refs, + ) + + +async def _write_rules( + s: AsyncSession, + ctx: TenantContext, + r: CheckRun, + hits: list[rules.Hit], + facts: rules.Facts, + by_doc: dict[UUID, SourceDocument], +) -> None: + """规则问题:挂投标原文的句子;有依据的(限价、有效期要求、项目名称)再挂招标侧出处。""" + for h in hits: + bid_cites = [] + for doc_id in dict.fromkeys(d.id for d, _ in h.evidence): + sents = [x for d, x in h.evidence if d.id == doc_id] + bid_cites += await citations.from_sentences(s, ctx, by_doc[doc_id], sents, "rule", []) + f = Finding( + id=uuid7(), + tenant_id=ctx.tenant_id, + project_id=r.project_id, + check_run_id=r.id, + checklist_item_id=h.item, + rule_code=h.code, + match_key=f"rule:{h.code}:{h.target}", + type=h.type, + severity=h.severity, + title=h.title[:200], + explanation=h.explanation, + detector="rule", + detail=h.detail, + checks_passed=[], + presence="new", + ) + s.add(f) + await s.flush() + for pos, cid in enumerate(facts.refs.get(h.basis or "", [])): + s.add( + FindingCitation(tenant_id=ctx.tenant_id, finding_id=f.id, citation_id=cid, side="tender", position=pos) + ) + for pos, c in enumerate(bid_cites): + s.add(FindingCitation(tenant_id=ctx.tenant_id, finding_id=f.id, citation_id=c.id, side="bid", position=pos)) + await s.flush() + + async def _write( s: AsyncSession, ctx: TenantContext, @@ -391,6 +493,7 @@ def _coverage( tokens: Any, skipped: dict[str, int], modes: dict[str, response_mode.Mode], + rule_cov: dict[str, Any], ) -> dict[str, Any]: """检查覆盖范围:报告首页、查标页顶部都读这里(FR-M3-09、m3-plan §2.5)。""" undetermined = sum(x.verdict == "undetermined" for x in results.values()) @@ -419,7 +522,9 @@ def _coverage( implicit = sum(x.verdict in ("implicit_accepted", "implicit_review") for x in results.values()) if implicit: checked.append(f"其中 {implicit} 条按招标文件「未列入偏离的视为接受」处理,没有复述不算问题") - not_checked.append("报价形式、形式要件、资质匹配:规则检查还没有上线") + checked.append(rule_cov["checked"]) + not_checked += rule_cov["not_checked"] + not_checked.append("资质匹配(证书有效期、主体一致):还没有上线") if not r.checklist_confirmed: not_checked.append("清单未确认,检查结果以当前清单为准") return { diff --git a/server/src/bid_doctor/check/service.py b/server/src/bid_doctor/check/service.py index 3cdbe9a..693eba5 100644 --- a/server/src/bid_doctor/check/service.py +++ b/server/src/bid_doctor/check/service.py @@ -19,6 +19,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from bid_doctor.audit import service as audit from bid_doctor.billing import service as billing +from bid_doctor.check import rules from bid_doctor.check.models import CheckItemResult, CheckRun, CheckRunDocument, Finding, FindingCitation from bid_doctor.checklist import edit as checklist_edit from bid_doctor.checklist import service as checklist @@ -35,7 +36,9 @@ from bid_doctor.project import service as projects from bid_doctor.project.models import Project CHECK_TASK = "check_project" -RULE_SET_VERSION = "2026.11-r0" # 第 1 步还没有规则;规则集有版本,检查批次记下当时用的版本(架构 §3.7) +RULE_SET_VERSION = ( + "2026.11-r1" # 规则集有版本,检查批次记下当时用的版本(架构 §3.7);r1:报价形式、形式要件(rules.py) +) WAIVE_WINDOW = timedelta(hours=24) ERROR_MESSAGES = { "bid_text_missing": "投标文件里几乎读不出文字(可能都是扫描件),扫描件识别还没有上线", @@ -307,7 +310,9 @@ async def _to_api_many(s: AsyncSession, run: CheckRun, rows: list[Finding]) -> l } if item else None, - "rule": {"code": f.rule_code, "description": ""} if f.rule_code and not item else None, + "rule": {"code": f.rule_code, "description": rules.LABELS.get(f.rule_code, "")} + if f.rule_code and not item + else None, "detail": f.detail, "tender_citations": [cites[x.citation_id] for x in ls if x.side == "tender" and x.citation_id in cites], "bid_citations": [cites[x.citation_id] for x in ls if x.side == "bid" and x.citation_id in cites], diff --git a/server/src/bid_doctor/document/pipeline/text_layer.py b/server/src/bid_doctor/document/pipeline/text_layer.py index fc0072a..153604b 100644 --- a/server/src/bid_doctor/document/pipeline/text_layer.py +++ b/server/src/bid_doctor/document/pipeline/text_layer.py @@ -59,6 +59,7 @@ class Sentence: fmt_runs: list[tuple[int, int, str]] = field(default_factory=list) # [起, 止, 格式],格式取 b / i / u 的组合 location: str = "body" open_end: bool = False # 写到页面末尾还没结束(没有句末标点、末行写满),可能接续到下一页 + cell: list[float] | None = None # 在有框线的表格里:所在单元格 [x0, top, x1, bottom](查标按单元格分行) @property def marked(self) -> str: @@ -83,6 +84,7 @@ class Sentence: "format_runs": [list(r) for r in self.fmt_runs], "location": self.location, "open_end": self.open_end, + **({"cell": self.cell} if self.cell else {}), } @classmethod @@ -96,6 +98,7 @@ class Sentence: fmt_runs=[(a, b, f) for a, b, f in d["format_runs"]], location=d["location"], open_end=d.get("open_end", False), + cell=d.get("cell"), ) @@ -150,6 +153,7 @@ Cell = tuple[float, float, float, float] # 单元格 (x0, top, x1, bottom) class FragInfo: right: float | None = None # 单元格的右边界:判断段落末行用它,不用页面右边距 cell_end: bool = False # 单元格的最后一个片段:句子在这里结束 + cell: Cell | None = None def _cell_of(c: dict[str, Any], cells: list[Cell]) -> int | None: @@ -171,13 +175,13 @@ def _fragments( for i, cell_chars in by_cell.items(): assert i is not None x0, top, x1, _ = cells[i] - blocks.append(((top, x0), _line_fragments(cell_chars), FragInfo(right=x1))) + blocks.append(((top, x0), _line_fragments(cell_chars), FragInfo(right=x1, cell=cells[i]))) frags: list[list[dict[str, Any]]] = [] infos: list[FragInfo] = [] for _, fs, info in sorted(blocks, key=lambda b: b[0]): for j, f in enumerate(fs): frags.append(f) - infos.append(FragInfo(right=info.right, cell_end=j == len(fs) - 1) if info else FragInfo()) + infos.append(FragInfo(right=info.right, cell_end=j == len(fs) - 1, cell=info.cell) if info else FragInfo()) return frags, infos @@ -310,6 +314,7 @@ def _page(doc_id: str, page: Any, frags: list[list[dict[str, Any]]], infos: list fmt_runs=runs, location=location, open_end=seq == tail_seq, + cell=[round(v, 2) for v in cell] if (cell := infos[g[0][0]].cell) and location == "body" else None, ) ) return Page( diff --git a/server/tests/fixtures/bid-mini-forms.pdf b/server/tests/fixtures/bid-mini-forms.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c9a17e0bbe25725eec4f60d9e723617f11998b4a GIT binary patch literal 305742 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 A6951J literal 0 HcmV?d00001 diff --git a/server/tests/fixtures/make_bid_fixtures.py b/server/tests/fixtures/make_bid_fixtures.py index fabe896..18be895 100644 --- a/server/tests/fixtures/make_bid_fixtures.py +++ b/server/tests/fixtures/make_bid_fixtures.py @@ -3,7 +3,9 @@ cd server && ../.venv/bin/python tests/fixtures/make_bid_fixtures.py # 需要 Gotenberg(:3109) - bid-mini-v1.pdf:响应了资质证书和服务器内存,漏了交换机硬件 BFD → 1 个致命问题「未找到响应」; -- bid-mini-v2.pdf:补上了交换机硬件 BFD → 复查时这个问题「本次已解决」。 +- bid-mini-v2.pdf:补上了交换机硬件 BFD → 复查时这个问题「本次已解决」; +- bid-mini-forms.pdf:v2 的内容 + 格式文件里的规则问题(大写和小写金额不一致、授权书没填投标人名称、 + 报价附加条件、另外给了一个优惠报价),措辞故意和评测集的开发词库不同。 """ from __future__ import annotations @@ -18,6 +20,7 @@ GOTENBERG = "http://127.0.0.1:3109/forms/chromium/convert/html" HEAD = """""" COMMON = [ @@ -41,6 +44,16 @@ def render(name: str, parts: list[str]) -> None: print(name, len(r.content)) +FORMS = [ + "

    五、法定代表人授权委托书。本人王某(姓名)系(投标人名称)的法定代表人,现授权李某为我方代理人。

    ", + "

    六、开标一览表

    ", + "" + "
    项目名称投标总报价(元)投标总报价(大写)
    某市智慧政务系统集成项目1,280,000.00壹拾贰万捌仟元整
    ", + "

    让利后总价:1,250,000.00元。

    ", + "

    七、本次报价未包含设备运输费用,运输费用由采购人另行承担。

    ", +] + if __name__ == "__main__": render("bid-mini-v1.pdf", [*COMMON, *TAIL]) render("bid-mini-v2.pdf", [*COMMON, BFD, *TAIL]) + render("bid-mini-forms.pdf", [*COMMON, BFD, *FORMS, *TAIL]) diff --git a/server/tests/test_check_flow.py b/server/tests/test_check_flow.py index 688df01..dbdef6e 100644 --- a/server/tests/test_check_flow.py +++ b/server/tests/test_check_flow.py @@ -272,3 +272,36 @@ async def test_response_modes_are_stored_editable_and_used(user: User) -> None: (f,) = await findings(user, run["id"]) assert f["type"] == "manual_review" and f["severity"] == "hint" assert run["counts"]["fatal"] == 0 + + +async def test_rule_findings_are_written_with_citations_and_carried(user: User) -> None: + """规则检查(报价形式、形式要件):问题挂规则编号和投标原文,复查时按 match_key 沿用处理状态。""" + project_id, tender_id = await project_with_tender(user) + await run_parse(user, tender_id) + bid = await upload_bid(user, project_id, "bid-mini-forms.pdf") + run = await run_check(user, project_id, [bid["id"]]) + assert run["status"] == "succeeded", run + rule = {f["rule"]["code"]: f for f in await findings(user, run["id"]) if f["detector"] == "rule"} + assert set(rule) == { + "PRICE.CAPITAL_MISMATCH", + "PRICE.MULTIPLE_QUOTES", + "FORM.BLANK_FIELD", + "FORM.CONDITION_ADDED", + } + cap = rule["PRICE.CAPITAL_MISMATCH"] + assert cap["type"] == "price_form" and cap["severity"] == "important" and cap["checklist_item"] is None + assert cap["rule"]["description"] == "大写金额和小写金额核对" + (c,) = cap["bid_citations"] # 同一页的几句合成一条出处,各句分别画框 + assert "壹拾贰万捌仟元整" in c["quote"] and "1,280,000.00" in c["quote"] and len(c["quads"]) >= 2 + assert c["document_label"] == "技术分册" + # 「让利后总价」和投标总价不同:多半另报了一个价,但不是「备选方案」那种明确的两个报价,按重要 + assert rule["PRICE.MULTIPLE_QUOTES"]["severity"] == "important" and rule["FORM.BLANK_FIELD"]["severity"] == "hint" + assert any("报价形式" in x for x in run["coverage"]["checked"]) + assert run["coverage"]["rule_set_version"] == "2026.11-r1" + + await user.post(f"/findings/{cap['id']}/resolve", json={"status": "ignored", "note": "大写栏已手工更正"}) + again = await run_check(user, project_id, [bid["id"]]) + cap2 = next( + f for f in await findings(user, again["id"]) if (f["rule"] or {}).get("code") == "PRICE.CAPITAL_MISMATCH" + ) + assert cap2["presence"] == "carried" and cap2["status"] == "ignored" diff --git a/server/tests/test_check_rules.py b/server/tests/test_check_rules.py new file mode 100644 index 0000000..8ff8479 --- /dev/null +++ b/server/tests/test_check_rules.py @@ -0,0 +1,259 @@ +"""查标的规则检查(报价形式、形式要件):纯函数,投标文件用虚构的句子拼(rules.py,GUIDELINES §3.2)。 + +措辞故意和评测集的开发词库不同:规则按招投标文件的通用写法写,不按评测集的句子写。 +""" + +from __future__ import annotations + +from datetime import date +from decimal import Decimal +from uuid import uuid4 + +from bid_doctor.check import match, rules +from bid_doctor.document.pipeline.text_layer import Sentence + + +def cell(seq: int, text: str, x: float, y: float, page: int = 1, w: float = 60) -> Sentence: + return Sentence(key=f"b:p{page}:s{seq}", page_no=page, seq=seq, text=text, quads=[[x, y, x + w, y + 12]]) + + +def para(seq: int, text: str, page: int = 1) -> Sentence: + return cell(seq, text, 74, 30 + seq * 24, page, 440) + + +def doc(sentences: list[Sentence], header: list[Sentence] | None = None) -> match.BidDoc: + return match.BidDoc(uuid4(), 1, "商务分册", sentences, [], header or []) + + +def codes(hits: list[rules.Hit]) -> list[str]: + return sorted(h.code for h in hits) + + +def test_upper_amount_and_day_counts() -> None: + assert rules.upper_amount("壹佰贰拾贰万柒仟陆佰元整") == Decimal(1_227_600) + assert rules.upper_amount("壹亿零伍佰万元零伍角") == Decimal("105000000.5") + assert rules.upper_amount("拾万元整") == Decimal(100_000) + assert rules.validity_days("投标有效期为开标之日起一百二十(120)天。") == [120] + assert rules.validity_days("本投标文件自投标截止之日起三个月内有效。") == [90] + assert rules.validity_days("投标有效期内中标的,须在30日内签订合同。") == [] # 「有效期内」后面的天数不是有效期 + + +def opening_table(lower: str, upper: str) -> list[Sentence]: + return [ + cell(1, "项目名称", 80, 100), + cell(2, "投标总价(小写,元)", 220, 100), + cell(3, "投标总价(大写)", 380, 100), + cell(4, "某市数据中心扩容项目", 80, 124), + cell(5, lower, 220, 124), + cell(6, upper, 380, 124), + ] + + +def test_capital_mismatch_and_over_ceiling() -> None: + d = doc(opening_table("2,050,000.00", "贰佰万元整")) + hits, _ = rules.run(rules.Facts(ceiling=Decimal(2_000_000)), [d], []) + assert codes(hits) == ["PRICE.CAPITAL_MISMATCH", "PRICE.OVER_CEILING_TOTAL"] + over = next(h for h in hits if h.code == "PRICE.OVER_CEILING_TOTAL") + assert over.severity == "fatal" and over.basis == "ceiling" and over.evidence[0][1].text == "2,050,000.00" + assert "建议" not in over.explanation and "应改为" not in over.explanation # 红线 6:不写数字建议 + + same = doc(opening_table("2,050,000.00", "贰佰零伍万元整")) + assert rules.run(rules.Facts(ceiling=Decimal(3_000_000)), [same], [])[0] == [] + + +def item_table(rows: list[tuple[str, str, str, str]], total: str, page: int = 1) -> list[Sentence]: + out = [ + cell(1, "序号", 80, 100, page, 20), + cell(2, "货物名称", 110, 100, page, 100), + cell(3, "数量", 260, 100, page, 30), + cell(4, "单价(元)", 320, 100, page), + cell(5, "合价(元)", 420, 100, page), + ] + for i, (name, qty, unit, line) in enumerate(rows): + y, n = 124 + i * 22, 10 + i * 5 + out += [ + cell(n, str(i + 1), 82, y, page, 8), + cell(n + 1, name, 110, y, page, 100), + cell(n + 2, qty, 272, y, page, 8), # 窄数字居中,和表头「数量」不重叠 + cell(n + 3, unit, 320, y, page), + cell(n + 4, line, 420, y, page), + ] + out.append(cell(99, f"合计:{total}元", 80, 124 + len(rows) * 22 + 6, page, 300)) + return out + + +def test_item_table_arithmetic() -> None: + ok = item_table( + [("服务器", "3", "100,000.33", "300,001.00"), ("存储", "1", "50,000.00", "50,000.00")], "350,001.00" + ) + assert rules.run(rules.Facts(), [doc(ok)], [])[0] == [] # 单价舍入到分,合价差几分钱不算错 + + bad = item_table( + [("服务器", "3", "100,000.00", "310,000.00"), ("存储", "1", "50,000.00", "50,000.00")], "350,000.00" + ) + (h,) = rules.run(rules.Facts(), [doc(bad)], [])[0] + assert h.code == "PRICE.SUM_MISMATCH" and "服务器" in h.explanation and "合计行" in h.explanation + assert "310,000.00" in [s.text for _, s in h.evidence] + + +def test_foreign_project_in_header_and_filled_fields() -> None: + facts = rules.Facts(name="某市数据中心扩容项目", code="ZC-2026-0815-A", purchaser="某市大数据局") + header = [ + Sentence( + key=f"b:p{p}:s0", + page_no=p, + seq=0, + text="某县人民医院信息化改造工程", + quads=[[0, 0, 1, 1]], + location="header_footer", + ) + for p in (1, 2) + ] + body = [ + para(1, "致:某市大数据局(采购人名称)"), + para(2, "参加ZC-2026-0815-A(项目编号)某市数据中心扩容项目(项目名称)的投标。"), + para(3, "我方承诺参加ZC-2025-0311-B(项目编号)的投标活动。"), + para(4, "类似业绩:项目名称:某省政务云平台运维服务项目,合同金额200万元。"), # 业绩不在填写位里,不查 + ] + hits, _ = rules.run(facts, [doc(body, header)], []) + assert codes(hits) == ["FORM.FOREIGN_PROJECT", "FORM.FOREIGN_PROJECT"] + head = next(h for h in hits if h.target.startswith("header:")) + assert head.detail["pages"] == [1, 2] and head.basis == "name" + assert any("ZC-2025-0311-B" in h.explanation for h in hits) + + +def test_blank_fields_and_kept_annotations() -> None: + d = doc( + [ + para(1, "投标人名称(盖单位章):某某科技有限公司"), # 括注保留、名称已填:不算 + para(2, "本授权书于年月日签署生效。"), + para(3, "被授权人:____(签字)"), + para(4, "法定代表人身份证号:/"), # 划除:不算 + ] + ) + hits, _ = rules.run(rules.Facts(), [d], []) + assert [h.evidence[0][1].seq for h in hits] == [2, 3] and {h.severity for h in hits} == {"hint"} + + +def test_sign_dates_validity_conditions_and_extra_quotes() -> None: + facts = rules.Facts(deadline=date(2026, 12, 1), validity_days=90) + d = doc( + [ + para(1, "签署日期:2026年12月3日"), + para(2, "日期:2026年11月31日"), + para(3, "证书发证日期:2027年1月1日"), # 不是落款日期 + para(4, "本投标有效期为自投标截止之日起六十日。"), + para(5, "供货数量按实际需求结算,安装调试费用由采购人负责。"), + para(6, "培训费用已包含在报价中,不另行收费。"), # 承诺,不是附加条件 + para(7, "第二套报价方案总价:1,880,000.00元"), + para(8, "我方不提交备选方案报价。"), + ] + ) + hits, _ = rules.run(facts, [d], []) + got = {(h.code, h.evidence[0][1].seq) for h in hits} + assert got == { + ("FORM.DATE_AFTER_DEADLINE", 1), + ("FORM.DATE_AFTER_DEADLINE", 2), + ("FORM.VALIDITY_SHORT", 4), + ("FORM.CONDITION_ADDED", 5), + ("PRICE.MULTIPLE_QUOTES", 7), + } + bad_date = next(h for h in hits if h.evidence[0][1].seq == 2) + assert bad_date.title == "落款日期写法有误" + + +def test_response_table_negative_and_dedupe() -> None: + rows = [ + cell(1, "实质性要求响应表", 74, 76, w=200), + cell(2, "序号", 80, 100, w=20), + cell(3, "实质性要求", 110, 100, w=150), + cell(4, "响应情况", 300, 100), + cell(5, "1", 82, 124, w=8), + cell(6, "“交货期”的响应情况", 110, 124, w=150), + cell(7, "完全响应", 300, 124), + cell(8, "2", 82, 146, w=8), + cell(9, "“付款方式”的响应情况", 110, 146, w=150), + cell(10, "部分满足", 300, 146), + cell(11, "3", 82, 168, w=8), + cell(12, "“质保期”的响应情况", 110, 168, w=150), + ] + d = doc(rows) + hits, _ = rules.run(rules.Facts(), [d], []) + assert [(h.title, h.severity) for h in hits] == [ + ("响应表里写了「部分满足」", "fatal"), + ("响应表里有一行没有填响应情况", "important"), # 「空着」可能是版面没认准:不给致命 + ] + covered = {("商务分册", 1, "部分满足")} # 响应匹配已经按清单项报了这一句 + assert [h.title for h in rules.drop_covered(hits, covered)] == ["响应表里有一行没有填响应情况"] + + +def test_missing_facts_go_to_coverage() -> None: + _, cov = rules.run(rules.Facts(), [doc([para(1, "我方承诺按招标文件要求提供服务。")])], []) + text = ";".join(cov["not_checked"]) + for want in ("没有认出开标一览表", "最高限价", "投标截止时间", "投标有效期", "项目名称"): + assert want in text + + +def boxed(seq: int, text: str, cell: list[float], dy: float = 0) -> Sentence: + """有框线表格里的句子:文字框可以比单元格上沿低(垂直居中),分行按单元格。""" + x0, top, x1, _ = cell + s = cell_sentence = Sentence( + key=f"b:p1:s{seq}", page_no=1, seq=seq, text=text, quads=[[x0 + 2, top + 2 + dy, x1 - 2, top + 14 + dy]] + ) + cell_sentence.cell = cell + return s + + +def test_real_layout_negatives() -> None: + """真实投标文件常见、但不该报的写法(Fable 审读提出的反例)。""" + # 分组小计不算进明细;金额栏叫「投标报价」的分项表不是开标一览表,几行金额不算「多个总价」 + rows = item_table([("服务器", "2", "10,000.00", "20,000.00"), ("交换机", "1", "5,000.00", "5,000.00")], "25,000.00") + rows.insert(-1, cell(90, "硬件小计", 110, 175, w=100)) + rows.insert(-1, cell(91, "25,000.00", 420, 175)) + multi = [ + cell(1, "年度", 80, 300, page=2), + cell(2, "投标报价(元)", 220, 300, page=2), + cell(3, "第一年", 80, 324, page=2), + cell(4, "100,000.00", 220, 324, page=2), + cell(5, "第二年", 80, 348, page=2), + cell(6, "120,000.00", 220, 348, page=2), + ] + # 响应表:短格垂直居中,文字比编号低半行,但在同一个单元格行里 + resp = [ + boxed(20, "序号", [80, 400, 100, 420]), + boxed(21, "符合性审查要求", [100, 400, 300, 420]), + boxed(22, "响应情况", [300, 400, 380, 420]), + boxed(23, "1", [80, 420, 100, 460]), + boxed(24, "投标人须承诺按期交货,交货期不超过合同签订后三十日", [100, 420, 300, 460]), + boxed(25, "完全响应", [300, 420, 380, 460], dy=14), + ] + header = [ + Sentence( + key="b:p1:s0", page_no=1, seq=0, text="第三章 技术服务方案", quads=[[0, 0, 1, 1]], location="header_footer" + ) + ] + body = [ + para(30, "致:贵单位"), + para(31, "附件:原厂报价单。本报价有效期30天,过期另行报价。", page=3), + para(32, "优惠后报价:25,000.00元", page=3), + para(33, "日 期:二〇二六年十一月二十日", page=3), + ] + facts = rules.Facts( + name="某市数据中心扩容项目", purchaser="某市大数据局", deadline=date(2026, 12, 1), validity_days=60 + ) + hits, _ = rules.run(facts, [doc([*rows, *multi, *resp, *body], header)], []) + assert [(h.code, h.severity) for h in hits] == [("FORM.CONDITION_ADDED", "important")] # 「过期另行报价」照报 + + +def test_validity_in_letter_table_and_chinese_dates() -> None: + page = [ + para(1, "投标函"), + cell(2, "投标有效期", 80, 200), + cell(3, "四十五天", 220, 200), + para(4, "落款日期:二〇二六年十二月三日"), + ] + hits, _ = rules.run(rules.Facts(deadline=date(2026, 12, 1), validity_days=60), [doc(page)], []) + assert {(h.code, h.severity) for h in hits} == { + ("FORM.VALIDITY_SHORT", "fatal"), + ("FORM.DATE_AFTER_DEADLINE", "hint"), + }