Signal/python-backend/core/rollup.py
Kisa bd92126384 feat: patient rollup + nested patients payload (plan 02, P3)
core/rollup.py implements the locked truth table: worst gradeable line wins;
a Plan Type Needed line caps the patient at Action Needed; non-gradeable
lines are displayed but never touch the light; no-graded-lines patients carry
None (no label invented). /api/upload gains additive patients[] (one entry
per patient, nested device lines with doc checklists, citations, timing
flags, dedup display fields) and patient_stats (counts patients, not rows).
Flat records payload untouched until P6. Independent audit: SHIP; its LOW
findings fixed (date coercion on timing keys, empty-rollup guard, severity
divergence documented). 151 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 05:20:45 -04:00

101 lines
3.7 KiB
Python

"""
rollup.py
Signal — STTIL Solutions
Patient-level rollup of coverage-line readiness verdicts (build plan 02, P3).
Design locked in readiness-model-brief section 4 and the phase-2 design Item 3:
- The light = worst gradeable line. Severity (worst first):
Plan Type Needed > At Risk > Action Needed > On Track > Clear to Ship.
- Any gradeable Plan Type Needed line caps the patient no greener than
Action Needed (the patient IS being served; one unmapped plan type must not
read as the whole patient being ungradeable).
- Non-gradeable lines (non-CGM) never participate in the light or the counts.
- Tabs count patients, not rows.
A patient with no graded gradeable lines has rollup_status None — the UI
treatment is an open Kisa question; no label is invented here.
PHI CONTRACT: operates on patient_id only; this module logs nothing.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
from core.dedup import CoverageLine
from core.readiness import LineStatus
# Higher = worse. Plan Type Needed ranks worst for LINE severity ordering;
# its patient-level effect is the capping rule below, not raw worstness.
SEVERITY = {
LineStatus.CLEAR_TO_SHIP: 0,
LineStatus.ON_TRACK: 1,
LineStatus.ACTION_NEEDED: 2,
LineStatus.AT_RISK: 3,
LineStatus.PLAN_TYPE_NEEDED: 4,
}
@dataclass
class PatientRollup:
patient_id: str
lines: list[CoverageLine] # all lines, incl. non-gradeable
rollup_status: Optional[LineStatus] # None = no graded gradeable lines
cgm_line_count: int # gradeable lines
total_line_count: int # all lines
plan_type_needed: bool # any gradeable line in PTN
def rollup_patient(lines: list[CoverageLine]) -> PatientRollup:
"""Roll one patient's coverage lines into a single verdict, per the
locked truth table (plan 02 P3):
| gradeable statuses present | rollup_status |
| none | None |
| all Plan Type Needed | Plan Type Needed |
| some PTN + others | worse of (Action Needed, worst other)|
| no PTN | worst by severity |
"""
if not lines:
raise ValueError("rollup_patient requires at least one CoverageLine")
graded = [
l for l in lines if l.gradeable and l.line_status is not None
]
statuses = [LineStatus(l.line_status) for l in graded]
ptn = [s for s in statuses if s == LineStatus.PLAN_TYPE_NEEDED]
if not statuses:
status: Optional[LineStatus] = None
elif len(ptn) == len(statuses):
status = LineStatus.PLAN_TYPE_NEEDED
elif ptn:
worst_other = max(
(s for s in statuses if s != LineStatus.PLAN_TYPE_NEEDED),
key=SEVERITY.get,
)
status = max(worst_other, LineStatus.ACTION_NEEDED, key=SEVERITY.get)
else:
status = max(statuses, key=SEVERITY.get)
return PatientRollup(
patient_id=lines[0].patient_id,
lines=lines,
rollup_status=status,
cgm_line_count=sum(1 for l in lines if l.gradeable),
total_line_count=len(lines),
plan_type_needed=bool(ptn),
)
def rollup_patients(lines: dict) -> dict[str, PatientRollup]:
"""Group CoverageLines (any keying) by patient and roll each up."""
by_patient: dict[str, list[CoverageLine]] = defaultdict(list)
for line in lines.values():
by_patient[line.patient_id].append(line)
return {
patient_id: rollup_patient(patient_lines)
for patient_id, patient_lines in by_patient.items()
}