"""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 pathlib import Path 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 ) def load_dimension(path: str | Path) -> Dimension: """Fail-fast standalone loader for a dimension scope config (mirrors ``load_goal_config``). A dimension config is *authoritative startup input*, so loading is fail-fast: a missing file raises ``FileNotFoundError`` and malformed/invalid content raises ``pydantic.ValidationError``. This is the deliberate contrast to the tolerant verdict-inbox RAW layer (``load_verdicts_from_dir``), which skips bad files rather than raising — startup scope must never be silently degraded. Stdlib + ``pydantic`` only: ``dimension.py`` is in ``_MAF_FREE_MODULES`` and the AST guard (``tests/test_okf.py::test_okf_is_maf_free``) fails on any ``agent_framework``/ ``mcp`` import here. :raises FileNotFoundError: ``path`` does not point at an existing file. :raises pydantic.ValidationError: the JSON is malformed or violates the ``Dimension`` schema. """ p = Path(path) if not p.is_file(): raise FileNotFoundError(f"dimension config not found: {str(path)!r}") return Dimension.model_validate_json(p.read_text(encoding="utf-8"))