feat(v2): T07-c 预审运行装配与运行级集成,T07 收尾
T07 第三块。ScreeningRun 是**既有 run 对象的受约束投影**,不是第二份运行存储; AC-13/14 由既有 enqueue 实现,本批只验证其成立、不重做。 新增契约:Degradation、DimensionResult、ProbabilityView、ScreeningRun。 契约层的硬约束:`probability` 与 `probability_view.real_estimate` 的类型就是 `None`,传任何数值都被 pydantic 拒绝——"未校准就没有概率"不靠自觉; 演示估计必须标 synthetic_demo 且带"人工设定"说明、取值 0–1。 新增 services/prescreen.py: - assemble():把冻结方案、政策结论、降级与维度状态收成契约合法的运行视图 - versions_block():冻结 plan_hash 与各组件版本;题库与主审模型未接入时**留空**, 不编造版本号 - dimension_results():维度状态由本次核查结果推出 - assert_versions_frozen():未冻结方案版本或专项修订的运行不得执行 **实现中被测试抓到一个真实错误**:初版把"被引用组件未报告结果"当作已核查 (状态 checked)。这是把静默当成通过,违背 BR-07。现改为:任一被引用组件没有 结果即 unknown;issue 优先于 unknown(具体阻断比信息缺失更该被看到)。 测试:新增 test_prescreen_run.py 20 项,其中 4 项直接验证既有 enqueue 的 AC-13(新证据→新快照+新运行、旧运行留存、旧 revision 冲突)与 AC-14(同键同载荷 返回同一运行、异载荷 409)。全量 410 项 OK(390 → 410);verify.py 10/10 PASS。 T07 三块完成,卡片验收对照:AC-05/06/07/12/45(T07-a)· AC-09/10/21(T07-b)· AC-13/14 验证(T07-c)。AC-08 的跨项目引用拒绝已在 T03 覆盖。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -755,3 +755,86 @@ class JudgmentConfig(Contract):
|
||||
if not self.disabled_reason:
|
||||
raise ValueError("关闭专项必须记录关闭原因,不能静默")
|
||||
return self
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 预审运行(T07-c)
|
||||
class Degradation(Contract):
|
||||
"""降级记录:未完成的检查必须留痕,不能静默(BR-09)。"""
|
||||
|
||||
code: ErrorCode
|
||||
message: str = Field(min_length=1, max_length=DESC_MAX)
|
||||
|
||||
|
||||
class DimensionResult(Contract):
|
||||
"""维度在本次运行中的状态。`unknown` 不等于不成立(03 §9.1)。"""
|
||||
|
||||
id: Identifier
|
||||
stage: int | None = None
|
||||
state: Literal["unknown", "checked", "blocked"]
|
||||
basis_ids: list[Identifier] = Field(default_factory=list, max_length=EVIDENCE_MAX)
|
||||
|
||||
|
||||
class ProbabilityView(Contract):
|
||||
"""概率呈现。P0 的 real_estimate **必须为空**,演示值只能走 demo_estimate(BR-29)。"""
|
||||
|
||||
target_event: Literal["company_review_meeting_pass"] = "company_review_meeting_pass"
|
||||
snapshot_id: Identifier
|
||||
as_of: AwareDatetime
|
||||
real_estimate: None = None
|
||||
reason: Literal["not_calibrated"] = "not_calibrated"
|
||||
demo_estimate: dict | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_demo_isolated(self) -> ProbabilityView:
|
||||
demo = self.demo_estimate
|
||||
if demo is None:
|
||||
return self
|
||||
if demo.get("source") != "synthetic_demo":
|
||||
raise ValueError("演示估计必须标明来源 synthetic_demo")
|
||||
if not demo.get("label"):
|
||||
raise ValueError("演示估计必须带固定的『人工设定』说明,避免被当作真实概率")
|
||||
value = demo.get("value")
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool) or not 0 <= value <= 1:
|
||||
raise ValueError("演示估计须为 0–1 的数值")
|
||||
return self
|
||||
|
||||
|
||||
class ScreeningRun(Contract):
|
||||
"""预审运行视图(docs/v2/03-detailed-design.md §9.2 + §9.3 增量)。
|
||||
|
||||
这是既有 run 对象的**受约束投影**,不是第二份运行存储:plan_ref、snapshot、
|
||||
judgment_config 与 versions 在创建时冻结(BR-13 / BR-20)。
|
||||
"""
|
||||
|
||||
id: Identifier
|
||||
project_id: Identifier
|
||||
data_domain: DataDomain
|
||||
revision: int = Field(ge=1)
|
||||
snapshot_id: Identifier
|
||||
plan_ref: Ref
|
||||
status: Literal["queued", "running", "awaiting_input", "completed", "partial", "failed", "cancelled"]
|
||||
recommendation: Literal["advance", "supplement", "stop", "insufficient_information"] | None = None
|
||||
findings: list[dict] = Field(default_factory=list, max_length=500)
|
||||
missing_inputs: list[str] = Field(default_factory=list, max_length=200)
|
||||
applied_experiences: list[dict] = Field(default_factory=list, max_length=200)
|
||||
# 未校准时恒为空——契约层直接禁止填入数值
|
||||
probability: None = None
|
||||
probability_reason: Literal["not_calibrated"] = "not_calibrated"
|
||||
probability_view: ProbabilityView | None = None
|
||||
degradations: list[Degradation] = Field(default_factory=list, max_length=100)
|
||||
judgment_config: JudgmentConfig
|
||||
versions: dict = Field(default_factory=dict)
|
||||
dimension_results: list[DimensionResult] = Field(default_factory=list, max_length=32)
|
||||
run_instruction: str = Field(default="", max_length=2000)
|
||||
created_at: AwareDatetime
|
||||
finished_at: AwareDatetime | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_run(self) -> ScreeningRun:
|
||||
if self.status in ("queued", "running") and self.recommendation is not None:
|
||||
raise ValueError("运行未结束时不得给出建议")
|
||||
if self.status == "failed" and not self.degradations:
|
||||
raise ValueError("failed 运行必须有降级记录,不能只留空结果")
|
||||
if self.probability_view and self.status in ("queued", "running"):
|
||||
raise ValueError("运行未结束时不得产出概率视图")
|
||||
return self
|
||||
|
||||
Reference in New Issue
Block a user