feat(s36): MAF-free costsim skeleton + model-map reader + registration
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
This commit is contained in:
parent
620d5cfb83
commit
47147e5f7c
3 changed files with 92 additions and 1 deletions
46
src/portfolio_optimiser/costsim.py
Normal file
46
src/portfolio_optimiser/costsim.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""S3.6 — kostnadssimulering før kjøring (D-I pkt. 3): offline, MAF-free cost estimator.
|
||||
|
||||
Estimates token/cost for a portfolio run BEFORE running — a what-if over the model-map
|
||||
(models × effort levels) — so run cost becomes an informed decision, not a surprise afterwards
|
||||
(revisjonspakke §0.5 F-INT-5: kjøringskost er en CFO-beslutning).
|
||||
|
||||
MAF-free by construction (D7-portable; målbilde context/output-layer rule): this module imports
|
||||
ONLY stdlib + pydantic — never ``agent_framework``/``mcp`` nor the MAF-bound framework modules
|
||||
(``backends``/``budget``/``contracts``/``run``). It reads ``data/model_map.json`` as PLAIN DATA via a
|
||||
``__file__``-relative path, deliberately NOT ``importlib.resources.files("portfolio_optimiser")``
|
||||
(which would import the MAF-bound package ``__init__`` and defeat MAF-freedom). Registered in
|
||||
``tests/test_okf.py``'s ``_MAF_FREE_MODULES`` and enforced by ``test_okf_is_maf_free`` (direct-import
|
||||
AST scan) + ``test_costsim_import_is_maf_free`` (transitive import-graph subprocess seam).
|
||||
|
||||
All money math is INTEGER øre — float NOK is non-deterministic under summation (see ``ledger.py``);
|
||||
kr is formatted only at the display edge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
_DATA_DIR = Path(__file__).resolve().parent / "data"
|
||||
_MODEL_MAP_FILE = _DATA_DIR / "model_map.json"
|
||||
|
||||
|
||||
def _load_model_map(
|
||||
profile: str, model_map: dict[str, dict[str, str]] | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Return the ``role -> model`` sub-map for ``profile`` from ``data/model_map.json``.
|
||||
|
||||
Reads the packaged JSON as plain data via a ``__file__``-relative path — deliberately NOT
|
||||
``importlib.resources.files("portfolio_optimiser")``, which would import the MAF-bound package
|
||||
``__init__`` and defeat the MAF-free guarantee. Tests inject ``model_map`` to avoid touching
|
||||
shipped config. Fail-fast (``ValueError``) when the profile is absent or malformed.
|
||||
"""
|
||||
table = (
|
||||
model_map
|
||||
if model_map is not None
|
||||
else json.loads(_MODEL_MAP_FILE.read_text(encoding="utf-8"))
|
||||
)
|
||||
entry = table.get(profile)
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError(f"unknown profile {profile!r} in model map")
|
||||
return dict(entry)
|
||||
45
tests/test_costsim_loadbearing.py
Normal file
45
tests/test_costsim_loadbearing.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""S3.6 costsim (kostnadssimulering) — offline, MAF-free, deterministic load-bearing seams.
|
||||
|
||||
``costsim`` estimates token/cost for a portfolio run BEFORE running (what-if over the model-map ×
|
||||
effort levels), with pricing as schema-validated config. It is MAF-free (reads ``model_map.json`` as
|
||||
plain data via ``__file__``-relative path, never imports ``backends``/``budget``/``contracts``) —
|
||||
proven load-bearing by BOTH the direct-import AST guard (``test_okf_is_maf_free`` covering
|
||||
``costsim.py``) AND the transitive import-graph seam below.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_COSTSIM = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "costsim.py"
|
||||
|
||||
|
||||
def test_costsim_registered_maf_free() -> None:
|
||||
"""Meta: costsim.py is registered in the MAF-free guard list, so ``test_okf_is_maf_free``
|
||||
actually scans it — otherwise the MAF-free claim would be green-but-dead (never checked)."""
|
||||
from tests.test_okf import _MAF_FREE_MODULES
|
||||
|
||||
assert "costsim.py" in _MAF_FREE_MODULES
|
||||
|
||||
|
||||
def test_costsim_import_is_maf_free() -> None:
|
||||
"""The genuine transitive-import seam: loading costsim.py's module body in ISOLATION must NOT
|
||||
pull ``agent_framework``/``mcp`` into ``sys.modules``. Loaded standalone via
|
||||
``spec_from_file_location`` — NOT as ``portfolio_optimiser.costsim``, since the package
|
||||
``__init__`` imports MAF-bound ``run``/``contracts`` and would mask costsim's own cleanliness.
|
||||
The AST guard (``test_okf_is_maf_free``) only catches DIRECT imports; a transitive
|
||||
``from portfolio_optimiser.contracts import ...`` in costsim would slip past it but trip THIS
|
||||
check — ``exec_module`` runs costsim's import statements, so any MAF-bound import lands
|
||||
``agent_framework`` in ``sys.modules``. Detach point: add such an import → RED."""
|
||||
check = (
|
||||
"import importlib.util, sys\n"
|
||||
f"spec = importlib.util.spec_from_file_location('costsim_standalone', {str(_COSTSIM)!r})\n"
|
||||
"mod = importlib.util.module_from_spec(spec)\n"
|
||||
"spec.loader.exec_module(mod)\n"
|
||||
"assert 'agent_framework' not in sys.modules, 'agent_framework leaked into costsim import graph'\n"
|
||||
"assert 'mcp' not in sys.modules, 'mcp leaked into costsim import graph'\n"
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", check], capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
|
@ -18,7 +18,7 @@ from portfolio_optimiser import okf
|
|||
|
||||
# Framework-neutral, D7-portable modules that must never import MAF/mcp (C2:
|
||||
# the guard previously scanned only okf.py; dimension.py is now covered too).
|
||||
_MAF_FREE_MODULES = ["okf.py", "dimension.py", "outbox.py"]
|
||||
_MAF_FREE_MODULES = ["okf.py", "dimension.py", "outbox.py", "costsim.py"]
|
||||
|
||||
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue