"""Multi-year-renta modelo authorization manifest, types, and derivation.
This module owns the type spine of the multi-year-renta authorization
gate decided in the ``modelo-multiyear-renta`` ADR. The gate's governing
principle is **default-deny-by-absence**: a modelo's calculation backend
is treated as NON-FUNCTIONAL until its authorization is *derived* from the
declarative manifest and *verified* by an enrolling end-to-end persona
test that drove the real backend across at least two distinct renta
(annual) periods.
Four collaborating pieces live here:
- :class:`AuthorizationState` and :class:`EnrollmentEvidenceClass` — the
closed value sets, declared as :class:`enum.StrEnum` per the
core-authority ADR (``core/`` owns closed enums).
- :class:`ModeloAuthorizationEntry` — the strict pydantic record modelling
one manifest entry. The ``>=2 distinct renta years`` invariant is
enforced at the type boundary so a malformed (single-year) enrollment
claim cannot construct.
- :class:`AuthorizationManifest` — the parsed, validated view of the
directory-mode manifest (``authorization.d/<modelo>.toml`` fragments,
one per enrolled modelo, merged by the loader). Default-deny is literal:
an absent directory or one with no fragments authorizes zero modelos.
- :class:`ModeloAuthorization` — the *derived* per-modelo capability the
runtime consults. It is computed at the registry boundary (see
:meth:`domain.calculations.registry.ValidatedRegistryAuthority.authorization`),
never authored independently per revision, so the capability cannot
drift from the manifest.
The manifest is the only writable authorization surface; everything else
is a derivation of it. This is the same trust-but-verify shape codified
for fixtures in ``fixture-provenance-declared-in-sidecar``: a declaration
in data, cross-checked against evidence the declaration cannot fabricate
(here, the year-set a recorder observes a real test exercising).
"""
from __future__ import annotations
from enum import StrEnum
from pathlib import Path
from typing import Final
from pydantic import BaseModel, Field, model_validator
from ...core import STRICT_FROZEN_CONFIG
from .. import NON_REGISTRY_MODELOS, Modelo, read_toml
from ._errors import AuthorizationManifestError
#: Minimum number of distinct renta (annual) years an enrolling test must
#: exercise before a modelo may be authorized. The owner mandate is two
#: distinct years: a single-period test cannot prove cross-year stability.
MIN_DISTINCT_RENTA_YEARS: Final[int] = 2
#: Directory holding the per-modelo authorization manifest fragments,
#: resolved relative to the AEAT registry root (sibling of ``legal/`` and
#: ``modelos/``). The manifest is directory-mode: each enrolled modelo owns
#: one ``authorization.d/<modelo>.toml`` file, mirroring the registry's own
#: directory-mode layout. The loader merges every fragment. Per-modelo
#: files mean concurrent enrollment across the campaign swarm writes
#: disjoint files — zero cross-coder contention on a single manifest.
AUTHORIZATION_MANIFEST_DIRNAME: Final[str] = "authorization.d"
#: The canonical fleet of modelo ids the gate enrolls. The owner mandate
#: is non-negotiable: every modelo enrolls, none is out-of-scope. This is
#: the honest denominator behind the meta-test's ``authorized N/30`` line.
#: It is the union of every modelo the registry authority can load today
#: plus the engine-build modelos whose registry directory carries no
#: ``manifest.toml`` yet (151 Beckham, 714 Patrimonio, 721 crypto), which
#: the ADR's engine-build sub-decision keeps in scope. The meta-test pins
#: this tuple against the live registry so a registry change that adds or
#: drops a modelo surfaces loudly rather than silently moving the
#: denominator. Retired identifiers carried by :class:`Modelo` but with no
#: registry definition (:data:`NON_REGISTRY_MODELOS`, e.g. the suppressed M037
#: censo simplificada) are NOT enrolled and are excluded here.
CANONICAL_MODELO_FLEET: Final[tuple[str, ...]] = tuple(m for m in Modelo if m not in NON_REGISTRY_MODELOS)
#: The enrolled-fleet size used as the meta-test denominator. Derived from
#: the canonical fleet rather than hard-coded so the two cannot disagree.
FLEET_SIZE: Final[int] = len(CANONICAL_MODELO_FLEET)
[docs]
class AuthorizationState(StrEnum):
"""Closed two-state authorization verdict for a modelo's backend.
``UNAUTHORIZED`` is the default for every modelo not proven by an
enrolling test; ``AUTHORIZED`` is granted only once a verified
enrollment record exists in the manifest.
"""
UNAUTHORIZED = "unauthorized"
AUTHORIZED = "authorized"
[docs]
class EnrollmentEvidenceClass(StrEnum):
"""Closed set of enrollment-evidence shapes across the modelo fleet.
The owner mandate keeps every modelo in scope, but the *shape* of the
cross-year evidence varies by modelo class (see the
``modelo-multiyear-renta`` ADR's cross-modelo-class ruling):
- ``CALCULATION`` — engine plus a cross-year carry (130, 100, 200,
202, 303, 131, …); the recorder captures two ``filing_year`` values
computed through ``calculate_modelo_revision``.
- ``RECONCILIATION`` — periodic→annual summary reconciled across two
years (390, 180, 190, 193).
- ``DATA_FIDELITY`` — informativa year-over-year fidelity plus
provenance, with no numeric oracle (347, 184, 721, 232, 349, …).
- ``THRESHOLD_CONTINUITY`` — structural exemption / obligation logic
across two years (720, 840, 036).
"""
CALCULATION = "calculation"
RECONCILIATION = "reconciliation"
DATA_FIDELITY = "data_fidelity"
THRESHOLD_CONTINUITY = "threshold_continuity"
[docs]
class ModeloAuthorizationEntry(BaseModel):
"""One enrolling claim from the authorization manifest.
An entry is a *claim* the enrolling end-to-end test must verify, not a
trusted grant. The ``>=2 distinct renta years`` invariant is enforced
here at the pydantic boundary so a single-year (un-cross-year) claim
cannot even construct — the minimum-two-years contract is
unconstructable to violate, not merely asserted in a test.
Attributes:
modelo: The modelo id the entry authorizes (e.g. ``"130"``).
renta_years: The distinct renta (annual) years the enrolling
evidence must exercise. At least :data:`MIN_DISTINCT_RENTA_YEARS`
distinct years are required; duplicates are rejected.
evidence_class: The enrollment-evidence shape for this modelo.
enrolling_test: Dotted-or-path reference to the end-to-end persona
test that enrolls this modelo. The meta-test confirms the
referenced test exists and that the year-set its recorder
observes equals ``renta_years``.
"""
model_config = STRICT_FROZEN_CONFIG
modelo: str = Field(min_length=1, max_length=8)
renta_years: tuple[int, ...]
evidence_class: EnrollmentEvidenceClass
enrolling_test: str = Field(min_length=1, max_length=256)
@model_validator(mode="after")
def _validate_distinct_renta_years(self) -> ModeloAuthorizationEntry:
"""Reject any claim that does not span >=2 distinct renta years.
Two failure modes are caught: fewer than the minimum number of
years, and a claim padded with duplicate years to fake breadth.
Both reduce to "the distinct-year count is below the floor".
"""
distinct = set(self.renta_years)
if len(distinct) < MIN_DISTINCT_RENTA_YEARS:
raise ValueError(
f"modelo {self.modelo!r} authorization claim must span at least "
f"{MIN_DISTINCT_RENTA_YEARS} distinct renta years; got "
f"{sorted(self.renta_years)!r} ({len(distinct)} distinct)",
)
return self
@property
def distinct_renta_years(self) -> tuple[int, ...]:
"""Return the claim's distinct renta years, sorted ascending."""
return tuple(sorted(set(self.renta_years)))
[docs]
class AuthorizationManifest(BaseModel):
"""Parsed, validated view of the directory-mode authorization manifest.
The manifest is the merge of every ``authorization.d/<modelo>.toml``
fragment. Default-deny is literal: an absent directory or one with no
fragments yields an empty :attr:`entries` tuple and therefore authorizes
zero modelos. A modelo id may appear at most once across fragments; a
duplicate is an authoring error, not a silent last-wins overwrite.
"""
model_config = STRICT_FROZEN_CONFIG
entries: tuple[ModeloAuthorizationEntry, ...] = ()
@model_validator(mode="after")
def _reject_duplicate_modelos(self) -> AuthorizationManifest:
seen: set[str] = set()
for entry in self.entries:
if entry.modelo in seen:
raise ValueError(
f"authorization manifest declares modelo {entry.modelo!r} more than once; "
f"each modelo enrolls through exactly one entry",
)
seen.add(entry.modelo)
return self
[docs]
def entry_for(self, modelo: str) -> ModeloAuthorizationEntry | None:
"""Return the enrolling :class:`ModeloAuthorizationEntry` for ``modelo``, or ``None`` if absent."""
for entry in self.entries:
if entry.modelo == modelo:
return entry
return None
@property
def authorized_modelos(self) -> frozenset[str]:
"""Return the set of modelo ids the manifest authorizes."""
return frozenset(entry.modelo for entry in self.entries)
[docs]
class ModeloAuthorization(BaseModel):
"""Derived per-modelo authorization capability the runtime consults.
This record is *computed* at the registry boundary from the manifest
cross-checked against the loadable registry — it is never authored
independently per revision, so it cannot drift from the manifest.
See
:meth:`domain.calculations.registry.ValidatedRegistryAuthority.authorization`.
Attributes:
modelo: The modelo id this capability describes.
state: ``AUTHORIZED`` iff the manifest enrolls the modelo;
``UNAUTHORIZED`` otherwise (the default for every modelo).
has_engine: Whether the modelo declares a ``calculation`` surface
application-link in the loaded registry. Drives the CLI
ADVISORY-vs-refusal split: an UNAUTHORIZED modelo that has an
engine still computes (with an advisory banner); a modelo with
no loadable engine is refused at ``work create`` by the
established stub guard.
entry: The manifest entry backing an ``AUTHORIZED`` state, or
``None`` when ``UNAUTHORIZED``.
"""
model_config = STRICT_FROZEN_CONFIG
modelo: str = Field(min_length=1, max_length=8)
state: AuthorizationState
has_engine: bool
entry: ModeloAuthorizationEntry | None = None
@model_validator(mode="after")
def _validate_entry_matches_state(self) -> ModeloAuthorization:
"""An AUTHORIZED capability must carry its backing manifest entry."""
if self.state is AuthorizationState.AUTHORIZED and self.entry is None:
raise ValueError(
f"modelo {self.modelo!r} is AUTHORIZED but carries no backing manifest entry; "
f"the derived capability must reference the entry that authorizes it",
)
if self.state is AuthorizationState.UNAUTHORIZED and self.entry is not None:
raise ValueError(
f"modelo {self.modelo!r} is UNAUTHORIZED but carries a manifest entry; "
f"an unauthorized capability must not reference an enrolling entry",
)
return self
@property
def is_authorized(self) -> bool:
"""Return whether the modelo's backend is authorized."""
return self.state is AuthorizationState.AUTHORIZED
[docs]
def manifest_dir(registry_root: Path) -> Path:
"""Return the ``authorization.d`` directory for an AEAT registry root.
The directory is a sibling of ``legal/`` and ``modelos/`` under the
registry root, so callers that hold the registry root (the authority,
the fingerprint walker) resolve it the same way.
"""
return registry_root / AUTHORIZATION_MANIFEST_DIRNAME
def _load_manifest_fragment(path: Path) -> ModeloAuthorizationEntry:
"""Parse and validate one ``authorization.d/<modelo>.toml`` fragment.
A fragment declares exactly one enrolled modelo as a top-level
``[modelo]`` table. The fragment filename stem should match the
declared ``modelo`` id (e.g. ``130.toml`` declares ``modelo = "130"``);
the loader cross-checks the two so a mis-filed fragment fails loudly.
Args:
path: The fragment file path.
Returns:
The validated :class:`ModeloAuthorizationEntry`.
Raises:
AuthorizationManifestError: When the fragment cannot be parsed,
omits or duplicates the ``[modelo]`` table, declares a stem that
disagrees with the ``modelo`` id, or violates an entry invariant
(single-year claim, unknown field).
"""
raw = read_toml(path, error_factory=AuthorizationManifestError)
raw_entry = raw.get("modelo")
if not isinstance(raw_entry, dict):
raise AuthorizationManifestError(
f"{path}: an authorization.d fragment must declare exactly one [modelo] table; "
f"got {type(raw_entry).__name__ if raw_entry is not None else 'no [modelo] table'}",
)
try:
# TOML hydration is a boundary coercion: arrays decode to ``list``
# and scalars to ``str``, but the entry model is ``strict=True`` for
# programmatic construction. Validate non-strictly here so
# ``renta_years`` (TOML array -> tuple) and ``evidence_class`` (TOML
# string -> StrEnum) hydrate at the boundary; every value invariant
# (>=2 distinct years, field bounds) still runs regardless of mode.
entry = ModeloAuthorizationEntry.model_validate(raw_entry, strict=False)
except ValueError as exc:
raise AuthorizationManifestError(f"{path}: invalid authorization fragment: {exc}") from exc
if entry.modelo != path.stem:
raise AuthorizationManifestError(
f"{path}: fragment filename stem {path.stem!r} does not match its declared "
f"modelo id {entry.modelo!r}; name the fragment <modelo>.toml",
)
return entry
[docs]
def load_authorization_manifest(registry_root: Path) -> AuthorizationManifest:
"""Load and validate the directory-mode authorization manifest.
The manifest is directory-mode: every enrolled modelo owns one
``authorization.d/<modelo>.toml`` fragment under ``registry_root``. The
loader merges every fragment in sorted order into one
:class:`AuthorizationManifest`.
Default-deny-by-absence: when the ``authorization.d`` directory is
missing or holds no ``*.toml`` fragments, the result is an empty
manifest (zero authorized modelos), never an error. A present-but-
malformed fragment raises :class:`AuthorizationManifestError` so a broken
authorization surface fails loudly rather than silently authorizing
nothing or everything.
Args:
registry_root: The AEAT registry root directory.
Returns:
The validated :class:`AuthorizationManifest`.
Raises:
AuthorizationManifestError: When a fragment cannot be parsed or
violates an entry/manifest invariant (single-year claim,
duplicate modelo across fragments, stem/id mismatch, unknown
field).
"""
directory = manifest_dir(registry_root)
if not directory.is_dir():
return AuthorizationManifest()
entries = tuple(_load_manifest_fragment(path) for path in sorted(directory.glob("*.toml")))
try:
return AuthorizationManifest(entries=entries)
except ValueError as exc:
raise AuthorizationManifestError(f"{directory}: invalid authorization manifest: {exc}") from exc
[docs]
def derive_modelo_authorization(
modelo: str,
*,
manifest: AuthorizationManifest,
has_engine: bool,
) -> ModeloAuthorization:
"""Derive the per-modelo capability from the manifest and engine presence.
This is the canonical derivation the registry authority exposes. The
capability is a pure function of (a) the manifest entry, if any, and
(b) whether the modelo declares a calculation surface in the loaded
registry — never an independently authored flag.
Args:
modelo: The modelo id to derive a capability for.
manifest: The loaded authorization manifest (default-deny source).
has_engine: Whether the modelo declares a ``calculation`` surface
application-link in the registry.
Returns:
The derived :class:`ModeloAuthorization`.
"""
entry = manifest.entry_for(modelo)
if entry is None:
return ModeloAuthorization(
modelo=modelo,
state=AuthorizationState.UNAUTHORIZED,
has_engine=has_engine,
entry=None,
)
return ModeloAuthorization(
modelo=modelo,
state=AuthorizationState.AUTHORIZED,
has_engine=has_engine,
entry=entry,
)
__all__ = [
"AUTHORIZATION_MANIFEST_DIRNAME",
"CANONICAL_MODELO_FLEET",
"FLEET_SIZE",
"MIN_DISTINCT_RENTA_YEARS",
"AuthorizationManifest",
"AuthorizationState",
"EnrollmentEvidenceClass",
"ModeloAuthorization",
"ModeloAuthorizationEntry",
"derive_modelo_authorization",
"load_authorization_manifest",
"manifest_dir",
]