The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
4.6 KiB
Python
100 lines
4.6 KiB
Python
"""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="lisens"`` — 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 a lisens
|
|
run)."""
|
|
bundle = okf.navigate_bundle(str(METHOD_BUNDLE))
|
|
other = okf.bundle_context(bundle, dimension="lisens")
|
|
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)
|