chore(core): stage 1 contracts — dialog, API schemas + fixtures, integrations, monitoring, modules
CI / ruff · pytest (pull_request) Successful in 22s

Freeze interfaces between lanes A (bot), B (backend), C (dashboard) so three developers work in
parallel without touching each other's files: dialog Event/Reply + engine registry with echo stub,
integration protocols with in-memory defaults, MonitoringService with noop stub, /api/v1 schemas
with fixture-backed routes, frozen main.py wiring, config/modules.yaml + due_scales(), DAR-5,
tzdata, Andrey trajectory, lanes in WORKFLOW, ADR-010.

Refs #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:22:25 +03:00
co-authored by Claude Opus 5.5
parent a86008f248
commit b4731fb1d6
30 changed files with 1126 additions and 89 deletions
+278
View File
@@ -0,0 +1,278 @@
"""Deterministic fixture data returned by stub routes until #12 implements them (synthetic only).
Lets the dashboard (lane C) be built against the frozen schemas right away. Mirrors the demo cabinet from
docs/product/01_demo_persona.md: veteran Andrey on day 13 (red) plus seven other patients.
"""
from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo
from app.api.schemas import (
AlertOut,
DiscontinuationRisk,
DoseOut,
PatientCard,
PatientListItem,
SeriesPoint,
)
from app.integrations.base import Prescription
TZ = ZoneInfo("Europe/Moscow")
TODAY = date(2026, 10, 3)
DOCTOR_ID = "doc-smirnova"
def _risk(score: int, factors: list[str]) -> DiscontinuationRisk:
level = "high" if score >= 60 else "medium" if score >= 30 else "low"
return DiscontinuationRisk(score=score, level=level, factors=factors)
PATIENTS: list[PatientListItem] = [
PatientListItem(
token="tok-andrey",
display_name="Андрей С.",
age=34,
modules=["veteran", "depression"],
drug="сертралин 50 мг",
therapy_day=13,
next_visit=TODAY + timedelta(days=15),
status_color="red",
top_alert_level="acute",
open_alerts=3,
phq9_last=18,
pcl5_last=44,
trend="up",
adherence_7d=1.0,
discontinuation_risk=_risk(55, ["хотел бросить (день 5)", "алкоголь 3 дня", "настроение ≤ 3"]),
last_event="PHQ-9 п.9 = 2 — кризисный протокол",
),
PatientListItem(
token="tok-dmitry",
display_name="Дмитрий В.",
age=41,
modules=["veteran"],
drug="сертралин 100 мг",
therapy_day=45,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=11,
pcl5_last=48,
trend="up",
adherence_7d=0.86,
discontinuation_risk=_risk(25, ["бессонница"]),
last_event="кошмары, сон < 4 ч 3 ночи",
),
PatientListItem(
token="tok-sergey",
display_name="Сергей П.",
age=37,
modules=["veteran"],
drug="пароксетин 20 мг",
therapy_day=30,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=12,
pcl5_last=41,
trend="flat",
adherence_7d=0.57,
discontinuation_risk=_risk(70, ["пропуски 3 из 7", "нет ответа 2 дня"]),
last_event="пропуски 3 из 7 дней",
),
PatientListItem(
token="tok-nikolay",
display_name="Николай Ф.",
age=52,
modules=["veteran"],
drug="сертралин 50 мг",
therapy_day=14,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=13,
pcl5_last=39,
trend="flat",
adherence_7d=0.86,
discontinuation_risk=_risk(40, ["алкоголь 4 из 7"]),
last_event="алкоголь 4 из 7 дней",
),
PatientListItem(
token="tok-anna",
display_name="Анна Л.",
age=23,
modules=["depression"],
drug="флуоксетин 20 мг",
therapy_day=8,
status_color="yellow",
top_alert_level="info",
open_alerts=1,
phq9_last=15,
trend="flat",
adherence_7d=1.0,
discontinuation_risk=_risk(30, ["беспокойство"]),
last_event="возраст < 25: усиленный мониторинг",
),
PatientListItem(
token="tok-irina",
display_name="Ирина К.",
age=34,
modules=["depression"],
drug="эсциталопрам 10 мг",
therapy_day=21,
status_color="green",
phq9_last=9,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(5, []),
last_event="PHQ-9 16 → 9",
),
PatientListItem(
token="tok-olga",
display_name="Ольга Н.",
age=29,
modules=["depression"],
drug="сертралин 50 мг",
therapy_day=10,
status_color="green",
phq9_last=12,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(10, []),
last_event="тошнота прошла",
),
PatientListItem(
token="tok-pavel",
display_name="Павел Р.",
age=45,
modules=["depression"],
drug="эсциталопрам 10 мг",
therapy_day=60,
status_color="green",
phq9_last=4,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(5, []),
last_event="ремиссия, PHQ-9 = 4",
),
]
# Andrey's 13 days (docs/product/01_demo_persona.md): mood, sleep hours (bucket midpoint), alcohol 0/1/2.
_ANDREY_DAYS = [
(4, 7, 0),
(4, 5, 0),
(4, 5, 0),
(3, 5, 0),
(3, 5, 0),
(4, 7, 0),
(4, 7, 0),
(4, 3, 1),
(4, 3, 2),
(4, 3, 2),
(3, 5, 1),
(3, 5, 0),
(2, 3, 0),
]
def _dt(d: date, hh: int = 9) -> datetime:
return datetime.combine(d, time(hh), TZ)
def andrey_card() -> PatientCard:
start = TODAY - timedelta(days=12)
series: list[SeriesPoint] = []
for i, (mood, sleep, alc) in enumerate(_ANDREY_DAYS):
d = start + timedelta(days=i)
series += [
SeriesPoint(day=d, metric="mood", value=mood),
SeriesPoint(day=d, metric="sleep_hours", value=sleep),
SeriesPoint(day=d, metric="alcohol", value=alc),
]
series += [
SeriesPoint(day=start - timedelta(days=3), metric="phq9", value=14),
SeriesPoint(day=start - timedelta(days=3), metric="pcl5", value=46),
SeriesPoint(day=start - timedelta(days=3), metric="auditc", value=3),
SeriesPoint(day=start + timedelta(days=6), metric="phq9", value=13),
SeriesPoint(day=start + timedelta(days=6), metric="pcl5", value=44),
SeriesPoint(day=TODAY, metric="phq9", value=18, scheduled=False),
]
doses = [
DoseOut(planned_at=_dt(start + timedelta(days=i), 8), drug="сертралин", status="skipped" if i == 4 else "taken")
for i in range(13)
]
alerts = [
AlertOut(
id=3,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(TODAY, 21),
level="acute",
color="red",
code="phq9_item9",
reason="PHQ-9 п.9 = 2 (мысли о смерти/самоповреждении)",
),
AlertOut(
id=2,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=9), 21),
level="moderate",
color="yellow",
code="alcohol_3d",
reason="Алкоголь 3 дня подряд на фоне СИОЗС",
),
AlertOut(
id=1,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=9), 21),
level="moderate",
color="yellow",
code="sleep_lt4_3n",
reason="Сон < 4 ч три ночи подряд",
),
AlertOut(
id=0,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=4), 21),
level="high",
color="yellow",
code="quit_intent",
reason="Намерение прекратить приём",
acked_by=DOCTOR_ID,
acked_at=_dt(start + timedelta(days=5), 11),
),
]
return PatientCard(
patient=PATIENTS[0],
prescriptions=[Prescription(drug="сертралин", dose="50 мг", instructions="утром после еды", start_date=start)],
series=series,
doses=doses,
alerts=alerts,
safety_plan=[
"Мои сигналы: не сплю, пью, «всё бессмысленно»",
"Что помогает: прогулка с собакой, душ",
"Люди и места: брат Олег, спортзал",
"Кому звонить: жена, сослуживец Миша",
"Специалисты: д-р Смирнова, 8-800-2000-122, 112",
"Безопасная среда: ружьё хранится у брата",
],
summary="За 13 дней: приверженность 12/13, эпизод намерения бросить на 5-й день (снят после звонка врача). "
"С 8-го дня сон < 4 ч и алкоголь; к 13-му дню настроение 2/10, внеплановый PHQ-9 = 18 (рост с 13), "
"п.9 = 2. Обсудить: безопасность, коррекция терапии сна, алкоголь.",
)
def card(token: str) -> PatientCard | None:
if token == "tok-andrey":
return andrey_card()
item = next((p for p in PATIENTS if p.token == token), None)
if item is None:
return None
return PatientCard(patient=item, prescriptions=[], series=[], doses=[], alerts=[], summary=item.last_event)
def all_alerts() -> list[AlertOut]:
return andrey_card().alerts