feat: garden review fixes — honest dose answer grows it, quiet after crisis, no-promise texts (ADR-015)
CI / ruff · pytest (pull_request) Successful in 2m21s
CI / ruff · pytest (pull_request) Successful in 2m21s
Refs #120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+23
-6
@@ -5,11 +5,13 @@ activity rows — no own table, no migration (ADR-003), and changing the rules r
|
||||
|
||||
«Without pressure»: points only add up (a skipped day takes nothing away), growth counts the fact of an activity
|
||||
and never its content (scale scores, mood), and there is no comparison between patients. Only the patient sees it.
|
||||
An honest «Пропущу» on a dose grows the garden like «Принял(а)», so the garden never rewards a false «taken» (#120).
|
||||
After a recent acute/high alert the garden is `quiet`: growth stays, celebration does not.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
@@ -21,7 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core import clock
|
||||
from app.core.intake_questions import DIARY_KINDS
|
||||
from app.db.clinical import DailyCheckin, Dose, ScaleResult
|
||||
from app.db.clinical import Alert, DailyCheckin, Dose, ScaleResult
|
||||
from app.db.intake import QuestionnaireRecord
|
||||
|
||||
Activity = Literal["meds", "diary", "questionnaire"]
|
||||
@@ -37,8 +39,15 @@ class TitleRule(BaseModel):
|
||||
title: str
|
||||
|
||||
|
||||
class QuietRule(BaseModel):
|
||||
alert_levels: list[str]
|
||||
hours: int
|
||||
|
||||
|
||||
class GardenConfig(BaseModel):
|
||||
activities: dict[Activity, ActivityRule]
|
||||
answered_dose_statuses: list[str]
|
||||
quiet: QuietRule
|
||||
step_points: int
|
||||
stages_per_plant: int
|
||||
max_plants: int
|
||||
@@ -73,6 +82,7 @@ class Garden(BaseModel):
|
||||
points_in_step: int # progress towards the next growth step: «8 из 10»
|
||||
plants: list[Plant]
|
||||
today: list[TodayItem]
|
||||
quiet: bool = False # recent acute/high alert: no «Сад подрос», no grow-in animation
|
||||
|
||||
|
||||
def compute(events: Iterable[tuple[Activity, date]], today: date, cfg: GardenConfig | None = None) -> Garden:
|
||||
@@ -113,7 +123,7 @@ async def garden_for(s: AsyncSession, token: str) -> Garden:
|
||||
|
||||
doses = (await s.scalars(select(Dose).where(Dose.token == token))).all()
|
||||
for d in doses:
|
||||
if d.status == "taken":
|
||||
if d.status in cfg.answered_dose_statuses:
|
||||
events.append(("meds", _day(d.answered_at or d.planned_at, tz)))
|
||||
checkins = (await s.scalars(select(DailyCheckin).where(DailyCheckin.token == token))).all()
|
||||
for c in checkins:
|
||||
@@ -133,19 +143,26 @@ async def garden_for(s: AsyncSession, token: str) -> Garden:
|
||||
events.append(("questionnaire", _day(sc.ts, tz)))
|
||||
|
||||
garden = compute(events, today, cfg)
|
||||
since = now - timedelta(hours=cfg.quiet.hours)
|
||||
recent_alert = await s.scalar(
|
||||
select(Alert.id)
|
||||
.where(Alert.token == token, Alert.level.in_(cfg.quiet.alert_levels), Alert.ts >= since, Alert.ts <= now)
|
||||
.limit(1)
|
||||
)
|
||||
garden.quiet = recent_alert is not None
|
||||
done_today = {kind for kind, day in events if day == today}
|
||||
|
||||
items: list[TodayItem] = []
|
||||
due = [d for d in doses if _day(d.planned_at, tz) == today]
|
||||
if due:
|
||||
taken = sum(d.status == "taken" for d in due)
|
||||
answered = sum(d.status in cfg.answered_dose_statuses for d in due)
|
||||
due_now = sum(d.planned_at <= now for d in due)
|
||||
items.append(
|
||||
TodayItem(
|
||||
activity="meds",
|
||||
title=cfg.today["meds"],
|
||||
done=taken > 0 and taken >= due_now,
|
||||
detail=f"{taken} из {len(due)}",
|
||||
done=answered > 0 and answered >= due_now,
|
||||
detail=f"{answered} из {len(due)}",
|
||||
)
|
||||
)
|
||||
items.append(TodayItem(activity="diary", title=cfg.today["diary"], done="diary" in done_today))
|
||||
|
||||
Reference in New Issue
Block a user