"""Monitoring service contract (frozen, #34). Split of responsibilities (ADR-007): * Lane B (#38) implements `MonitoringService`: builds a patient snapshot from the DB, runs the deterministic rules (`app.core.rules.evaluate`), persists `alert`/`crisis` actions and notifies the doctor itself. * It returns only the *dialog* actions (schedule_scale, med_info, reach_out, show_safety_plan, suggest_visit, crisis) so the engine (lane A, #10) can react in the conversation. The engine never creates alerts. """ from collections.abc import Callable from importlib import import_module, util from typing import Protocol from app.core.rules import Action class MonitoringService(Protocol): async def after_data_change(self, token: str) -> list[Action]: ... async def is_in_crisis(self, token: str) -> bool: """True while an `acute` alert is open (not acked by the doctor) — FSM stays in `crisis`.""" ... class NoopMonitoring: """Placeholder until #38 lands.""" async def after_data_change(self, token: str) -> list[Action]: return [] async def is_in_crisis(self, token: str) -> bool: return False _factory: Callable[[], MonitoringService] = NoopMonitoring def register_monitoring(factory: Callable[[], MonitoringService]) -> None: global _factory _factory = factory def get_monitoring() -> MonitoringService: # find_spec instead of try/except ImportError: real import errors inside the module must surface. if util.find_spec("app.services.monitoring") is not None: import_module("app.services.monitoring") return _factory()