from __future__ import annotations from dataclasses import dataclass from typing import Any, Mapping, Optional from .evaluator import guard_matches from .invariants import InvariantViolation from .model import StateMachineConfig @dataclass(frozen=True) class Decision: event_id: str from_state: str outcome_index: int to_state: Optional[str] emit: Optional[Mapping[str, Any]] class StateMachineRuntime: def __init__(self, config: StateMachineConfig): self._config = config def dispatch( self, *, current_state: str, event_id: str, context: Mapping[str, Any], ) -> Optional[Decision]: transition = self._config.transitions.get(event_id) if transition is None: raise KeyError(f"Unknown event: {event_id}") if transition.from_states and current_state not in transition.from_states and "*" not in transition.from_states: return None for index, outcome in enumerate(transition.outcomes): if guard_matches(outcome.when, context): return Decision( event_id=event_id, from_state=current_state, outcome_index=index, to_state=outcome.to, emit=outcome.emit, ) return None def check_invariants(self, snapshot: Mapping[str, Any]) -> list[InvariantViolation]: def _match_all(cond: Mapping[str, Any]) -> bool: for key, value in cond.items(): if snapshot.get(key) != value: return False return True violations: list[InvariantViolation] = [] for invariant_id, idef in self._config.invariants.items(): if not isinstance(idef, dict): continue should_check = False when = idef.get("when") when_any = idef.get("when_any") if isinstance(when, dict): should_check = _match_all(when) elif isinstance(when_any, list): for cond in when_any: if isinstance(cond, dict) and _match_all(cond): should_check = True break if not should_check: continue require = idef.get("require") if not isinstance(require, dict): continue for rk, rv in require.items(): if snapshot.get(rk) != rv: violations.append( InvariantViolation( invariant_id=str(invariant_id), message=f"require {rk}={rv}, got {snapshot.get(rk)}", ) ) return violations