feat(questionnaire): integrate revised intake and follow-up monitoring
CI / ruff · pytest (pull_request) Successful in 55s

Add gated TSQ, Russian C-SSRS Recent, durable responses and notifications, diaries, schedules and audited clinical review. Preserve manual assessment when the Russian follow-up source is unavailable.

Refs #52
This commit is contained in:
Pavel Eliseev
2026-10-03 18:19:34 +03:00
parent 8b61eecf1b
commit c42ef621e5
34 changed files with 2556 additions and 85 deletions
+15 -12
View File
@@ -41,9 +41,10 @@ class IntakePolicy(BaseModel):
version: str = Field(min_length=1)
approval: Approval | None = None
cssrs_positive_policy: Literal["pending", "stop_any", "graded"] = "pending"
rus_audit_s_expand_at: int | None = Field(default=None, ge=1, le=12, strict=True)
rus_audit_s_expand_from_zone: int | None = Field(default=None, ge=1, le=4, strict=True)
phq2_expand_at: int = Field(ge=1, le=6, strict=True)
pcptsd5_expand_at: int = Field(ge=1, le=5, strict=True)
tsq_expand_at: int = Field(ge=1, le=10, strict=True)
phq9_psychologist_at: int = Field(ge=0, le=27, strict=True)
phq9_specialist_at: int = Field(ge=0, le=27, strict=True)
phq9_priority_at: int = Field(ge=0, le=27, strict=True)
@@ -56,9 +57,11 @@ class IntakePolicy(BaseModel):
if self.approval and (
self.approval.policy_version != self.version
or self.cssrs_positive_policy == "pending"
or self.rus_audit_s_expand_from_zone is None
or (self.rus_audit_s_expand_from_zone is None and self.rus_audit_s_expand_at is None)
):
raise ValueError("approval requires the matching version and resolved clinical decisions")
if self.rus_audit_s_expand_from_zone is not None and self.rus_audit_s_expand_at is not None:
raise ValueError("choose a total threshold OR a zone for alcohol expansion")
if not self.phq9_psychologist_at < self.phq9_specialist_at < self.phq9_priority_at:
raise ValueError("PHQ-9 thresholds must be ordered")
if self.pcl5_specialist_at >= self.pcl5_priority_at:
@@ -94,7 +97,7 @@ def score_alcohol(answers: dict[str, int], *, short: bool, sex: Sex | None) -> A
for item in ("rus_audit_2", "rus_audit_3"):
if values.get(item, 0) != 0:
raise ValueError("never drinks conflicts with positive consumption answers")
if item not in values:
if item == "rus_audit_2" and item not in values:
values[item] = 0
derived.append(item)
result = score(scale_id, values, sex=sex)
@@ -121,6 +124,8 @@ def alcohol_expansion(result: AlcoholResult, policy: IntakePolicy) -> bool | Non
raise ValueError("expansion requires RUS-AUDIT-S")
if not policy.approval or not result.complete or result.zone is None:
return None
if policy.rus_audit_s_expand_at is not None:
return result.total >= policy.rus_audit_s_expand_at
assert policy.rus_audit_s_expand_from_zone is not None
return result.zone >= policy.rus_audit_s_expand_from_zone
@@ -203,8 +208,8 @@ def _additional_flags(pcl5, alcohol, sex, traumatic_brain_injury, psychotropics,
zone = full.zone if full.complete else short.zone
if zone is not None and zone >= 2:
flags.append("alcohol_zone_ii_or_higher")
if psychotropics:
flags.append("alcohol_psychotropics")
if psychotropics and (any(v > 0 for v in alcohol.values())):
flags.append("alcohol_psychotropics")
if full.zone == 4:
flags.append("alcohol_zone_iv")
except ValueError:
@@ -217,7 +222,7 @@ def _additional_flags(pcl5, alcohol, sex, traumatic_brain_injury, psychotropics,
def evaluate_intake(
*,
phq9: dict[str, int],
pcptsd5: dict[str, int],
tsq: dict[str, int],
pcl5: dict[str, int],
alcohol: dict[str, int],
cssrs: dict[str, int],
@@ -269,12 +274,10 @@ def evaluate_intake(
return IntakeDecision(policy.version, "clinical_review_required", None, (), tuple(flags), c.missing)
phq = score("phq9", phq9)
pc = score("pcptsd5", pcptsd5)
pc = score("tsq", tsq)
pcl = score("pcl5", pcl5)
full_alcohol = score_alcohol(alcohol, short=False, sex=sex)
short_values = {k: v for k, v in alcohol.items() if k in load_scale("rus_audit_s").item_ids}
if "rus_audit_3" in full_alcohol.derived:
short_values["rus_audit_3"] = 0
short_alcohol = score_alcohol(short_values, short=True, sex=sex)
missing = list(c.missing)
phq2_items = ("phq9_1", "phq9_2")
@@ -285,11 +288,11 @@ def evaluate_intake(
missing.extend(k for k in load_scale("phq9").item_ids if k not in phq9)
if trauma_exposure is None:
missing.append("trauma_exposure")
if trauma_exposure is False and any(pcptsd5.values()):
if trauma_exposure is False and tsq:
flags.append("trauma_filter_conflict")
pc_positive = trauma_exposure is True and pc.complete and pc.total >= policy.pcptsd5_expand_at
pc_positive = trauma_exposure is True and pc.complete and pc.total >= policy.tsq_expand_at
if trauma_exposure is True and not pc.complete:
missing.extend(k for k in load_scale("pcptsd5").item_ids if k not in pcptsd5)
missing.extend(k for k in load_scale("tsq").item_ids if k not in tsq)
if pc_positive and not pcl.complete:
missing.extend(k for k in load_scale("pcl5").item_ids if k not in pcl5)
# Partially administered optional scales also cannot be treated as completed negatives.