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:
YANG JIANKUAN
2026-11-04 16:44:22 +08:00
parent 47c6a1258e
commit db1b1f6f9f
17 changed files with 1792 additions and 20 deletions

View File

@@ -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"