"""Clinical modules config (`config/modules.yaml`, ADR-001) and scale scheduling. Shared by the dialog engine (lane A: start screening, daily question) and the scheduler (lane B: due scales). """ from datetime import date from functools import lru_cache from pathlib import Path from typing import Literal import yaml from pydantic import BaseModel, ConfigDict, Field from app.config import get_settings ModuleId = Literal["veteran", "depression", "family"] class _Strict(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) class ScheduleEntry(_Strict): scale: str every_days: int = Field(gt=0) from_day: int = 0 until_day: int | None = None class Threshold(_Strict): scale: str op: Literal[">=", ">"] value: int class ModuleDef(_Strict): title: str start_scales: list[str] start_questions: list[str] = Field(default_factory=list) daily_question: str | None = None schedule: list[ScheduleEntry] = Field(default_factory=list) enables_depression_if: Threshold | None = None enhanced_monitoring_under_age: int | None = None class CoreDef(_Strict): daily: list[str] silence_alert_days: int item9_followup: str class ModulesConfig(_Strict): core: CoreDef modules: dict[ModuleId, ModuleDef] @lru_cache def load_modules(path: Path | None = None) -> ModulesConfig: path = path or get_settings().config_dir / "modules.yaml" return ModulesConfig.model_validate(yaml.safe_load(path.read_text(encoding="utf-8"))) def due_scales( modules: set[str], therapy_day: int, last_done: dict[str, date], today: date, config: ModulesConfig | None = None ) -> list[str]: """Scales due today for a patient: per enabled module, a scale is due if never done in its window or `every_days` have passed since it was last completed. Deduplicated, in config order.""" cfg = config or load_modules() out: list[str] = [] for mod_id, mod in cfg.modules.items(): if mod_id not in modules: continue for e in mod.schedule: if therapy_day < e.from_day or (e.until_day is not None and therapy_day > e.until_day): continue last = last_done.get(e.scale) if (last is None or (today - last).days >= e.every_days) and e.scale not in out: out.append(e.scale) return out