Source code for cadrumo.domain.calculations.registry.schema_surfaces

"""Casilla, relation, export, and record schema models."""

from __future__ import annotations

import re
from collections.abc import Sequence
from decimal import Decimal
from enum import StrEnum
from typing import Annotated

from pydantic import BeforeValidator, Field, model_validator

from ....core.casilla_id import CasillaId
from ....core.errors.hierarchy import pydantic_validation_boundary
from ....core.identity.aeat_box import AeatBoxNumber
from ....core.identity.continuidad import ContinuidadId
from ._schema_export_exemption import ExportExemptionReasonValue
from .casilla_lineage import CasillaLineageOriginField
from .errors import RegistryValidationError
from .ids import (
    BindingId,
    ExportFieldId,
    FormulaId,
    RecordId,
    RevisionId,
    SourceRefId,
)
from .modelo_localization import require_modelo_localization, resolve_modelo_localization
from .schema_base import (
    CasillaDataType,
    CasillaDataTypeField,
    CasillaSignConstraint,
    CasillaSignConstraintField,
    LegalRefs,
    RegistryModel,
    SourceRefs,
    coerce_enum_member,
)
from .schema_input_kind import InputKind, InputKindValue
from .schema_scalars import DecimalValue

__all__ = [
    "CalculationCompletenessCasilla",
    "CalculationCompletenessManifest",
    "CasillaAlias",
    "CasillaConstraints",
    "CasillaContinuidadEvolutionDefinition",
    "CasillaDefinition",
    "validate_family_identity_uniqueness",
]


