feat(check): 需人工核对(P2)和分项 / 单价上限(C7)

- 需人工核对:份数、密封、递交、CA 签章加密、解密、保证金这类文件里看不出来的要求,
  从正式清单按主题合并、带招标原文出处;manual-checks 接口按项目存勾选、写勾选人和时间,
  复查沿用;查标页加「需人工核对」一块,覆盖范围写明列了几条
- 「报价」类清单项:读标从采购需求的清单表规则读出分项 / 单价上限(checklist/price.py);
  查标交给规则 PRICE.OVER_CEILING_ITEM 比对分项报价表,不交给模型;规则集 2026.11-r2
- 版面工具(分页、分行、归栏)挪到 document/pipeline/layout.py,读标和查标共用
- 评测:price_defects.py 从规矩投标文件派生只带报价问题的 4 份(P01、P02、P03)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 17:31:32 +08:00
parent db1b1f6f9f
commit 81c58798d2
22 changed files with 888 additions and 84 deletions

View File

@@ -297,7 +297,7 @@ async def test_rule_findings_are_written_with_citations_and_carried(user: User)
# 「让利后总价」和投标总价不同:多半另报了一个价,但不是「备选方案」那种明确的两个报价,按重要
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"
assert run["coverage"]["rule_set_version"] == "2026.11-r2"
await user.post(f"/findings/{cap['id']}/resolve", json={"status": "ignored", "note": "大写栏已手工更正"})
again = await run_check(user, project_id, [bid["id"]])
@@ -305,3 +305,37 @@ async def test_rule_findings_are_written_with_citations_and_carried(user: User)
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"
async def test_manual_checks_are_kept_per_project(user: User) -> None:
"""需人工核对:来自清单(按主题合并),勾选按项目存、写勾选人和时间,复查后还在(决策 P2)。"""
project_id, _, _ = await checked_project(user)
for title in ("须用CA数字证书加密投标文件", "开标后30分钟内在线解密投标文件", "本次采购不收取投标保证金"):
r = await user.post(
f"/projects/{project_id}/checklist/items", json={"categories": ["schedule"], "title": title}
)
assert r.status_code == 201, r.text
items = (await user.get(f"/projects/{project_id}/manual-checks")).json()["items"]
assert [(i["check_key"], i["checked"]) for i in items] == [
("rule:MANUAL.DECRYPT", False),
("rule:MANUAL.SIGN_ENCRYPT", False),
]
assert items[1]["requirement_text"] == "须用CA数字证书加密投标文件"
r = await user.put(
f"/projects/{project_id}/manual-checks",
json={"check_key": "rule:MANUAL.DECRYPT", "checked": True, "note": "已演练"},
)
assert r.status_code == 200, r.text
m = r.json()
assert m["checked"] and m["note"] == "已演练" and m["checked_by"]["name"] and m["checked_at"]
bad = await user.put(
f"/projects/{project_id}/manual-checks", json={"check_key": "rule:MANUAL.NOPE", "checked": True}
)
assert bad.status_code == 422
bid_id = (await user.get(f"/projects/{project_id}/check-preview")).json()["bid_documents"][0]["id"]
again = await run_check(user, project_id, [bid_id])
assert any("列入「需人工核对」2 条" in x for x in again["coverage"]["not_checked"])
kept = (await user.get(f"/projects/{project_id}/manual-checks")).json()["items"]
assert [i["checked"] for i in kept] == [True, False]