feat(v2): T07-a 预审结果约束与结论校验
T07 是"预审执行"大卡,按纪律拆子卡。本批为第一块:纯确定性的结论校验, 不调用任何模型、不计算概率。覆盖 AC-05/06/07/09/12/45 与 BR-05/06/07/12。 新增 services/prescreen_policy.py: - decide():四级优先(hard_block → critical_incomplete → open_findings → clean), 优先级顺序即业务口径,改动等于改结论,已显式标注 - **硬性阻断不可覆盖**:命中 hard 规则即 stop,并指出规则版本、保留模型原始 结构化提案供审计;专项模型的正面单题判断不能翻盘(有专门反例) - **未知不等于风险也不等于通过**:关键核查不可完成给 insufficient_information 并列出未完成项,findings 的 result 原样保留、**不得被改写成 issue** - **没有 second path 给概率**:probability() 不接受任何入参,模型分数无法顺手传入; native_metrics 里名为 probability 的指标会被 assert_no_probability_leak 拦下, self_reported 置信度可入档但不得变概率 - stop 明确不触碰正式审批(AC-45),预算耗尽按 E-08 列未完成项而非伪造完成 实现中修正了两处自己的测试写法:断言"不含高风险三字"会被免责说明本身命中; 断言 open_findings 为空与"unknown 属待处理"的定义矛盾。两处都改成检验真正的 不变量(结论非风险判定 + findings 不被改写)。 测试:新增 test_prescreen_policy.py 19 项。全量 368 项 OK(349 → 368); verify.py 10/10 PASS。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""T07-a 预审结果约束:把核查结果收敛成受约束的建议。
|
||||
|
||||
依据:docs/v2/01-requirements.md §5、03-detailed-design.md §5;
|
||||
BR-05/06/07/12、E-02/06/07/08、AC-05/06/07/09/12/45。
|
||||
|
||||
本模块只做**确定性结论校验**,不调用任何模型、不算概率。四条不可让渡的规则:
|
||||
|
||||
1. **硬性阻断不可被覆盖**(BR-06):已确认的 hard 规则命中即 `stop`,模型的高分
|
||||
或"建议推进"都不能翻盘。同时**保留模型原始结构化提案**供审计,并指出规则版本。
|
||||
2. **未知不等于风险,也不等于通过**(BR-07):关键核查不可完成时给
|
||||
`insufficient_information` 并列补件,**不得**据此评判企业高风险。
|
||||
3. **单题不能代表综合意见**(BR-05):专项判断只进 findings 与被引用,不能直接
|
||||
写入 recommendation。
|
||||
4. **未校准就没有概率**(BR-12):`probability` 恒为 None + `not_calibrated`,
|
||||
任何模型自报置信度都不改写它。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
RECOMMENDATIONS = ("advance", "supplement", "stop", "insufficient_information")
|
||||
|
||||
NOT_CALIBRATED = "not_calibrated"
|
||||
NO_PROBABILITY_REASON = "尚未完成真实历史校准;不展示审批通过概率"
|
||||
|
||||
# 判定优先级(从上到下,先命中先返回)。顺序本身是业务口径,改动等于改结论。
|
||||
PRECEDENCE = ("hard_block", "critical_incomplete", "open_findings", "clean")
|
||||
|
||||
INCOMPLETE_RESULTS = ("unknown", "error", "not_applicable")
|
||||
|
||||
|
||||
def _codes(findings) -> set[str]:
|
||||
return {str(item.get("code")) for item in findings or () if item.get("code")}
|
||||
|
||||
|
||||
def hard_blocks(findings, hard_rule_codes) -> list[dict]:
|
||||
"""命中的硬性阻断项。`hard_rule_codes` 来自冻结方案里 effect=hard 且 on_match=block 的规则。"""
|
||||
wanted = set(hard_rule_codes or ())
|
||||
return [dict(item) for item in findings or () if item.get("code") in wanted]
|
||||
|
||||
|
||||
def critical_incomplete(findings, critical_check_ids) -> list[str]:
|
||||
"""关键核查项中未完成的那些编码(未知/执行失败/不适用)。"""
|
||||
critical = set(critical_check_ids or ())
|
||||
return sorted({
|
||||
str(item.get("code")) for item in findings or ()
|
||||
if item.get("code") in critical and item.get("result") in INCOMPLETE_RESULTS
|
||||
})
|
||||
|
||||
|
||||
def open_findings(findings) -> list[dict]:
|
||||
"""需要业务处理但未阻断的问题(issue / unknown)。"""
|
||||
return [dict(item) for item in findings or ()
|
||||
if item.get("result") in ("issue", "unknown")]
|
||||
|
||||
|
||||
def probability() -> dict:
|
||||
"""P0 恒为空。**不接受任何入参**——不给"顺手传个模型分数"留口子。"""
|
||||
return {"value": None, "reason": NOT_CALIBRATED, "message": NO_PROBABILITY_REASON}
|
||||
|
||||
|
||||
def decide(findings, missing_inputs=(), degradations=(), judgments=(),
|
||||
critical_check_ids=(), hard_rule_codes=(), rule_versions=()) -> dict:
|
||||
"""收敛结论。返回 recommendation 与支撑它的理由、降级与审计信息。"""
|
||||
blocks = hard_blocks(findings, hard_rule_codes)
|
||||
incomplete = critical_incomplete(findings, critical_check_ids)
|
||||
open_items = open_findings(findings)
|
||||
|
||||
degradations = [dict(item) for item in degradations or ()]
|
||||
missing = sorted({str(item) for item in missing_inputs or ()})
|
||||
|
||||
if blocks:
|
||||
# 命中硬阻断:即便专项模型给出正面单题判断,也不改变结论(BR-05 / BR-06)
|
||||
stage = "hard_block"
|
||||
recommendation = "stop"
|
||||
reason = "存在已确认的硬性阻断,暂不建议推进;" + "、".join(
|
||||
f'{item.get("code")}@{item.get("basis_version", "未标注版本")}' for item in blocks)
|
||||
elif incomplete:
|
||||
stage = "critical_incomplete"
|
||||
recommendation = "insufficient_information"
|
||||
reason = ("关键核查未能完成,无法形成结论,请补充材料后重新发起;"
|
||||
"不得据此判定企业高风险。未完成项:" + "、".join(incomplete))
|
||||
missing = sorted(set(missing) | set(incomplete))
|
||||
elif open_items:
|
||||
stage = "open_findings"
|
||||
recommendation = "supplement"
|
||||
reason = f"存在 {len(open_items)} 项待处理事项,建议补充材料或说明后重新核查"
|
||||
else:
|
||||
stage = "clean"
|
||||
recommendation = "advance"
|
||||
reason = "已完成方案内的确定性核查,未发现阻断项;正式审批仍由业务流程决定"
|
||||
|
||||
return {
|
||||
"recommendation": recommendation,
|
||||
"recommendation_reason": reason,
|
||||
"stage": stage,
|
||||
"findings": [dict(item) for item in findings or ()],
|
||||
"open_findings": open_items,
|
||||
"hard_blocks": blocks,
|
||||
"critical_incomplete": incomplete,
|
||||
"missing_inputs": missing,
|
||||
"degradations": degradations,
|
||||
# 专项判断只作为依据入档,**不参与** recommendation 的判定
|
||||
"judgments": [dict(item) for item in judgments or ()],
|
||||
"judgment_can_override": False,
|
||||
"rule_versions": [dict(item) for item in rule_versions or ()],
|
||||
"probability": probability(),
|
||||
# AC-45:stop 只是"暂不建议推进",系统不得自动关闭项目或执行正式审批
|
||||
"formal_approval_touched": False,
|
||||
}
|
||||
|
||||
|
||||
def assert_no_probability_leak(result: dict) -> None:
|
||||
"""任何路径都不得产出未校准概率(BR-12 / AC-12)。"""
|
||||
value = (result.get("probability") or {}).get("value")
|
||||
if value is not None:
|
||||
raise ValueError("未校准的数据集不得产出审批通过概率")
|
||||
for item in result.get("judgments", ()):
|
||||
for metric in item.get("native_metrics", ()) or ():
|
||||
if str(metric.get("name", "")).lower() in ("probability", "approval_probability"):
|
||||
raise ValueError("专项模型的原生指标不得被当作审批概率")
|
||||
|
||||
|
||||
def budget_exhausted(reason: str = "RUN_BUDGET_EXCEEDED") -> dict:
|
||||
"""预算耗尽(E-08):停止并列出未完成项,不伪造完成。"""
|
||||
return {"code": reason, "message": "本次核查已达到执行上限,请复核未完成项",
|
||||
"retryable": False}
|
||||
Reference in New Issue
Block a user