"""Typed Pydantic IR for a candidate cost-saving measure (B1). Pure module — **no** ``agent_framework`` and no solver. The two structural invariants (non-negative quantities; a claimed saving may not exceed the affected items' own total) are enforced at construction by Pydantic, so a malformed proposal can never be built. This IR is the D7-portable contract both the deterministic validator and the LLM->IR generator speak. """ from __future__ import annotations from pydantic import BaseModel, Field, model_validator class AffectedItem(BaseModel): """One project cost line a proposal claims to save against.""" code: str quantity: float = Field(ge=0) # quantities must be >= 0 unit_cost: float = Field(gt=0) @property def total(self) -> float: return self.quantity * self.unit_cost class SavingsProposal(BaseModel): """Typed IR for a candidate cost-saving measure (B1).""" project_id: str measure: str affected_items: list[AffectedItem] = Field(min_length=1) claimed_saving_nok: float = Field(gt=0) # code -> (low_unit_cost, high_unit_cost) for the Monte Carlo step; empty = degenerate. assumptions: dict[str, tuple[float, float]] = Field(default_factory=dict) @model_validator(mode="after") def _claim_within_affected_total(self) -> SavingsProposal: total = sum(item.total for item in self.affected_items) if self.claimed_saving_nok > total: raise ValueError( f"claimed saving {self.claimed_saving_nok} exceeds affected items' total {total}" ) return self