feat(fase1): dimension IR + admits, MAF-free guard extended (F1)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-07 07:42:07 +02:00
commit a44256a994
3 changed files with 135 additions and 8 deletions

View file

@ -0,0 +1,44 @@
"""Typed IR for a cost-reduction *dimension* + the ``admits`` scoping gate (Fase 1, F1).
Pure module imports **only** ``pydantic`` + stdlib. No ``agent_framework`` and no
``verdicts`` (so ``ProposalFeatures`` is never referenced here); ``admits`` takes
primitives. This keeps the module D7-portable and MAF-free, enforced by
``tests/test_okf.py::test_okf_is_maf_free`` which AST-scans this file alongside
``okf.py``.
A dimension is one cost axis a project is reduced along (energy, paving, ...).
``admits`` decides whether a candidate measure belongs to a dimension: its
``measure_type`` must be allowed, and when the dimension constrains cost codes
at least one affected code must match an allowed prefix.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
class Dimension(BaseModel):
"""One cost-reduction axis a project's candidate measures are scoped to."""
id: str
label: str = Field(min_length=1)
allowed_measure_types: frozenset[str]
allowed_code_prefixes: frozenset[str] = frozenset()
def admits(*, measure_type: str, codes: frozenset[str], dimension: Dimension) -> bool:
"""True iff a candidate measure belongs to ``dimension``.
Two conjoined conditions:
- ``measure_type`` is in ``dimension.allowed_measure_types``; **and**
- the dimension imposes no code constraint (``allowed_code_prefixes`` empty),
**or** at least one of ``codes`` starts with one of the allowed prefixes.
"""
if measure_type not in dimension.allowed_measure_types:
return False
if not dimension.allowed_code_prefixes:
return True
return any(
code.startswith(prefix) for code in codes for prefix in dimension.allowed_code_prefixes
)

76
tests/test_dimension.py Normal file
View file

@ -0,0 +1,76 @@
"""Unit tests for the dimension IR + ``admits`` scoping gate (Fase 1, Step 1, SC1).
``admits`` must accept an in-dimension candidate (``measure_type`` in the allowed
set) and reject an out-of-dimension one, exercising BOTH branches of the
code-prefix logic empty prefixes (measure_type-only) and non-empty prefixes
where a matching vs non-matching code decides the outcome. A ``Dimension`` with an
empty ``label`` must fail construction (Pydantic ``ValidationError``).
Model test: ``tests/test_validator.py`` (rejection assertions).
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from portfolio_optimiser.dimension import Dimension, admits
def _energy_dim(*, prefixes: frozenset[str] = frozenset()) -> Dimension:
return Dimension(
id="energi",
label="Energi",
allowed_measure_types=frozenset({"energy_efficiency"}),
allowed_code_prefixes=prefixes,
)
def test_admits_in_dimension_measure_type_only() -> None:
"""Empty ``allowed_code_prefixes``: only the measure_type gate applies."""
dim = _energy_dim()
assert (
admits(measure_type="energy_efficiency", codes=frozenset({"07.1"}), dimension=dim) is True
)
def test_admits_rejects_out_of_dimension_measure_type() -> None:
"""Out-of-dimension measure_type is rejected regardless of codes."""
dim = _energy_dim()
assert admits(measure_type="paving", codes=frozenset({"07.1"}), dimension=dim) is False
def test_admits_prefix_branch_matching_code() -> None:
"""Non-empty prefixes: a code matching a prefix is admitted."""
dim = _energy_dim(prefixes=frozenset({"07"}))
assert (
admits(
measure_type="energy_efficiency",
codes=frozenset({"07.1", "99.9"}),
dimension=dim,
)
is True
)
def test_admits_prefix_branch_no_matching_code() -> None:
"""Non-empty prefixes: no code matches any prefix -> rejected even when measure_type is allowed."""
dim = _energy_dim(prefixes=frozenset({"07"}))
assert (
admits(
measure_type="energy_efficiency",
codes=frozenset({"99.9"}),
dimension=dim,
)
is False
)
def test_dimension_empty_label_raises() -> None:
"""An empty ``label`` violates ``Field(min_length=1)`` -> ValidationError at construction."""
with pytest.raises(ValidationError):
Dimension(
id="energi",
label="",
allowed_measure_types=frozenset({"energy_efficiency"}),
)

View file

@ -9,10 +9,17 @@ No ``agent_framework``/``mcp`` import is allowed in ``okf`` — guarded by ``tes
from __future__ import annotations
import ast
from pathlib import Path
import pytest
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"]
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
@ -162,14 +169,14 @@ def test_link_in_index_is_idempotent(tmp_path) -> None:
assert body.count("(promoted-verdict-x.md)") == 1
def test_okf_is_maf_free() -> None:
"""D7 portability: ``okf.py`` IMPORTS no ``agent_framework`` / ``mcp`` (the docstring may name
them to document the constraint, exactly as ``retrieval.py`` does) checked via the AST, not a
raw substring, so the prose claim doesn't trip the guard."""
import ast
@pytest.mark.parametrize("module_name", _MAF_FREE_MODULES)
def test_okf_is_maf_free(module_name: str) -> None:
"""D7 portability: each framework-neutral module IMPORTS no ``agent_framework`` / ``mcp`` (a
docstring may name them to document the constraint, exactly as ``retrieval.py`` does) checked
via the AST, not a raw substring, so the prose claim doesn't trip the guard. Parametrized over
``_MAF_FREE_MODULES`` so ``dimension.py`` is guarded alongside ``okf.py`` (C2)."""
src = (
Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "okf.py"
Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / module_name
).read_text(encoding="utf-8")
imported: list[str] = []
for node in ast.walk(ast.parse(src)):
@ -178,4 +185,4 @@ def test_okf_is_maf_free() -> None:
elif isinstance(node, ast.ImportFrom):
imported.append(node.module or "")
forbidden = [m for m in imported if m.split(".")[0] in {"agent_framework", "mcp"}]
assert forbidden == [], f"okf.py must not import MAF/mcp, found: {forbidden}"
assert forbidden == [], f"{module_name} must not import MAF/mcp, found: {forbidden}"