New load_dimension(str | Path) in dimension.py: is_file() -> FileNotFoundError, then Dimension.model_validate_json -> pydantic.ValidationError on malformed shape. Fail-fast because dimension config is authoritative startup input (contrast the tolerant verdict-inbox RAW layer). Stdlib + pydantic only — stays MAF-free (test_okf_is_maf_free AST guard green). Exported from __init__.py in both the import block and __all__. RED-first: 3 loader tests failed on the missing import, green after. tests/test_dimension.py + test_okf.py: 29 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNNiJRk1sSwxgVLS5AobT1
68 lines
2.8 KiB
Python
68 lines
2.8 KiB
Python
"""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"))
|