[docs] class CasillaEvolutionKind(StrEnum): """How a casilla changed between two revisions. ``REPURPOSED`` and ``RETIRED`` are not degrees of change: a repurposed casilla still exists and now means something else, a retired one is gone. Reading one as the other would carry a prior year's value into a box that no longer holds it. """ UNCHANGED = "unchanged" LABEL_EVOLVED = "label_evolved" SECTION_EVOLVED = "section_evolved" REPRESENTATION_EVOLVED = "representation_evolved" LEGAL_REFS_EVOLVED = "legal_refs_evolved" LABEL_AND_LEGAL_REFS_EVOLVED = "label_and_legal_refs_evolved" REPURPOSED = "repurposed" RETIRED = "retired"
CasillaEvolutionKindField = Annotated[CasillaEvolutionKind, BeforeValidator(coerce_enum_member(CasillaEvolutionKind))] """Registry token hydrated into a CasillaEvolutionKind member."""
[docs] class SemanticRoleCardinality(StrEnum): """Whether a semantic role is shared across casillas or deliberately unique.""" SHARED = "shared" INTENTIONAL_SINGLETON = "intentional_singleton"
SemanticRoleCardinalityField = Annotated[ SemanticRoleCardinality, BeforeValidator(coerce_enum_member(SemanticRoleCardinality)) ] """Registry token hydrated into a SemanticRoleCardinality member."""
[docs] class CasillaContinuidadEvolutionDefinition(RegistryModel): """Declared cross-revision evolution for one casilla continuity chain.""" id: RecordId continuidad_id: ContinuidadId from_revision: RevisionId to_revision: RevisionId evolution_kind: CasillaEvolutionKindField legal_refs: LegalRefs source_refs: SourceRefs @model_validator(mode="after") @pydantic_validation_boundary def _validate_revision_pair(self) -> CasillaContinuidadEvolutionDefinition: if self.from_revision == self.to_revision: raise RegistryValidationError( f"casilla continuidad evolution {self.id!r} must span two different revisions", ) return self
[docs] class CasillaAlias(RegistryModel): """A localized label variant for a casilla, carrying its own grounding. Used by Plan C's semantic-role validator: two casillas in different modelos can share a ``semantic_role`` even though their primary labels differ, provided each declares the divergent phrasing via an ``aliases`` entry with a documented BOE or AEAT source. The text is resolved from the shared locale catalogue; the revision loader derives ``localization_key`` from the containing casilla and alias occurrence. """ localization_key: str = Field(min_length=1, exclude=True, repr=False) legal_refs: LegalRefs source_refs: SourceRefs
[docs] def get_label(self, locale: str) -> str: """Resolve the alias label through the shared catalogue.""" return require_modelo_localization((self.localization_key,), locale=locale)
@property def label(self) -> str: """Return the strict official-Spanish alias label.""" return self.get_label("es")
def _validate_constraint_numeric_bounds( sign: CasillaSignConstraint, min_value: DecimalValue | None, max_value: DecimalValue | None, ) -> None: """Validate the numeric bounds and sign contract for a constraint.""" if min_value is not None and max_value is not None and min_value > max_value: raise RegistryValidationError(f"casilla constraints: min_value {min_value} > max_value {max_value}") if sign == "non_negative" and max_value is not None and max_value < Decimal("0"): raise RegistryValidationError( "casilla constraints: sign='non_negative' is incompatible with negative max_value", ) if sign == "non_positive" and min_value is not None and min_value > Decimal("0"): raise RegistryValidationError( "casilla constraints: sign='non_positive' is incompatible with positive min_value", ) def _validate_constraint_text_bounds(min_length: int | None, max_length: int | None) -> None: """Validate the lower and upper bounds of a text constraint.""" if min_length is not None and max_length is not None and min_length > max_length: raise RegistryValidationError(f"casilla constraints: min_length {min_length} > max_length {max_length}") def _validate_constraint_enum_values(enum: tuple[str, ...] | None) -> None: """Validate that an optional constraint enum is populated and unique.""" if enum is not None and len(enum) == 0: raise RegistryValidationError("casilla constraints: enum must declare at least one value") if enum is not None and len(set(enum)) != len(enum): raise RegistryValidationError("casilla constraints: enum values must be unique") def _validate_constraint_pattern(pattern: str | None) -> None: """Validate an optional constraint regex without changing its error surface.""" if pattern is not None: try: re.compile(pattern) except re.error as exc: raise RegistryValidationError( f"casilla constraints: pattern {pattern!r} is not a valid regex: {exc}", ) from exc
[docs] class CasillaConstraints(RegistryModel): """Declarative value constraints applied after a casilla is evaluated. Captures the legal sign / range rules AEAT mandates per LIRPF / LIVA / LIS articles: a withholding casilla cannot carry a negative value, a deductibility cap restricts the maximum, a non-negativity floor on a cuota líquida prevents arithmetic underflow propagating through downstream formulas. Each constraint declares its own legal grounding so the engine can surface a BOE permalink in the violation envelope when a computed value falls outside the declared bounds. The runtime raises a typed `CasillaConstraintViolationError`; the Sheets apply adapter renders the same record as a `setDataValidation` rule on the corresponding cell so the operator sees the constraint directly in the workbook UI. """ sign: CasillaSignConstraintField = CasillaSignConstraint.ANY min_value: DecimalValue | None = None max_value: DecimalValue | None = None pattern: str | None = None min_length: int | None = Field(default=None, ge=0) max_length: int | None = Field(default=None, ge=0) enum: tuple[str, ...] | None = None legal_refs: LegalRefs source_refs: SourceRefs @model_validator(mode="after") @pydantic_validation_boundary def _validate_bounds(self) -> CasillaConstraints: _validate_constraint_numeric_bounds(self.sign, self.min_value, self.max_value) _validate_constraint_text_bounds(self.min_length, self.max_length) _validate_constraint_enum_values(self.enum) _validate_constraint_pattern(self.pattern) return self
[docs] def violates(self, value: Decimal) -> str | None: """Return a short reason string if a Decimal `value` violates this constraint, else None. Numeric-only violation check preserved for downstream consumers of computed casilla values. Text constraints (`pattern`, `min_length`, `max_length`, `enum`) are surfaced through the sibling :meth:`violates_text` method. """ if self.sign == "non_negative" and value < Decimal("0"): return f"value {value} violates sign=non_negative" if self.sign == "non_positive" and value > Decimal("0"): return f"value {value} violates sign=non_positive" if self.min_value is not None and value < self.min_value: return f"value {value} below min_value {self.min_value}" if self.max_value is not None and value > self.max_value: return f"value {value} above max_value {self.max_value}" return None
[docs] def violates_text(self, value: str) -> str | None: """Return a short reason string if a text `value` violates the text constraints, else None. Checks the four text-shape constraints introduced by Plan B: ``pattern``, ``min_length``, ``max_length``, and ``enum``. Used by consumers that resolve a casilla value into a string at evaluation time. """ length = len(value) if self.min_length is not None and length < self.min_length: return f"value length {length} below min_length {self.min_length}" if self.max_length is not None and length > self.max_length: return f"value length {length} above max_length {self.max_length}" if self.pattern is not None and not re.fullmatch(self.pattern, value): return f"value {value!r} does not match pattern {self.pattern!r}" if self.enum is not None and value not in self.enum: return f"value {value!r} not in enum {self.enum!r}" return None
def _validate_computed_input_kind( casilla_id: CasillaId, input_kind: InputKind, formula: FormulaId | None, binding: BindingId | None, ) -> None: """Enforce the formula/binding requirements for computed casillas.""" if input_kind == InputKind.COMPUTED and formula is None: raise RegistryValidationError(f"computed casilla {casilla_id!r} must declare formula") if input_kind == InputKind.COMPUTED and binding is not None: raise RegistryValidationError(f"computed casilla {casilla_id!r} must not declare binding") def _validate_binding_declarations( casilla_id: CasillaId, input_kind: InputKind, binding: BindingId | None, alternate_bindings: tuple[BindingId, ...], ) -> None: """Enforce that only bound casillas carry primary or alternate bindings.""" if input_kind != InputKind.BOUND and alternate_bindings: raise RegistryValidationError(f"non-bound casilla {casilla_id!r} must not declare alternate_bindings") if input_kind == InputKind.BOUND and binding is None: raise RegistryValidationError(f"bound casilla {casilla_id!r} must declare binding") def _validate_binding_uniqueness( casilla_id: CasillaId, binding: BindingId | None, alternate_bindings: tuple[BindingId, ...], ) -> None: """Reject a primary binding repeated in, or duplicates within, alternates.""" if binding is not None and binding in alternate_bindings: raise RegistryValidationError( f"casilla {casilla_id!r} alternate_bindings must not repeat primary binding {binding!r}", ) if len(set(alternate_bindings)) != len(alternate_bindings): raise RegistryValidationError(f"casilla {casilla_id!r} alternate_bindings must be unique") def _validate_bound_formula( casilla_id: CasillaId, input_kind: InputKind, formula: FormulaId | None, ) -> None: """Reject formulas on casillas whose values come from a binding.""" if input_kind == InputKind.BOUND and formula is not None: raise RegistryValidationError(f"bound casilla {casilla_id!r} must not declare formula") def _validate_projection_only( casilla_id: CasillaId, input_kind: InputKind, formula: FormulaId | None, binding: BindingId | None, alternate_bindings: tuple[BindingId, ...], ) -> None: """Reject calculation inputs on projection-only casillas.""" if input_kind == InputKind.PROJECTION_ONLY and any((formula, binding, alternate_bindings)): raise RegistryValidationError( f"projection-only casilla {casilla_id!r} must not declare formula, binding, or alternate_bindings", )
[docs] class CasillaDefinition(RegistryModel): """A single AEAT casilla within a modelo revision. ``id`` is the canonical reference identity. ``number`` and ``segmento`` are reviewed AEAT record-design metadata. For single-segment modelos ``segmento`` is unset and ``number`` alone is unique within the revision. Multi-segment AEAT modelos (e.g. Modelo 200) reuse the same five-digit ``number`` across distinct record segments with a different meaning in each; those casillas carry the AEAT record-segment code in ``segmento`` to disambiguate metadata. """ id: CasillaId number: str segmento: str | None = Field( default=None, min_length=1, max_length=32, description=( "AEAT record-segment code (e.g. 'DP200014') for multi-segment " "modelos that reuse a casilla number across record segments. " "Unset for single-segment modelos; references still use canonical " "casilla.id, not this metadata pair." ), ) localization_keys: tuple[str, ...] = Field(min_length=1, exclude=True, repr=False) section: tuple[str, ...] data_type: CasillaDataTypeField = CasillaDataType.MONEY required: bool = False input_kind: InputKindValue = InputKind.MANUAL formula: FormulaId | None = None binding: BindingId | None = None alternate_bindings: tuple[BindingId, ...] = Field( default_factory=tuple, description=( "Equivalent binding slots that may populate the same bound casilla. " "Used only when the registry has reviewed multiple legally grounded " "source paths for the same factual amount; conflicting supplied " "values are rejected before calculation." ), ) export_refs: tuple[ExportFieldId, ...] = () export_exemption_reason: ExportExemptionReasonValue | None = Field( default=None, description=( "Why this casilla files no slot on the official fixed-width record. " "Required by registry build for a completeness-manifest casilla that " "the export completeness gate would otherwise demand -- one declaring " "a formula or marked required -- but that no export field addresses. " "Without it, exemption from that gate is expressed by absence alone, " "which reads identically whether the casilla genuinely cannot reach a " "box or was simply never annotated. A casilla the record DOES address " "must not declare one, and ``internal_only`` already asserts the " "internal-intermediate case on its own." ), ) constraints: CasillaConstraints | None = None form_number: AeatBoxNumber | None = Field(default=None) continuidad_id: ContinuidadId | None = Field( default=None, description=( "Stable cross-revision continuity key for non-overlapping annual " "forms. When present, it identifies the legal concept continuity " "chain independently of the revision-local casilla id." ), ) continuidad_origin: CasillaLineageOriginField | None = Field( default=None, description=( "How this row stands toward its predecessor edition: a grounded or " "seeded continuation of a predecessor row, or which kind of absence " "-- new on the form, present on the form but not declared by the " "predecessor edition, or not printed on any form. Unset carries no " "claim either way." ), ) continuidad_evidence: str | None = Field( default=None, min_length=1, max_length=1024, description=( "The checkable source behind continuidad_origin: the official " "record-design extract or adjudication record, with the line or campo " "that settles it. Required for every origin except seeded." ), ) inherited_from: RevisionId | None = Field( default=None, exclude=True, repr=False, description=( "The edition that last stated this row, when the edition holding it " "inherits it from its declared predecessor chain; unset when the " "holding edition states the row itself. Set by the loader during " "materialisation and refused when authored. Excluded from " "serialisation, because where a row is stated says nothing about " "what it means." ), ) semantic_role: str | None = Field(default=None, min_length=1, max_length=128) semantic_role_cardinality: SemanticRoleCardinalityField = SemanticRoleCardinality.SHARED semantic_role_cardinality_reason: str | None = Field(default=None, min_length=1, max_length=256) aliases: tuple[CasillaAlias, ...] = () internal_only: bool = Field( default=False, description=( "App-internal computed casilla that participates in the calculation " "graph but is intentionally absent from the AEAT-published Diseño de " "Registros. Typically a regulatory ceiling or intermediate the app " "materialises as a casilla so verification predicates and downstream " "formulas can reference it. An internal_only casilla MUST be computed " "(``input_kind = COMPUTED``), MUST carry no ``export_refs``, and MUST " "carry legal_refs / source_refs grounding the internal computation in " "real regulatory authority." ), ) legal_refs: LegalRefs source_refs: SourceRefs
[docs] def get_label(self, locale: str) -> str: """Resolve one label scalar through the canonical shared catalogues.""" return require_modelo_localization(self.localization_keys, locale=locale)
[docs] def get_help(self, locale: str) -> str | None: """Resolve optional help through the same identity and fallback chain.""" help_keys = tuple(f"{key.removesuffix('.label')}.help" for key in self.localization_keys) return resolve_modelo_localization(help_keys, locale=locale)
@property def label(self) -> str: """Return the strict official-Spanish regulatory label.""" return self.get_label("es") @model_validator(mode="after") @pydantic_validation_boundary def _validate_input_kind(self) -> CasillaDefinition: # Localization is resolved by the registry authority after its shared # catalogue has been selected. Constructing a schema from an arbitrary # test or operator-supplied root must not consult the bundled catalogue; # the structural validator still enforces every non-localized rule here. _validate_computed_input_kind(self.id, self.input_kind, self.formula, self.binding) _validate_binding_declarations(self.id, self.input_kind, self.binding, self.alternate_bindings) _validate_binding_uniqueness(self.id, self.binding, self.alternate_bindings) _validate_bound_formula(self.id, self.input_kind, self.formula) _validate_projection_only(self.id, self.input_kind, self.formula, self.binding, self.alternate_bindings) self._validate_export_exposure() self._validate_singleton_role_declaration() self._validate_lineage_origin() return self def _validate_export_exposure(self) -> None: """Hold internal_only, export_refs and the exemption reason coherent. All three axes answer one question -- whether an official record addresses this casilla -- so a casilla may assert at most one of "app-internal", "exported" and "exempt from the completeness gate". """ if self.internal_only and self.export_refs: raise RegistryValidationError( f"internal_only casilla {self.id!r} must not declare export_refs " "(an app-internal casilla cannot also be exported to a fichero record)", ) if self.export_exemption_reason is not None and self.export_refs: raise RegistryValidationError( f"casilla {self.id!r} declares export_exemption_reason " f"{self.export_exemption_reason.value!r} while also declaring export_refs " "(a casilla the official record addresses is not exempt from the export " "completeness gate)", ) if self.internal_only and self.export_exemption_reason is not None: raise RegistryValidationError( f"internal_only casilla {self.id!r} must not declare export_exemption_reason " f"{self.export_exemption_reason.value!r} " "(internal_only already asserts the internal-intermediate exemption)", ) if self.internal_only and self.input_kind != InputKind.COMPUTED: raise RegistryValidationError( f"internal_only casilla {self.id!r} must be computed " "(an internal ceiling has no legitimate computation surface unless formula-derived)", ) def _validate_singleton_role_declaration(self) -> None: """Require a declared singleton cardinality to carry its role and reason. This is the per-casilla coherence half; the cross-casilla check that a role claimed as a singleton is not actually shared runs at registry scope over the whole modelo tuple. """ if self.semantic_role_cardinality != "intentional_singleton": if self.semantic_role_cardinality_reason is not None: raise RegistryValidationError( f"casilla {self.id!r} declares semantic_role_cardinality_reason " "without intentional singleton cardinality", ) return if self.semantic_role is None: raise RegistryValidationError( f"casilla {self.id!r} declares intentional singleton role cardinality without semantic_role", ) if self.semantic_role_cardinality_reason is None: raise RegistryValidationError( f"casilla {self.id!r} declares intentional singleton role cardinality without reason", ) def _validate_lineage_origin(self) -> None: """Hold continuidad_origin, continuidad_id and continuidad_evidence coherent. A continuation names the chain it continues, so it needs a continuidad_id; an absence may still carry one, because a chain can start at the row. Only this row-local coherence is checked here -- whether the predecessor edition really does (or does not) carry the chain is a cross-revision question. """ origin = self.continuidad_origin if origin is None: if self.continuidad_evidence is not None: raise RegistryValidationError( f"casilla {self.id!r} declares continuidad_evidence without continuidad_origin", ) return if origin.continues_a_chain and self.continuidad_id is None: raise RegistryValidationError( f"casilla {self.id!r} declares continuidad_origin {origin.value!r} without continuidad_id " "(a continuation must name the chain it continues)", ) if origin.requires_evidence and self.continuidad_evidence is None: raise RegistryValidationError( f"casilla {self.id!r} declares continuidad_origin {origin.value!r} without continuidad_evidence", )
[docs] class CalculationCompletenessCasilla(RegistryModel): """One required canonical ``casilla.id`` in a calculation-completeness manifest. ``casilla_id`` is the authoritative reference key consumed by the validator. ``number`` and ``segmento`` remain official record-design metadata for drift review: the validator cross-checks them against the referenced casilla, but no consumer may resolve the manifest by those metadata fields. The manifest enumerates only the casillas inside a modelo's *calculation closure* — formula targets, the casillas referenced inside any formula expression, formula and binding endpoint casillas, and verification-expectation operands. Pure accounting-statement data-entry fields that feed no calculation are intentionally absent from this required set. """ casilla_id: CasillaId number: str = Field(min_length=1) segmento: str | None = Field( default=None, min_length=1, max_length=32, description=( "AEAT record-segment code (e.g. 'DP200014') for multi-segment " "modelos that reuse a casilla number across record segments. " "Unset for single-segment modelos." ), )
[docs] def record_design_metadata(self) -> tuple[str | None, str]: """Return the reviewed ``(segmento, number)`` metadata pair.""" return (self.segmento, self.number)
[docs] def manifest_key(self) -> tuple[str, str | None, str]: """Return the canonical id plus its reviewed record-design metadata.""" return (self.casilla_id, self.segmento, self.number)
def _validate_manifest_population(casillas: tuple[CalculationCompletenessCasilla, ...]) -> None: """Require a calculation-completeness manifest to enumerate casillas.""" if not casillas: raise RegistryValidationError("calculation-completeness manifest must enumerate at least one casilla") def _validate_manifest_casilla_ids(casillas: tuple[CalculationCompletenessCasilla, ...]) -> None: """Reject duplicate canonical casilla ids in a completeness manifest.""" casilla_ids = [casilla.casilla_id for casilla in casillas] duplicate_ids = sorted({casilla_id for casilla_id in casilla_ids if casilla_ids.count(casilla_id) > 1}) if duplicate_ids: rendered_ids = ", ".join(repr(casilla_id) for casilla_id in duplicate_ids) raise RegistryValidationError( f"calculation-completeness manifest declares duplicate casilla ids: {rendered_ids}", ) def _validate_manifest_record_design_metadata(casillas: tuple[CalculationCompletenessCasilla, ...]) -> None: """Reject duplicate reviewed segment/number metadata in a manifest.""" metadata_pairs = [casilla.record_design_metadata() for casilla in casillas] duplicates = sorted({pair for pair in metadata_pairs if metadata_pairs.count(pair) > 1}) if duplicates: rendered = ", ".join( f"{number!r}" if segmento is None else f"{number!r} within segmento {segmento!r}" for segmento, number in duplicates ) raise RegistryValidationError( f"calculation-completeness manifest declares duplicate casilla record-design metadata: {rendered}", ) def _validate_manifest_grounding(source_ref: SourceRefId, source_refs: SourceRefs) -> None: """Require the manifest's derivation source to be among its evidence refs.""" if source_ref not in source_refs: raise RegistryValidationError("calculation-completeness manifest source_ref must be included in source_refs") def _validate_manifest_extraction_state(manual_extraction: bool, reason: str | None) -> None: """Keep manual-extraction attribution and its explanation in lockstep.""" if manual_extraction and reason is None: raise RegistryValidationError( "calculation-completeness manifest with manual_extraction must declare manual_extraction_reason", ) if not manual_extraction and reason is not None: raise RegistryValidationError( "calculation-completeness manifest declares manual_extraction_reason without manual_extraction", )
[docs] class CalculationCompletenessManifest(RegistryModel): """The required calculation-closure casilla set for a modelo revision. Enumerates the canonical ``casilla.id`` values in a modelo's *calculation closure* — the casillas the cross-connecting calculation engine traverses: every formula target, every casilla referenced inside a formula expression, every binding and relation endpoint casilla, and every verification-expectation operand. It is derived from the official AEAT Diseño de Registros *intersected with* the modelo's calculation surface — Diseño-authoritative on each casilla's segment, number, and label, but the checked-in reference key is the registry's canonical ``casilla.id``. The segment/number metadata is retained as reviewed evidence and cross-checked against that id. The registry validator enforces ``manifest-required ⊆ declared`` by canonical ``casilla.id`` plus a segment/number metadata check and a ``legal_refs`` / ``source_refs`` grounding check on each required casilla. A casilla the revision declares but the manifest does not list (a pure accounting-statement field) is *not* a failure. ``manual_extraction`` flags a manifest authored from a manual read of a PDF-only Diseño that resists machine extraction; the off-load-path drift re-verification skips the machine re-derivation for such manifests and records ``manual_extraction_reason`` instead of failing silently. """ source_ref: SourceRefId = Field( description="Catalogue source id of the AEAT Diseño de Registros the manifest was derived from.", ) casillas: tuple[CalculationCompletenessCasilla, ...] manual_extraction: bool = False manual_extraction_reason: str | None = Field(default=None, min_length=1, max_length=512) legal_refs: LegalRefs source_refs: SourceRefs @model_validator(mode="after") @pydantic_validation_boundary def _validate_manifest(self) -> CalculationCompletenessManifest: _validate_manifest_population(self.casillas) _validate_manifest_casilla_ids(self.casillas) _validate_manifest_record_design_metadata(self.casillas) _validate_manifest_grounding(self.source_ref, self.source_refs) _validate_manifest_extraction_state(self.manual_extraction, self.manual_extraction_reason) return self
[docs] def casilla_ids(self) -> frozenset[CasillaId]: """Return the frozenset of required canonical ``casilla.id`` values.""" return frozenset(casilla.casilla_id for casilla in self.casillas)
# Single source of truth for the predicate-DSL operator names. The # registry-load validator # (_validate_surfaces.validate_verification_expectation_section) # uses this set to reject unknown operators at authoring time. The # runtime evaluator # (cadrumo.application.modelo.verification_actions._evaluate_predicate_expression) # carries its own regex per operator but MUST keep its set of operators # identical to this constant — drift between the two sets is a # silent-pass hazard at the predicate layer (a typo would silently pass # the gate that's missing the operator). A gate test asserts the # runtime evaluator recognises every name in this constant.
[docs] def validate_family_identity_uniqueness(family: str, identities: Sequence[str]) -> None: """Refuse a revision declaring two members of ``family`` under one identity. A family inherits across editions by identity, so a revision holding the same identity twice makes the merge unanswerable: neither member can be said to supersede the predecessor's, and neither can be retired without also withdrawing the other. The collision is refused rather than resolved by renaming one of them, because a generated suffix would invent a distinction the corpus does not state. Args: family: The declaration family the identities belong to. identities: Each member's identity, in declaration order. Raises: RegistryValidationError: Two or more members share an identity. """ duplicates = sorted({identity for identity in identities if identities.count(identity) > 1}) if duplicates: rendered = ", ".join(repr(identity) for identity in duplicates) raise RegistryValidationError(f"{family} declares duplicate ids: {rendered}")