diff --git a/Resources/JITAI_Task_Overview.docx b/Resources/JITAI_Task_Overview.docx index 321d216..d626128 100644 Binary files a/Resources/JITAI_Task_Overview.docx and b/Resources/JITAI_Task_Overview.docx differ diff --git a/Resources/REACT_Architecture.docx b/Resources/REACT_Architecture.docx index 873f9d1..372008b 100644 Binary files a/Resources/REACT_Architecture.docx and b/Resources/REACT_Architecture.docx differ diff --git a/backend/app/distress.py b/backend/app/distress.py index c6b8771..61c79ee 100644 --- a/backend/app/distress.py +++ b/backend/app/distress.py @@ -117,7 +117,7 @@ def raise_baseline_flag(user, signals): def active_distress_flag(user, now=None): now = now or timezone.now() - return ( + candidates = ( DistressFlag.objects .filter(user=user) .filter( @@ -125,8 +125,11 @@ def active_distress_flag(user, now=None): | Q(source='momentary', expires_at__gt=now) ) .order_by('source', '-raised_at') - .first() ) + for flag in candidates: + if flag.is_active(now): + return flag + return None def resource_card(): diff --git a/backend/app/models.py b/backend/app/models.py index 7d88325..db149cd 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -439,7 +439,10 @@ class Meta: def is_active(self, now=None): now = now or timezone.now() if self.source == 'baseline': - return self.contact_documented_at is None + from dashboard.data.config import BASELINE_ALERT_ONLY_SIGNALS + suppresses = not self.signals or any( + code not in BASELINE_ALERT_ONLY_SIGNALS for code in self.signals) + return suppresses and self.contact_documented_at is None return self.expires_at is not None and now < self.expires_at def __str__(self): diff --git a/backend/app/tests.py b/backend/app/tests.py index 08ab0db..90a43e4 100644 --- a/backend/app/tests.py +++ b/backend/app/tests.py @@ -5423,6 +5423,27 @@ def test_a_baseline_flag_does_not_expire_with_time(self): raised_at=self.T - timedelta(days=400)) self.assertIsNotNone(active_distress_flag(self.user, now=self.T)) + def test_alert_only_baseline_signals_do_not_suppress(self): + from app.distress import active_distress_flag + for signals in (['audit_c'], ['problem_gambling'], ['audit_c', 'problem_gambling']): + flag = DistressFlag.objects.create(user=self.user, source='baseline', signals=signals) + self.assertFalse(flag.is_active(self.T)) + self.assertIsNone(active_distress_flag(self.user, now=self.T)) + + def test_a_baseline_flag_with_any_suppressing_signal_still_suppresses(self): + from app.distress import active_distress_flag + for signals in (['audit_c', 'scoff'], ['phq9_self_harm'], ['food_insecurity'], []): + DistressFlag.objects.all().delete() + flag = DistressFlag.objects.create(user=self.user, source='baseline', signals=signals) + self.assertTrue(flag.is_active(self.T)) + self.assertEqual(active_distress_flag(self.user, now=self.T), flag) + + def test_an_alert_only_flag_does_not_hide_a_suppressing_one(self): + from app.distress import active_distress_flag + DistressFlag.objects.create(user=self.user, source='baseline', signals=['audit_c']) + suppressing = DistressFlag.objects.create(user=self.user, source='baseline', signals=['scoff']) + self.assertEqual(active_distress_flag(self.user, now=self.T), suppressing) + def test_a_momentary_flag_with_no_expiry_is_inactive(self): from app.distress import active_distress_flag self._momentary(expires_at=None) diff --git a/dashboard/data/config.py b/dashboard/data/config.py index ae9fe1e..18f47b8 100644 --- a/dashboard/data/config.py +++ b/dashboard/data/config.py @@ -20,6 +20,7 @@ __all__ = [ 'ARM_RANDOMIZATION_P_DEFAULT', 'ARM_RANDOMIZATION_P_ENV', + 'BASELINE_ALERT_ONLY_SIGNALS', 'BENCHMARKS', 'CHECKIN_REMINDER_DELAY_MINUTES', 'DAILY_PROMPT_CAP', @@ -93,6 +94,13 @@ def _int_list_from_env(name): DISTRESS_MOMENTARY_PAUSE_HOURS = 24 +# Baseline signals that are a note in the record and a conversation at the next +# visit, nothing more. They never suppress randomization (Dr. Chang, 2026-10-05): +# suppressing every alcohol or gambling screen-positive would remove the +# participants the study is about. PHQ-9, SCOFF and the Hunger Vital Sign still +# suppress. +BASELINE_ALERT_ONLY_SIGNALS = ('audit_c', 'problem_gambling') + # Momentary distress override cutoffs, confirmed by Dr. Chang 2026-09-22: exact # scale-endpoint values, not thresholds. Separate from ROUTING_TRIGGER_RULES in # app/ema_catalog.py:174-230 (coping-message topic selection only) -- do not