"""Step 9 load-bearing seam (SC7): assessment-method encoding — a PERSISTED dimension-scoped example (Arm A) + an INDEPENDENT deterministic validator rule (Arm B). Arm A (navigable, dimension-scoped): the persisted, MAF-repo-local method bundle (``data/method_examples/energi/``) carries a ``type: methodology`` file with ``dimension: energi``. Its sentinel reaches ``bundle_context(..., dimension="energi")`` but is ABSENT at ``dimension="asfalt"`` — the detach is the Step-3 dimension filter (H4: "text is rendered" is not a detachable seam on its own; the dimension SCOPING is). Arm B (independent validator rule): a proposal the generic P90 stage PASSES but the stricter energy- method cap REJECTS — proving the method rule is an INDEPENDENT gate (not redundant with P90). Detach the rule (or flip the measure) and the same proposal validates. The rule returns the validator's own ``Rejection`` type, so the validator stays the sole blocking gate and provenance stays honest (the numbers only). Patterns: ``test_okf.py:141/51`` (navigable + bundle_context), ``test_validator.py:49`` (rule). """ from __future__ import annotations from pathlib import Path from portfolio_optimiser import okf from portfolio_optimiser.ir import AffectedItem, SavingsProposal from portfolio_optimiser.validator import ( Rejection, ValidatedProposal, validate_proposal, ) METHOD_BUNDLE = ( Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "data" / "method_examples" / "energi" ) _METHOD_SENTINEL = "ENERGI-METODE-IPMVP-A-SENTINEL" # --- Arm A: navigable, dimension-scoped persisted example ---------------------------------------- def test_persisted_method_navigable_when_dimension_matches() -> None: """The persisted energi method (``dimension: energi``) is navigable + rendered when the run is scoped to energi — an actually-delivered, persisted '>=1 encoded method' (not an ephemeral copy).""" bundle = okf.navigate_bundle(str(METHOD_BUNDLE)) scoped = okf.bundle_context(bundle, dimension="energi") assert _METHOD_SENTINEL in scoped def test_persisted_method_absent_for_other_dimension() -> None: """LOAD-BEARING (SC7-A): the energi method is ABSENT when the run is scoped to another dimension. RED if the Step-3 dimension filter is detached (the energi method then leaks into an asfalt run).""" bundle = okf.navigate_bundle(str(METHOD_BUNDLE)) other = okf.bundle_context(bundle, dimension="asfalt") assert _METHOD_SENTINEL not in other # --- Arm B: independent deterministic validator rule --------------------------------------------- def _proposal(measure: str, claimed: float) -> SavingsProposal: """Affected total 100000 (degenerate MC, empty assumptions) -> generic P90 = 0.30 x 100000 = 30000; the energy-method cap = 0.15 x 100000 = 15000.""" return SavingsProposal( project_id="P-ENERGI", measure=measure, affected_items=[AffectedItem(code="ENERGI-TOTAL-EL", quantity=100000, unit_cost=1.0)], claimed_saving_nok=claimed, assumptions={}, ) def test_method_rule_is_independent_of_generic_p90() -> None: """LOAD-BEARING (SC7-B): claimed 20000 is <= generic P90 (30000) but > the energy-method cap (15000). A NON-energy proposal with the same numbers validates (P90 passes); the energy-method proposal is REJECTED by the stricter method rule — an independent flip. RED if the method rule is detached (the energy proposal then validates too).""" # Control: same numbers, non-energy measure -> the generic P90 stage validates it. control = validate_proposal(_proposal("scope_reduction", 20000)) assert isinstance(control, ValidatedProposal), ( "the generic P90 stage should pass 20000 <= 30000" ) # Energy method: the stricter method cap flips the SAME numbers to a Rejection. result = validate_proposal(_proposal("energy_efficiency", 20000)) assert isinstance(result, Rejection), "the energy-method cap must reject a P90-valid over-claim" assert "method cap" in result.reason # The rule returns the validator's OWN Rejection type -> the validator stays the sole blocking # gate (numbers only); it never introduces a new gate type. def test_method_rule_admits_within_the_stricter_cap() -> None: """Causality control: an energy proposal WITHIN the method cap (claimed 15000 == 0.15 x 100000) validates — proving the rejection above is caused by exceeding the cap, not the measure string.""" result = validate_proposal(_proposal("energy_efficiency", 15000)) assert isinstance(result, ValidatedProposal)