"""Permissive Engine — pre-condiciones declarativas para acciones de control. Cada acción de control crítica (arrancar motor, abrir válvula de mar, etc.) debe pasar TODAS sus pre-condiciones antes de ejecutarse. Las pre-condiciones se evalúan en el **servidor** del Runtime, no en la UI — única fuente de verdad (Parte 1 sec 9). Estados posibles de cada permissive: - **OK**: condición cumplida. - **FAIL**: condición no cumplida, BLOQUEA acción. - **WARNING**: el sensor que debería verificar la condición no existe o está en estado inválido. Requiere override consciente del Admin del buque. - **N/A**: la pre-condición no aplica a este buque. """ from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator OperatorStr = Literal["==", "!=", ">", "<", ">=", "<=", "between", "is_true", "is_false"] class Condition(BaseModel): """Una pre-condición evaluable contra el valor de un tag.""" model_config = ConfigDict(extra="forbid") tag_ref: str = Field( ..., min_length=1, max_length=128, description="Tag.id a evaluar.", ) operator: OperatorStr = Field( ..., description="Operador de comparación. 'between' usa threshold_low + threshold_high.", ) threshold: float | None = Field( default=None, description="Para operadores escalares (==, !=, >, <, >=, <=).", ) threshold_low: float | None = Field( default=None, description="Para 'between' (límite inferior)." ) threshold_high: float | None = Field( default=None, description="Para 'between' (límite superior)." ) severity: Literal["fail", "warning"] = Field( default="fail", description=( "'fail' bloquea la acción. 'warning' permite pero exige override " "consciente del Admin del buque." ), ) message_on_fail: str = Field( default="", max_length=512, description="Mensaje al operador si falla." ) @model_validator(mode="after") def _between_requires_both(self) -> Condition: if self.operator == "between" and ( self.threshold_low is None or self.threshold_high is None ): raise ValueError( "Operator 'between' requiere threshold_low y threshold_high." ) if self.operator in ("==", "!=", ">", "<", ">=", "<=") and self.threshold is None: raise ValueError( f"Operator '{self.operator}' requiere threshold." ) return self class PermissiveRule(BaseModel): """Conjunto de pre-condiciones que rigen una acción de control.""" model_config = ConfigDict(extra="forbid") id: str = Field(..., min_length=1, max_length=128) action_id: str = Field( ..., min_length=1, max_length=128, description="ID de la acción que protege, ej: 'START_ME_PORT'.", ) description: str = Field(default="", max_length=512) conditions: list[Condition] = Field(default_factory=list) on_fail_message: str = Field( default="", max_length=512, description="Mensaje agregado al operador cuando el conjunto falla.", ) @field_validator("conditions") @classmethod def _at_least_one_condition(cls, v: list[Condition]) -> list[Condition]: if not v: raise ValueError( "PermissiveRule requiere al menos 1 condition (use lista vacía solo si " "la acción no tiene permissives, en cuyo caso no debería existir esta regla)." ) return v