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