Signal/docs/superpowers/plans/2026-06-07-signal-demo-mvp-build.md
Kisa bcb1a10fe6 feat: demo MVP — 90/60/45 priority tiers, doc state machine, confirm visit workflow
- New CoverageFlag enum: SUPPLY_LAPSED, VISIT_REQUIRED, TRANSFER_PENDING,
  RENEWAL_CRITICAL/ELEVATED/SOON, RESUPPLY_READY, ACTIVE
- Doc state machine: 5-item payer-dependent status per patient (SWO, Visit,
  PECOS, PA, Diagnosis) with cascade chain
- Confirm Visit endpoint: staff enters prescriber-confirmed date, persisted in
  Supabase confirmed_visits table, survives all future CSV imports
- Supabase migration: 001_add_confirmed_visits.sql (run manually in SQL editor)
- Frontend: Badge rebuilt for 8 flags, DocStatusBar 5-dot display,
  ConfirmVisitModal, expandable WorklistTable rows
- Legal: LOI, NDA, BAA drafts at pitch/legal/ for Nixon Law Group review
- Compliance docs: privacy policy, incident response, data handling
- CSV generator: market_data.json + PA/NJ generator scripts
- 15/15 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:29:33 -04:00

71 KiB

Signal Demo MVP — Core Build Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build Confirm Visit workflow, 90/60/45 priority system, full documentation state machine, CSV cycle reset diff, transferred patient detection, and polished worklist UI for the Robert Robinson (Gaboro) demo on June 10, 2026.

Architecture: Extend the existing FastAPI backend (Railway) with new status flags, a doc state machine, confirmed visit persistence in Supabase, and a Confirm Visit API endpoint. Extend the React/Vite frontend (Vercel) with expandable rows, a 5-dot doc status bar, and the Confirm Visit modal that updates live without a page refresh.

Tech Stack: Python 3.11 / FastAPI (Railway), Supabase (PostgreSQL + RLS), React 18 / Vite / Tailwind CSS (pnpm), Clerk auth (JWT v2)

Key URLs:

PHI Contract (non-negotiable): All backend code may only touch: patient_id, device_type, shipment_date, quantity, payer, component, and doc status fields (swo_status, pa_status, etc.). No patient names, SSNs, DOBs, addresses, or contact info. patient_id is hashed before storage.

Clerk org for Gaboro: org_3EPAEcAw06V2yGMSkxE3UjqIA3c (already in CLAUDE.md + provisioned in Supabase via DEMO_ORG_SLUG="gaboro-pilot")


File Map

Create:

  • python-backend/core/doc_state_machine.py — DocState dataclass, payer-dependent doc requirements, cascade logic
  • python-backend/db/migrations/001_add_confirmed_visits.sql — Supabase migration for confirmed_visits table
  • signal-ui/src/components/DocStatusBar.jsx — 5 colored dots, one per doc item
  • signal-ui/src/components/ConfirmVisitModal.jsx — date input modal with validation
  • signal-ui/src/components/CascadeAlert.jsx — cascade consequence chain display
  • tests/test_doc_state_machine.py
  • tests/test_coverage_flags.py

Modify:

  • python-backend/core/coverage_calculator.py — new CoverageFlag enum, 90/60/45 visit tier logic, visit date priority chain, updated ShipmentRecord with optional doc fields
  • python-backend/api/main.py — updated RecordOut (doc_state, visit_confidence, cascade, is_transfer), new confirm-visit endpoint, updated FLAG_LABELS
  • python-backend/api/normalizer.py — add doc columns + transfer columns to HEADER_MAP, update ShipmentRecord construction
  • python-backend/core/persistence.py — add get_confirmed_visit(), upsert_confirmed_visit(), load_confirmed_visits_for_org()
  • python-backend/config/payer_rules.json — add medicare_advantage section, pa_required/pecos_required flags
  • python-backend/db/schema.sql — add confirmed_visits table definition
  • signal-ui/src/components/WorklistTable.jsx — expandable rows, DocStatusBar, Confirm Visit button, cascade display
  • signal-ui/src/components/Badge.jsx — new status labels + visual weight (bold/normal per tier)
  • signal-ui/src/lib/api.js — add confirmVisit() function

Task 1: Extend ShipmentRecord with optional doc fields

Files:

  • Modify: python-backend/core/coverage_calculator.py:42-55

Read python-backend/core/coverage_calculator.py before editing.

  • Step 1: Write the failing test

Create tests/test_coverage_flags.py:

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))

from datetime import date, timedelta
from core.coverage_calculator import ShipmentRecord, calculate_coverage, CoverageFlag

TODAY = date.today()

def make_record(**kwargs):
    defaults = dict(
        patient_id="PT001",
        device_type="dexcom_g7",
        shipment_date=TODAY - timedelta(days=10),
        quantity=1,
        payer="medicare",
        component="sensor",
    )
    defaults.update(kwargs)
    return ShipmentRecord(**defaults)

def test_shipment_record_accepts_csv_visit_date():
    r = make_record(csv_visit_date=TODAY - timedelta(days=30))
    assert r.csv_visit_date == TODAY - timedelta(days=30)

def test_shipment_record_accepts_doc_fields():
    r = make_record(
        csv_swo_status="On File",
        csv_pecos_verified="Yes",
        csv_pa_status="Not Required",
        csv_diagnosis_on_file="Yes",
    )
    assert r.csv_swo_status == "On File"
    assert r.csv_pecos_verified == "Yes"

def test_shipment_record_doc_fields_default_none():
    r = make_record()
    assert r.csv_visit_date is None
    assert r.csv_swo_status is None
    assert r.csv_transfer_from is None
  • Step 2: Run test — verify it fails
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_coverage_flags.py -v

Expected: FAIL — ShipmentRecord has no field csv_visit_date

  • Step 3: Add optional doc fields to ShipmentRecord

Replace the existing ShipmentRecord dataclass (lines ~42-55) with:

from typing import Optional  # add to imports if not already present

@dataclass(frozen=True)
class ShipmentRecord:
    """
    Minimal shipment record. Only non-PHI fields allowed.

    patient_id: Supplier's internal MRN or account number.
                This is the sole crosswalk key — no real identity data here.
    csv_* fields: Optional doc status columns parsed from the CSV.
    csv_transfer_from: Non-empty string means this is a transferred patient.
    """
    patient_id: str
    device_type: str
    shipment_date: date
    quantity: int
    payer: str
    component: str = "sensor"
    # Optional doc fields — populated when CSV contains these columns
    csv_visit_date: Optional[date] = None
    csv_swo_status: Optional[str] = None      # "On File" | "Pending" | "Expired"
    csv_pecos_verified: Optional[str] = None  # "Yes" | "No"
    csv_pa_status: Optional[str] = None       # "Approved" | "Pending" | "Denied" | "Not Required"
    csv_diagnosis_on_file: Optional[str] = None  # "Yes" | "No"
    csv_transfer_from: Optional[str] = None   # prior supplier name if transfer
  • Step 4: Run test — verify it passes
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_coverage_flags.py::test_shipment_record_accepts_csv_visit_date tests/test_coverage_flags.py::test_shipment_record_doc_fields_default_none -v

Expected: PASS

  • Step 5: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/core/coverage_calculator.py tests/test_coverage_flags.py
git commit -m "feat: extend ShipmentRecord with optional doc state fields from CSV"

Task 2: New CoverageFlag enum + 90/60/45 priority system

Files:

  • Modify: python-backend/core/coverage_calculator.py

  • Step 1: Write failing tests

Add to tests/test_coverage_flags.py:

def test_supply_lapsed_flag_when_coverage_ended():
    # Shipment 200 days ago, 1 sensor (10 wear days) — coverage ended long ago
    r = make_record(shipment_date=TODAY - timedelta(days=200), quantity=1)
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.SUPPLY_LAPSED

def test_renewal_critical_at_45_days():
    # Last confirmed visit was 135 days ago — next visit due in 45 days
    r = make_record(
        shipment_date=TODAY - timedelta(days=10),
        quantity=1,
        csv_visit_date=TODAY - timedelta(days=135),
    )
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.RENEWAL_CRITICAL

def test_renewal_elevated_at_60_days():
    r = make_record(
        shipment_date=TODAY - timedelta(days=10),
        quantity=1,
        csv_visit_date=TODAY - timedelta(days=120),
    )
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.RENEWAL_ELEVATED

def test_renewal_soon_at_90_days():
    r = make_record(
        shipment_date=TODAY - timedelta(days=10),
        quantity=1,
        csv_visit_date=TODAY - timedelta(days=90),
    )
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.RENEWAL_SOON

def test_active_when_visit_far_away():
    r = make_record(
        shipment_date=TODAY - timedelta(days=10),
        quantity=1,
        csv_visit_date=TODAY - timedelta(days=10),
    )
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.ACTIVE

def test_transfer_pending_flag():
    r = make_record(csv_transfer_from="Prior Supplier LLC")
    result = calculate_coverage(r)
    assert result.flag == CoverageFlag.TRANSFER_PENDING

def test_priority_sort_order():
    lapsed = make_record(shipment_date=TODAY - timedelta(days=200))
    critical = make_record(patient_id="PT002", shipment_date=TODAY - timedelta(days=10),
                           csv_visit_date=TODAY - timedelta(days=135))
    active = make_record(patient_id="PT003", shipment_date=TODAY - timedelta(days=10),
                         csv_visit_date=TODAY - timedelta(days=10))
    from core.coverage_calculator import calculate_batch
    results = calculate_batch([active, critical, lapsed])
    assert results[0].flag == CoverageFlag.SUPPLY_LAPSED
    assert results[1].flag == CoverageFlag.RENEWAL_CRITICAL
    assert results[2].flag == CoverageFlag.ACTIVE
  • Step 2: Run tests — verify they fail
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_coverage_flags.py -v 2>&1 | head -40
  • Step 3: Replace CoverageFlag enum + update calculate_coverage

In python-backend/core/coverage_calculator.py, replace the entire CoverageFlag enum and CoverageResult dataclass and all calculation functions with the following. Read the existing file first to preserve imports.

New CoverageFlag enum (replace existing):

class CoverageFlag(str, Enum):
    SUPPLY_LAPSED     = "SUPPLY_LAPSED"      # supply cycle ended, no new shipment
    VISIT_REQUIRED    = "VISIT_REQUIRED"     # visit date past due, no confirmed new visit
    TRANSFER_PENDING  = "TRANSFER_PENDING"   # transferred patient, all docs need verification
    RENEWAL_CRITICAL  = "RENEWAL_CRITICAL"   # ≤ 45 days to next visit due
    RENEWAL_ELEVATED  = "RENEWAL_ELEVATED"   # ≤ 60 days to next visit due
    RENEWAL_SOON      = "RENEWAL_SOON"       # ≤ 90 days to next visit due
    RESUPPLY_READY    = "RESUPPLY_READY"     # within refill window, visit not yet urgent
    ACTIVE            = "ACTIVE"             # all clear

New CoverageResult dataclass (replace existing):

@dataclass
class CoverageResult:
    patient_id: str
    device_type: str
    payer: str
    component: str
    last_shipment_date: date
    coverage_end_date: date
    next_visit_due_date: Optional[date]
    flag: CoverageFlag
    days_until_coverage_end: int
    days_until_visit_due: Optional[int]
    priority_score: int
    visit_date_confidence: str = "estimated"  # "confirmed" | "estimated"
    is_transfer: bool = False
    rule_version: str = RULE_VERSION

New helper functions (replace _compute_priority):

def _resolve_visit_date(
    record: ShipmentRecord,
    confirmed_visit_date: Optional[date] = None,
) -> tuple[Optional[date], str]:
    """
    Visit date priority chain:
    1. CSV visit date column (if present + valid)
    2. Supabase confirmed visit date (stored by staff)
    3. Estimated proxy: shipment_date - 30 days
    Returns (resolved_date, confidence) where confidence is 'confirmed' or 'estimated'.
    """
    if record.csv_visit_date:
        return record.csv_visit_date, "confirmed"
    if confirmed_visit_date:
        return confirmed_visit_date, "confirmed"
    estimated = record.shipment_date - timedelta(days=30)
    return estimated, "estimated"


def _compute_visit_flag(
    next_visit_due: date,
    today: date,
) -> CoverageFlag:
    """Map days-to-next-visit to the appropriate tier flag."""
    days = (next_visit_due - today).days
    if days < 0:
        return CoverageFlag.VISIT_REQUIRED
    if days <= 45:
        return CoverageFlag.RENEWAL_CRITICAL
    if days <= 60:
        return CoverageFlag.RENEWAL_ELEVATED
    if days <= 90:
        return CoverageFlag.RENEWAL_SOON
    return CoverageFlag.ACTIVE


def _compute_priority(flag: CoverageFlag, days_until_visit: Optional[int]) -> int:
    """
    Priority score for worklist sort. Higher = more urgent.
    Supply Lapsed and Transfer Pending are always highest.
    Within visit tiers, urgency increases as days decrease.
    """
    urgency_days = abs(days_until_visit) if days_until_visit is not None else 0
    if flag == CoverageFlag.SUPPLY_LAPSED:
        return 2000 + urgency_days
    if flag == CoverageFlag.TRANSFER_PENDING:
        return 1800
    if flag == CoverageFlag.VISIT_REQUIRED:
        return 1500 + urgency_days
    if flag == CoverageFlag.RENEWAL_CRITICAL:
        return 1000 + (45 - max(0, urgency_days))
    if flag == CoverageFlag.RENEWAL_ELEVATED:
        return 700 + (60 - max(0, urgency_days))
    if flag == CoverageFlag.RENEWAL_SOON:
        return 400 + (90 - max(0, urgency_days))
    if flag == CoverageFlag.RESUPPLY_READY:
        return 200
    return 0  # ACTIVE

New calculate_coverage function (replace existing):

def calculate_coverage(
    record: ShipmentRecord,
    as_of: Optional[date] = None,
    confirmed_visit_date: Optional[date] = None,
) -> CoverageResult:
    """
    Calculate coverage status for a single shipment record.

    Args:
        record: ShipmentRecord with non-PHI fields only.
        as_of:  Date to evaluate against. Defaults to today.
        confirmed_visit_date: Confirmed visit date from Supabase (staff-entered).
    """
    rules = _load_payer_rules()
    today = as_of or date.today()

    # Transferred patients: all docs need verification before anything else
    if record.csv_transfer_from:
        return CoverageResult(
            patient_id=record.patient_id,
            device_type=record.device_type,
            payer=record.payer,
            component=record.component,
            last_shipment_date=record.shipment_date,
            coverage_end_date=record.shipment_date,
            next_visit_due_date=None,
            flag=CoverageFlag.TRANSFER_PENDING,
            days_until_coverage_end=0,
            days_until_visit_due=None,
            priority_score=1800,
            visit_date_confidence="estimated",
            is_transfer=True,
            rule_version=RULE_VERSION,
        )

    wear_days = _get_wear_days(rules, record.device_type, record.component)
    payer_config = _get_payer_config(rules, record.payer)

    total_wear_days = wear_days * record.quantity
    coverage_end = record.shipment_date + timedelta(days=total_wear_days)
    days_until_end = (coverage_end - today).days

    # Supply lapsed — coverage cycle ended
    if days_until_end < 0:
        visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
        next_visit_due = visit_date + timedelta(days=180) if visit_date else None
        days_visit = (next_visit_due - today).days if next_visit_due else None
        return CoverageResult(
            patient_id=record.patient_id,
            device_type=record.device_type,
            payer=record.payer,
            component=record.component,
            last_shipment_date=record.shipment_date,
            coverage_end_date=coverage_end,
            next_visit_due_date=next_visit_due,
            flag=CoverageFlag.SUPPLY_LAPSED,
            days_until_coverage_end=days_until_end,
            days_until_visit_due=days_visit,
            priority_score=_compute_priority(CoverageFlag.SUPPLY_LAPSED, days_visit),
            visit_date_confidence=confidence,
            rule_version=RULE_VERSION,
        )

    visit_renewal_days = payer_config.get("visit_renewal_days")
    refill_window_days = payer_config.get("refill_window_days", 30)

    # Resolve visit date using priority chain
    visit_date, confidence = _resolve_visit_date(record, confirmed_visit_date)
    next_visit_due: Optional[date] = None
    days_until_visit: Optional[int] = None
    flag = CoverageFlag.ACTIVE

    if visit_renewal_days and visit_date:
        next_visit_due = visit_date + timedelta(days=visit_renewal_days)
        days_until_visit = (next_visit_due - today).days
        flag = _compute_visit_flag(next_visit_due, today)

    # If no visit urgency but within refill window, mark resupply ready
    if flag == CoverageFlag.ACTIVE and days_until_end <= refill_window_days:
        flag = CoverageFlag.RESUPPLY_READY

    priority = _compute_priority(flag, days_until_visit)

    return CoverageResult(
        patient_id=record.patient_id,
        device_type=record.device_type,
        payer=record.payer,
        component=record.component,
        last_shipment_date=record.shipment_date,
        coverage_end_date=coverage_end,
        next_visit_due_date=next_visit_due,
        flag=flag,
        days_until_coverage_end=days_until_end,
        days_until_visit_due=days_until_visit,
        priority_score=priority,
        visit_date_confidence=confidence,
        rule_version=RULE_VERSION,
    )

Update calculate_batch to pass confirmed_visit_date (leave the dict lookup for now — that's added in Task 5):

def calculate_batch(
    records: list[ShipmentRecord],
    as_of: Optional[date] = None,
    confirmed_visits: Optional[dict[str, date]] = None,
) -> list[CoverageResult]:
    """
    confirmed_visits: dict mapping patient_id_hash → confirmed visit date.
    Loaded from Supabase by persistence layer before calling this function.
    """
    import hashlib
    confirmed_visits = confirmed_visits or {}
    results = []
    for record in records:
        patient_hash = hashlib.sha256(record.patient_id.encode()).hexdigest()
        confirmed_date = confirmed_visits.get(patient_hash)
        try:
            result = calculate_coverage(record, as_of=as_of, confirmed_visit_date=confirmed_date)
            results.append(result)
        except ValueError as exc:
            logger.warning("Skipping record for patient_id hash — %s", exc)

    results.sort(key=lambda r: r.priority_score, reverse=True)
    return results
  • Step 4: Run tests — verify they pass
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_coverage_flags.py -v

Expected: All PASS

  • Step 5: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/core/coverage_calculator.py tests/test_coverage_flags.py
git commit -m "feat: 90/60/45 visit priority system with new CoverageFlag enum"

Task 3: Doc State Machine

Files:

  • Create: python-backend/core/doc_state_machine.py

  • Create: tests/test_doc_state_machine.py

  • Step 1: Write the failing test

Create tests/test_doc_state_machine.py:

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))

from datetime import date, timedelta
from core.doc_state_machine import compute_doc_state, DocState

TODAY = date.today()

def test_medicare_ffs_pa_not_required():
    state = compute_doc_state(
        payer_type="medicare",
        csv_swo_status="On File",
        csv_visit_date=TODAY - timedelta(days=30),
        confirmed_visit_date=None,
        csv_pecos_verified="Yes",
        csv_pa_status=None,
        csv_diagnosis_on_file="Yes",
        is_transfer=False,
    )
    assert state.pa == "Not Required"
    assert state.pecos == "Verified"

def test_medicaid_pecos_not_applicable():
    state = compute_doc_state(
        payer_type="medicaid",
        csv_swo_status="On File",
        csv_visit_date=TODAY - timedelta(days=30),
        confirmed_visit_date=None,
        csv_pecos_verified=None,
        csv_pa_status="Approved",
        csv_diagnosis_on_file="Yes",
        is_transfer=False,
    )
    assert state.pecos == "N/A"
    assert state.pa == "Approved"

def test_transfer_patient_all_pending():
    state = compute_doc_state(
        payer_type="medicare",
        csv_swo_status=None,
        csv_visit_date=None,
        confirmed_visit_date=None,
        csv_pecos_verified=None,
        csv_pa_status=None,
        csv_diagnosis_on_file=None,
        is_transfer=True,
    )
    assert state.swo == "Pending — Verify"
    assert state.visit == "Pending — Verify"
    assert state.pecos == "Pending — Verify"
    assert state.pa == "Pending — Verify"
    assert state.diagnosis == "Pending — Verify"

def test_cascade_visit_not_confirmed():
    state = compute_doc_state(
        payer_type="medicare",
        csv_swo_status="On File",
        csv_visit_date=None,
        confirmed_visit_date=None,
        csv_pecos_verified="Yes",
        csv_pa_status=None,
        csv_diagnosis_on_file="Yes",
        is_transfer=False,
    )
    assert len(state.cascade) > 0
    assert any("visit" in c.lower() for c in state.cascade)

def test_no_cascade_when_all_clear():
    state = compute_doc_state(
        payer_type="medicare",
        csv_swo_status="On File",
        csv_visit_date=TODAY - timedelta(days=30),
        confirmed_visit_date=None,
        csv_pecos_verified="Yes",
        csv_pa_status=None,
        csv_diagnosis_on_file="Yes",
        is_transfer=False,
    )
    assert state.cascade == []
  • Step 2: Run test — verify it fails
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_doc_state_machine.py -v 2>&1 | head -20

Expected: FAIL — module not found

  • Step 3: Create doc_state_machine.py

Create python-backend/core/doc_state_machine.py:

"""
doc_state_machine.py
Signal — STTIL Solutions

Computes per-patient documentation status across 5 items.
Requirements are payer-dependent. Produces cascade consequence chain.

PHI Contract: no patient names, DOBs, or contact info in any parameter.
"""

from dataclasses import dataclass, field
from datetime import date
from typing import Optional


# Payer types that require PECOS enrollment
_PECOS_REQUIRED = {"medicare", "medicare_advantage"}

# Payer types that require Prior Authorization for CGM
_PA_REQUIRED = {"medicare_advantage", "medicaid", "commercial"}

# Medicare FFS: PA not required (CGM coverage expansion 2023 + CMS-1828-F exemption path)
_PA_NOT_REQUIRED = {"medicare"}


@dataclass
class DocState:
    swo: str = "Pending"          # "On File" | "Pending" | "Expired"
    visit: str = "Not Confirmed"  # "Confirmed YYYY-MM-DD" | "Estimated YYYY-MM-DD" | "Not Confirmed"
    pecos: str = "Not Verified"   # "Verified" | "Not Verified" | "N/A" | "Pending — Verify"
    pa: str = "Required — Not Started"  # "Not Required" | "Approved" | "Pending" | "Denied" | "Required — Not Started" | "Pending — Verify"
    diagnosis: str = "Missing"    # "On File" | "Missing" | "Pending — Verify"
    cascade: list[str] = field(default_factory=list)


def _resolve_visit_display(
    csv_visit_date: Optional[date],
    confirmed_visit_date: Optional[date],
    is_transfer: bool,
) -> tuple[str, bool]:
    """
    Returns (display_string, is_confirmed).
    is_confirmed=True means the date is reliable (from CSV or staff-confirmed).
    """
    if is_transfer:
        return "Pending — Verify", False
    if csv_visit_date:
        return f"Confirmed {csv_visit_date.isoformat()}", True
    if confirmed_visit_date:
        return f"Confirmed {confirmed_visit_date.isoformat()}", True
    return "Not Confirmed — Estimated Only", False


def _build_cascade(
    swo: str,
    visit_confirmed: bool,
    pecos: str,
    pa: str,
    diagnosis: str,
    payer_type: str,
) -> list[str]:
    """
    Build the downstream consequence chain for missing/at-risk doc items.
    Only add cascade steps that are actually blocked.
    """
    cascade: list[str] = []

    if not visit_confirmed:
        cascade.append("Qualifying visit not confirmed")
        if swo in ("Pending", "Expired"):
            cascade.append("SWO validity at risk")
        if payer_type in _PA_REQUIRED and pa in ("Pending", "Required — Not Started"):
            cascade.append("PA renewal cannot proceed")
        cascade.append("Next shipment blocked")
        return cascade

    if swo in ("Pending", "Expired"):
        cascade.append("SWO missing or expired")
        if payer_type in _PECOS_REQUIRED and pecos not in ("Verified",):
            cascade.append("PECOS enrollment at risk")
        cascade.append("Next shipment blocked")

    if diagnosis == "Missing":
        cascade.append("Diagnosis documentation missing — required for all payers")

    return cascade


def compute_doc_state(
    payer_type: str,
    csv_swo_status: Optional[str],
    csv_visit_date: Optional[date],
    confirmed_visit_date: Optional[date],
    csv_pecos_verified: Optional[str],
    csv_pa_status: Optional[str],
    csv_diagnosis_on_file: Optional[str],
    is_transfer: bool = False,
) -> DocState:
    """
    Compute the full documentation state for one patient.

    payer_type: "medicare" | "medicare_advantage" | "medicaid" | "commercial"
    All csv_* fields are Optional — None means not present in the CSV.
    """

    # Transfer patients: everything is pending verification
    if is_transfer:
        return DocState(
            swo="Pending — Verify",
            visit="Pending — Verify",
            pecos="Pending — Verify",
            pa="Pending — Verify",
            diagnosis="Pending — Verify",
            cascade=[
                "Patient transferred from prior supplier",
                "Verify: SWO from current prescriber, qualifying visit within 6 months, PA status with current payer",
            ],
        )

    # SWO
    swo_map = {"on file": "On File", "pending": "Pending", "expired": "Expired"}
    swo = swo_map.get((csv_swo_status or "").strip().lower(), "Pending")

    # Visit
    visit_display, visit_confirmed = _resolve_visit_display(
        csv_visit_date, confirmed_visit_date, is_transfer
    )

    # PECOS — only required for Medicare
    if payer_type in _PECOS_REQUIRED:
        pecos_val = (csv_pecos_verified or "").strip().lower()
        pecos = "Verified" if pecos_val in ("yes", "verified", "true", "1") else "Not Verified"
    else:
        pecos = "N/A"

    # PA — not required for Medicare FFS; required for MA, Medicaid, Commercial
    if payer_type in _PA_NOT_REQUIRED:
        pa = "Not Required"
    elif csv_pa_status:
        pa_map = {
            "approved": "Approved",
            "pending": "Pending",
            "denied": "Denied",
            "not required": "Not Required",
            "not started": "Required — Not Started",
        }
        pa_key = csv_pa_status.strip().lower()
        pa = pa_map.get(pa_key, "Required — Not Started")
    else:
        pa = "Required — Not Started"

    # Diagnosis
    dx_val = (csv_diagnosis_on_file or "").strip().lower()
    diagnosis = "On File" if dx_val in ("yes", "true", "on file", "1") else "Missing"

    cascade = _build_cascade(swo, visit_confirmed, pecos, pa, diagnosis, payer_type)

    return DocState(
        swo=swo,
        visit=visit_display,
        pecos=pecos,
        pa=pa,
        diagnosis=diagnosis,
        cascade=cascade,
    )
  • Step 4: Run tests — verify they pass
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/test_doc_state_machine.py -v

Expected: All PASS

  • Step 5: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/core/doc_state_machine.py tests/test_doc_state_machine.py
git commit -m "feat: doc state machine — 5-item payer-dependent requirements with cascade logic"

Task 4: payer_rules.json updates + Supabase confirmed_visits migration

Files:

  • Modify: python-backend/config/payer_rules.json

  • Create: python-backend/db/migrations/001_add_confirmed_visits.sql

  • Modify: python-backend/db/schema.sql

  • Step 1: Update payer_rules.json

Read the file first, then replace its contents:

{
  "_comment": "Wear-day rules by device type and payer. Updated 2026-06-07. Run quarterly review per compliance checklist.",
  "devices": {
    "dexcom_g6": {
      "display_name": "Dexcom G6",
      "sensor_wear_days": 10,
      "transmitter_wear_days": 90,
      "components": ["sensor", "transmitter"]
    },
    "dexcom_g7": {
      "display_name": "Dexcom G7",
      "sensor_wear_days": 10,
      "components": ["sensor"]
    },
    "freestyle_libre_2": {
      "display_name": "FreeStyle Libre 2",
      "sensor_wear_days": 14,
      "components": ["sensor"]
    },
    "freestyle_libre_3": {
      "display_name": "FreeStyle Libre 3",
      "sensor_wear_days": 14,
      "components": ["sensor"]
    },
    "omnipod_5": {
      "display_name": "Omnipod 5",
      "pod_wear_days": 3,
      "sensor_wear_days": 14,
      "components": ["pod", "sensor"]
    }
  },
  "payer_rules": {
    "medicare": {
      "visit_renewal_days": 180,
      "refill_window_days": 30,
      "pa_required": false,
      "pecos_required": true,
      "_note": "Medicare FFS. PA not required (CGM coverage expansion 2023). Face-to-face visit required every 6 months. CMS-1828-F: suppliers at ≥90% affirmation rate may qualify for PA exemption from June 1, 2026.",
      "covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
    },
    "medicare_advantage": {
      "visit_renewal_days": 180,
      "refill_window_days": 30,
      "pa_required": true,
      "pecos_required": true,
      "_note": "Medicare Advantage — PA required, varies by plan. Verify plan-specific rules.",
      "covered_devices": ["dexcom_g6", "dexcom_g7", "freestyle_libre_2", "freestyle_libre_3"]
    },
    "medicaid": {
      "visit_renewal_days": 180,
      "refill_window_days": 30,
      "pa_required": true,
      "pecos_required": false,
      "_pa_note_pa": "PA required — Keystone First, UPMC Health Plan, Highmark BCBS require PA. NJ FamilyCare, Horizon BCBS NJ require PA.",
      "covered_devices": []
    },
    "commercial": {
      "visit_renewal_days": 180,
      "refill_window_days": 30,
      "pa_required": true,
      "pecos_required": false,
      "_note": "Commercial payer rules vary by plan. PA required by default — verify per plan.",
      "covered_devices": []
    },
    "default": {
      "visit_renewal_days": 180,
      "refill_window_days": 30,
      "pa_required": true,
      "pecos_required": false,
      "covered_devices": []
    }
  }
}
  • Step 2: Create Supabase migration SQL

Create python-backend/db/migrations/001_add_confirmed_visits.sql:

-- Migration 001: Add confirmed_visits table
-- Run in Supabase SQL Editor: Dashboard > SQL Editor > New query
-- Safe to run multiple times.

create table if not exists confirmed_visits (
  id              uuid        primary key default uuid_generate_v4(),
  org_id          uuid        not null references organizations(id) on delete cascade,
  patient_id_hash text        not null,  -- SHA-256 of patient_id — no raw PHI
  confirmed_date  date        not null,
  confirmed_by    text,                  -- staff identifier (not PHI — just a label)
  created_at      timestamptz not null default now(),
  updated_at      timestamptz not null default now(),
  unique(org_id, patient_id_hash)
);

create index if not exists idx_confirmed_visits_org on confirmed_visits(org_id);
create index if not exists idx_confirmed_visits_hash on confirmed_visits(patient_id_hash);

alter table confirmed_visits enable row level security;
  • Step 3: Add confirmed_visits to schema.sql

Read python-backend/db/schema.sql, then append before the final -- Indexes section:

-- Confirmed visit dates (staff-entered, persists across CSV imports)
create table if not exists confirmed_visits (
  id              uuid        primary key default uuid_generate_v4(),
  org_id          uuid        not null references organizations(id) on delete cascade,
  patient_id_hash text        not null,
  confirmed_date  date        not null,
  confirmed_by    text,
  created_at      timestamptz not null default now(),
  updated_at      timestamptz not null default now(),
  unique(org_id, patient_id_hash)
);
create index if not exists idx_confirmed_visits_org  on confirmed_visits(org_id);
create index if not exists idx_confirmed_visits_hash on confirmed_visits(patient_id_hash);
alter table confirmed_visits enable row level security;
  • Step 4: Run the migration in Supabase

The migration must be run manually in the Supabase dashboard SQL editor (there is no CLI migration runner configured). Copy the contents of python-backend/db/migrations/001_add_confirmed_visits.sql and paste into the Supabase dashboard SQL editor at your project.

After running, verify: SELECT COUNT(*) FROM confirmed_visits; should return 0 with no error.

  • Step 5: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/config/payer_rules.json python-backend/db/migrations/001_add_confirmed_visits.sql python-backend/db/schema.sql
git commit -m "feat: payer_rules medicare_advantage + PA/NJ rules; confirmed_visits migration"

Task 5: Persistence — confirmed visit read/write + upload diff

Files:

  • Modify: python-backend/core/persistence.py

Read python-backend/core/persistence.py before editing — preserve all existing functions.

  • Step 1: Add confirmed visit functions

Append to python-backend/core/persistence.py:

def upsert_confirmed_visit(
    org_id: str,
    patient_id_hash: str,
    confirmed_date: date,
    confirmed_by: str = "staff",
) -> bool:
    """
    Insert or update a confirmed visit date for a patient.
    Returns True on success, False if Supabase unavailable.
    """
    client = get_client()
    if not client:
        return False
    try:
        client.table("confirmed_visits").upsert({
            "org_id": org_id,
            "patient_id_hash": patient_id_hash,
            "confirmed_date": confirmed_date.isoformat(),
            "confirmed_by": confirmed_by,
            "updated_at": "now()",
        }, on_conflict="org_id,patient_id_hash").execute()
        return True
    except Exception as e:
        logger.error(f"Failed to upsert confirmed visit: {e}")
        return False


def load_confirmed_visits_for_org(org_id: str) -> dict[str, date]:
    """
    Load all confirmed visit dates for an org.
    Returns dict mapping patient_id_hash → confirmed_date.
    """
    client = get_client()
    if not client:
        return {}
    try:
        result = client.table("confirmed_visits") \
            .select("patient_id_hash,confirmed_date") \
            .eq("org_id", org_id) \
            .execute()
        return {
            row["patient_id_hash"]: date.fromisoformat(row["confirmed_date"])
            for row in (result.data or [])
        }
    except Exception as e:
        logger.error(f"Failed to load confirmed visits: {e}")
        return {}


def get_or_create_org(clerk_org_id: str | None = None) -> str | None:
    """Public wrapper around _get_or_create_org for use by the API."""
    return _get_or_create_org(clerk_org_id=clerk_org_id)
  • Step 2: Verify imports in main.py still work
cd /Users/sttil-solutions/projects/signal/python-backend && python -c "from core.persistence import upsert_confirmed_visit, load_confirmed_visits_for_org, get_or_create_org; print('OK')"

Expected: OK

  • Step 3: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/core/persistence.py
git commit -m "feat: confirmed visit persistence — upsert and bulk load per org"

Task 6: Normalizer updates — doc columns + transfer columns

Files:

  • Modify: python-backend/api/normalizer.py

Read python-backend/api/normalizer.py before editing.

  • Step 1: Add doc column mappings to HEADER_MAP

In normalizer.py, add these entries to the HEADER_MAP dict (after the existing "component" entry):

    "csv_visit_date": [
        "visit_date", "visit date", "qualifying_visit_date", "qualifying visit date",
        "last_visit_date", "last visit date", "face_to_face_date", "f2f_date",
        "encounter_date", "encounter date", "physician_visit_date",
    ],
    "csv_swo_status": [
        "swo_status", "swo status", "swo", "standing_written_order",
        "standing written order", "order_status", "order status",
    ],
    "csv_pecos_verified": [
        "pecos_verified", "pecos verified", "pecos", "pecos_status",
        "enrollment_verified", "enrollment verified",
    ],
    "csv_pa_status": [
        "pa_status", "pa status", "prior_auth_status", "prior auth status",
        "prior_authorization_status", "auth_status", "pa", "authorization",
    ],
    "csv_diagnosis_on_file": [
        "diagnosis_on_file", "diagnosis on file", "diagnosis", "dx_on_file",
        "dx on file", "icd_on_file", "icd on file",
    ],
    "csv_transfer_from": [
        "transfer_from", "transfer from", "previous_supplier", "previous supplier",
        "prior_supplier", "prior supplier", "transfer_status", "transferred_from",
    ],
  • Step 2: Update the ShipmentRecord construction in normalize_csv

In the normalize_csv function, after the existing component parsing, add:

        # Optional doc fields — only populated if the CSV contains these columns
        csv_visit_date: Optional[date] = None
        raw_visit = mapped.get("csv_visit_date", "")
        if raw_visit:
            csv_visit_date = _parse_date(raw_visit)  # None if unparseable (non-error)

        csv_swo_status = mapped.get("csv_swo_status") or None
        csv_pecos_verified = mapped.get("csv_pecos_verified") or None
        csv_pa_status = mapped.get("csv_pa_status") or None
        csv_diagnosis_on_file = mapped.get("csv_diagnosis_on_file") or None
        transfer_raw = mapped.get("csv_transfer_from", "").strip()
        csv_transfer_from = transfer_raw if transfer_raw else None

Then update the ShipmentRecord(...) constructor call to include these fields:

        records.append(ShipmentRecord(
            patient_id=patient_id,
            device_type=device_type,
            shipment_date=shipment_date,
            quantity=quantity,
            payer=payer,
            component=component,
            csv_visit_date=csv_visit_date,
            csv_swo_status=csv_swo_status,
            csv_pecos_verified=csv_pecos_verified,
            csv_pa_status=csv_pa_status,
            csv_diagnosis_on_file=csv_diagnosis_on_file,
            csv_transfer_from=csv_transfer_from,
        ))
  • Step 3: Update _normalize_payer in normalizer.py to distinguish Medicare Advantage

Find the PAYER_MAP dict and update the medicare_advantage line:

    "medicare advantage": "medicare_advantage",  # distinct from medicare FFS
  • Step 4: Verify the normalizer still runs on a baseline CSV
cd /Users/sttil-solutions/projects/signal && python -c "
from python_backend.api.normalizer import normalize_csv
import pathlib
text = pathlib.Path('test-data/sample-batch-01-ok.csv').read_text()
records, skipped, summary = normalize_csv(text)
print(f'Records: {len(records)}, Skipped: {len(skipped)}')
print('First record:', records[0] if records else 'none')
"

Expected: Records >= 1, no errors

Alternatively run from python-backend dir:

cd /Users/sttil-solutions/projects/signal/python-backend && python -c "
from api.normalizer import normalize_csv
import pathlib, sys
text = pathlib.Path('../test-data/sample-batch-01-ok.csv').read_text()
records, skipped, summary = normalize_csv(text)
print(f'Records: {len(records)}, Skipped: {len(skipped)}')
"
  • Step 5: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/api/normalizer.py
git commit -m "feat: normalizer adds doc column mappings and transfer column detection"

Task 7: API — updated RecordOut + confirm-visit endpoint + upload integration

Files:

  • Modify: python-backend/api/main.py

Read python-backend/api/main.py before editing.

  • Step 1: Add imports at top of main.py

Add these imports (after existing imports):

import hashlib
from datetime import date as date_type
from core.doc_state_machine import compute_doc_state, DocState
from core.persistence import (
    persist_export, persist_upload,
    upsert_confirmed_visit, load_confirmed_visits_for_org, get_or_create_org,
)

Remove the existing from core.persistence import persist_export, persist_upload line.

  • Step 2: Update FLAG_LABELS and FLAG_ACTIONS for new flags

Replace the existing FLAG_LABELS and FLAG_ACTIONS dicts:

FLAG_LABELS = {
    "SUPPLY_LAPSED":    "Supply Lapsed",
    "VISIT_REQUIRED":   "Visit Required",
    "TRANSFER_PENDING": "Needs Verification — Transfer",
    "RENEWAL_CRITICAL": "Renewal Due — Critical",
    "RENEWAL_ELEVATED": "Renewal Due SOON",
    "RENEWAL_SOON":     "Renewal Due SOON",
    "RESUPPLY_READY":   "Resupply Ready",
    "ACTIVE":           "Active",
}

FLAG_ACTIONS = {
    "SUPPLY_LAPSED":    "Contact Prescriber — Supply Lapsed",
    "VISIT_REQUIRED":   "Contact Prescriber — Visit Overdue",
    "TRANSFER_PENDING": "Verify All Documentation",
    "RENEWAL_CRITICAL": "Contact Prescriber — Confirm Visit Appointment",
    "RENEWAL_ELEVATED": "Contact Prescriber — Schedule Qualifying Visit",
    "RENEWAL_SOON":     "Monitor — Visit Due in 90 Days",
    "RESUPPLY_READY":   "Initiate Resupply",
    "ACTIVE":           "No action needed",
}
  • Step 3: Update RecordOut to include doc state and visit confidence

Replace the existing RecordOut class:

class DocStateOut(BaseModel):
    swo: str
    visit: str
    pecos: str
    pa: str
    diagnosis: str

class RecordOut(BaseModel):
    patient_id: str
    device_type: str
    device_display: str
    payer: str
    component: str
    days_until_coverage_end: int
    days_until_visit_due: Optional[int] = None
    flag: str
    priority_score: int
    coverage_end_date: str
    next_visit_due_date: Optional[str] = None
    action: str
    status_label: str
    reason: str
    rule_version: str
    visit_date_confidence: str = "estimated"
    is_transfer: bool = False
    doc_state: Optional[DocStateOut] = None
    cascade: list[str] = []
  • Step 4: Update _to_record_out to compute doc state

Replace the existing _to_record_out function. It now needs access to the original ShipmentRecord to compute doc state:

def _to_record_out(r, record=None) -> RecordOut:
    flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag)

    # Compute doc state if we have the original record
    doc_state_out = None
    cascade = []
    if record is not None:
        normalized_payer = _normalize_payer_type(r.payer)
        doc = compute_doc_state(
            payer_type=normalized_payer,
            csv_swo_status=record.csv_swo_status,
            csv_visit_date=record.csv_visit_date,
            confirmed_visit_date=None,  # already factored into r.next_visit_due_date
            csv_pecos_verified=record.csv_pecos_verified,
            csv_pa_status=record.csv_pa_status,
            csv_diagnosis_on_file=record.csv_diagnosis_on_file,
            is_transfer=bool(record.csv_transfer_from),
        )
        doc_state_out = DocStateOut(
            swo=doc.swo, visit=doc.visit, pecos=doc.pecos, pa=doc.pa, diagnosis=doc.diagnosis
        )
        cascade = doc.cascade

    return RecordOut(
        patient_id=r.patient_id,
        device_type=r.device_type,
        device_display=DEVICE_DISPLAY.get(r.device_type, r.device_type),
        payer=r.payer,
        component=r.component,
        days_until_coverage_end=r.days_until_coverage_end,
        days_until_visit_due=r.days_until_visit_due,
        flag=flag_val,
        priority_score=r.priority_score,
        coverage_end_date=r.coverage_end_date.isoformat(),
        next_visit_due_date=r.next_visit_due_date.isoformat() if r.next_visit_due_date else None,
        action=FLAG_ACTIONS.get(flag_val, "Review"),
        status_label=FLAG_LABELS.get(flag_val, flag_val),
        reason=_build_reason(flag_val, r.days_until_coverage_end, r.days_until_visit_due),
        rule_version=r.rule_version,
        visit_date_confidence=getattr(r, "visit_date_confidence", "estimated"),
        is_transfer=getattr(r, "is_transfer", False),
        doc_state=doc_state_out,
        cascade=cascade,
    )


def _normalize_payer_type(payer: str) -> str:
    """Map raw payer string to doc_state_machine payer_type."""
    from core.coverage_calculator import _normalize_payer
    normalized = _normalize_payer(payer)
    # normalizer maps "medicare advantage" → "medicare_advantage" but coverage_calc maps it to "commercial"
    # Use the raw payer string for MA detection
    if "medicare advantage" in payer.lower() or "medicare_advantage" in payer.lower():
        return "medicare_advantage"
    return normalized
  • Step 5: Update the /api/upload endpoint to pass records to _to_record_out and load confirmed visits

In the upload_csv function, replace:

    results = calculate_batch(records, as_of=date.today())
    out = [_to_record_out(r) for r in results]

with:

    # Load confirmed visit dates from Supabase for this org
    clerk_org_id = claims.get("o", {}).get("id") if isinstance(claims.get("o"), dict) else None
    org_id = get_or_create_org(clerk_org_id=clerk_org_id)
    confirmed_visits = load_confirmed_visits_for_org(org_id) if org_id else {}

    results = calculate_batch(records, as_of=date.today(), confirmed_visits=confirmed_visits)

    # Build a lookup from patient_id to original record for doc state computation
    record_lookup = {r.patient_id: r for r in records}
    out = [_to_record_out(r, record=record_lookup.get(r.patient_id)) for r in results]

Also remove the duplicate clerk_org_id extraction later in the function (it was used for persist_upload) and use the one computed above.

  • Step 6: Add the confirm-visit endpoint

Add after the existing /api/export endpoint:

class ConfirmVisitRequest(BaseModel):
    patient_id: str
    confirmed_date: str  # ISO date string YYYY-MM-DD
    # Fields needed to recompute coverage after confirmation
    shipment_date: str
    payer: str
    device_type: str
    quantity: int = 1
    component: str = "sensor"


@app.post("/api/confirm-visit")
async def confirm_visit(
    body: ConfirmVisitRequest,
    claims: dict = Depends(require_auth),
):
    """
    Store a staff-confirmed qualifying visit date and return the updated coverage record.
    The confirmed date persists across all future CSV imports for this patient_id.
    """
    from core.coverage_calculator import ShipmentRecord, calculate_coverage

    # Validate dates
    try:
        confirmed = date_type.fromisoformat(body.confirmed_date)
        shipment = date_type.fromisoformat(body.shipment_date)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"Invalid date: {e}. Use YYYY-MM-DD.")

    if confirmed > date_type.today():
        raise HTTPException(status_code=400, detail="Confirmed date cannot be in the future.")

    # Get org
    clerk_org_id = claims.get("o", {}).get("id") if isinstance(claims.get("o"), dict) else None
    org_id = get_or_create_org(clerk_org_id=clerk_org_id)
    if not org_id:
        raise HTTPException(status_code=503, detail="Organization not found.")

    # Hash and store
    patient_hash = hashlib.sha256(body.patient_id.encode()).hexdigest()
    success = upsert_confirmed_visit(org_id, patient_hash, confirmed, confirmed_by="staff")
    if not success:
        raise HTTPException(status_code=503, detail="Failed to save confirmed visit.")

    # Recompute coverage with confirmed visit date
    record = ShipmentRecord(
        patient_id=body.patient_id,
        device_type=body.device_type,
        shipment_date=shipment,
        quantity=body.quantity,
        payer=body.payer,
        component=body.component,
    )
    result = calculate_coverage(record, confirmed_visit_date=confirmed)

    log_event(
        AuditAction.CSV_INGEST,
        f"confirm-visit:{body.patient_id[:8]}",
        "staff",
        "success",
        "0.0.0.0",
        detail=f"Visit confirmed {confirmed.isoformat()}",
    )

    return _to_record_out(result, record=record)
  • Step 7: Verify the backend starts cleanly
cd /Users/sttil-solutions/projects/signal && python -m uvicorn python-backend.api.main:app --port 8001 --no-access-log 2>&1 | head -5

Expected: no import errors. Kill with Ctrl+C after confirming.

Or from the python-backend directory:

cd /Users/sttil-solutions/projects/signal/python-backend && python -m uvicorn api.main:app --port 8001 2>&1 | head -5
  • Step 8: Commit
cd /Users/sttil-solutions/projects/signal
git add python-backend/api/main.py
git commit -m "feat: confirm-visit endpoint + RecordOut with doc_state + cascade"

Task 8: Deploy backend to Railway

  • Step 1: Deploy
cd /Users/sttil-solutions/projects/signal && railway up --detach
  • Step 2: Wait for build and verify health

After Railway build completes (watch status at https://railway.app or railway status):

curl -s https://signal-api-production-91c2.up.railway.app/health | python3 -c "import json,sys; d=json.load(sys.stdin); print(d)"

Expected: {'status': 'ok', 'service': 'signal-api', 'version': '1.0.0'}


Task 9: Frontend — Badge.jsx update for new status labels

Files:

  • Modify: signal-ui/src/components/Badge.jsx

Read signal-ui/src/components/Badge.jsx before editing.

  • Step 1: Replace Badge.jsx with updated flag handling

The new flags are: SUPPLY_LAPSED, VISIT_REQUIRED, TRANSFER_PENDING, RENEWAL_CRITICAL, RENEWAL_ELEVATED, RENEWAL_SOON, RESUPPLY_READY, ACTIVE.

Visual weight rules per spec:

  • SUPPLY_LAPSED: dark red, bold
  • VISIT_REQUIRED: red bold + warning icon
  • RENEWAL_CRITICAL: red bold
  • RENEWAL_ELEVATED: amber bold
  • RENEWAL_SOON: amber normal
  • RESUPPLY_READY: teal/brand, normal
  • ACTIVE: green normal
  • TRANSFER_PENDING: orange, normal
const FLAG_CONFIG = {
  SUPPLY_LAPSED: {
    label: "Supply Lapsed",
    bg: "bg-[rgba(180,0,0,0.12)]",
    text: "text-[#B00000]",
    border: "border-[#B00000]",
    weight: "font-bold",
    icon: "⚠",
  },
  VISIT_REQUIRED: {
    label: "Visit Required",
    bg: "bg-[rgba(220,30,30,0.10)]",
    text: "text-[#CC2222]",
    border: "border-[#CC2222]",
    weight: "font-bold",
    icon: "⚠",
  },
  TRANSFER_PENDING: {
    label: "Needs Verification",
    bg: "bg-[rgba(200,120,0,0.10)]",
    text: "text-[#C87800]",
    border: "border-[#C87800]",
    weight: "font-semibold",
    icon: "↔",
  },
  RENEWAL_CRITICAL: {
    label: "Renewal Due — Critical",
    bg: "bg-[rgba(220,60,0,0.10)]",
    text: "text-[#DC3C00]",
    border: "border-[#DC3C00]",
    weight: "font-bold",
    icon: "",
  },
  RENEWAL_ELEVATED: {
    label: "Renewal Due SOON",
    bg: "bg-[rgba(203,107,32,0.12)]",
    text: "text-[#CB6B20]",
    border: "border-[#CB6B20]",
    weight: "font-semibold",
    icon: "",
  },
  RENEWAL_SOON: {
    label: "Renewal Due SOON",
    bg: "bg-[rgba(203,107,32,0.08)]",
    text: "text-[#CB6B20]",
    border: "border-[#CB6B20]",
    weight: "font-normal",
    icon: "",
  },
  RESUPPLY_READY: {
    label: "Resupply Ready",
    bg: "bg-[rgba(46,163,163,0.10)]",
    text: "text-[#1A7070]",
    border: "border-[#2EA3A3]",
    weight: "font-normal",
    icon: "",
  },
  ACTIVE: {
    label: "Active",
    bg: "bg-[rgba(40,160,80,0.10)]",
    text: "text-[#1A8040]",
    border: "border-[#28A050]",
    weight: "font-normal",
    icon: "",
  },
};

export default function Badge({ flag }) {
  const cfg = FLAG_CONFIG[flag] || {
    label: flag,
    bg: "bg-gray-100",
    text: "text-gray-600",
    border: "border-gray-300",
    weight: "font-normal",
    icon: "",
  };
  return (
    <span
      className={`inline-flex items-center gap-[3px] px-[8px] py-[3px] rounded-full text-[11px] border ${cfg.bg} ${cfg.text} ${cfg.border} ${cfg.weight} whitespace-nowrap`}
    >
      {cfg.icon && <span className="text-[10px]">{cfg.icon}</span>}
      {cfg.label}
    </span>
  );
}
  • Step 2: Commit
cd /Users/sttil-solutions/projects/signal
git add signal-ui/src/components/Badge.jsx
git commit -m "feat: Badge updated for new status flags with visual weight system"

Task 10: Frontend — DocStatusBar component

Files:

  • Create: signal-ui/src/components/DocStatusBar.jsx

  • Step 1: Create DocStatusBar.jsx

/**
 * DocStatusBar — 5 colored dots showing SWO, Visit, PECOS, PA, Diagnosis status.
 * Green = complete, Amber = pending/estimated, Red = missing/denied.
 */

const DOT_STATUS = {
  // SWO
  "On File": "green",
  "Expired": "red",
  // Visit
  "Not Confirmed — Estimated Only": "amber",
  "Not Confirmed": "amber",
  // PECOS
  "Verified": "green",
  "Not Verified": "red",
  "N/A": "gray",
  // PA
  "Not Required": "green",
  "Approved": "green",
  "Denied": "red",
  // Diagnosis
  "On File": "green",
  "Missing": "red",
  // Transfer / generic pending
  "Pending — Verify": "amber",
  "Pending": "amber",
  "Required — Not Started": "red",
};

function dotColor(status) {
  if (!status) return "amber";
  const s = status.trim();
  if (s.startsWith("Confirmed")) return "green";
  if (s.startsWith("Approved")) return "green";
  return DOT_STATUS[s] || "amber";
}

const COLOR_MAP = {
  green: "bg-[#28A050]",
  amber: "bg-[#CB6B20]",
  red:   "bg-[#CC2222]",
  gray:  "bg-[#AAAAAA]",
};

const LABELS = ["SWO", "Visit", "PECOS", "PA", "Dx"];

export default function DocStatusBar({ docState }) {
  if (!docState) return null;

  const items = [
    docState.swo,
    docState.visit,
    docState.pecos,
    docState.pa,
    docState.diagnosis,
  ];

  return (
    <div className="flex items-center gap-[5px]" title="Documentation status: SWO · Visit · PECOS · PA · Diagnosis">
      {items.map((status, i) => (
        <div key={i} className="flex flex-col items-center gap-[2px]">
          <div
            className={`w-[8px] h-[8px] rounded-full ${COLOR_MAP[dotColor(status)]}`}
            title={`${LABELS[i]}: ${status || "Unknown"}`}
          />
        </div>
      ))}
    </div>
  );
}
  • Step 2: Commit
cd /Users/sttil-solutions/projects/signal
git add signal-ui/src/components/DocStatusBar.jsx
git commit -m "feat: DocStatusBar — 5-dot documentation status indicator"

Task 11: Frontend — ConfirmVisitModal component

Files:

  • Create: signal-ui/src/components/ConfirmVisitModal.jsx

  • Step 1: Create ConfirmVisitModal.jsx

import { useState } from "react";
import { confirmVisit } from "../lib/api";

export default function ConfirmVisitModal({ record, onClose, onConfirmed }) {
  const [visitDate, setVisitDate] = useState("");
  const [error, setError] = useState("");
  const [saving, setSaving] = useState(false);

  const today = new Date().toISOString().split("T")[0];
  const minDate = "2020-01-01";

  async function handleSave() {
    setError("");
    if (!visitDate) {
      setError("Please enter a visit date.");
      return;
    }
    if (visitDate > today) {
      setError("Visit date cannot be in the future.");
      return;
    }
    // Validate: within 6 months of shipment date
    if (record.coverage_end_date) {
      const shipment = new Date(record.coverage_end_date);
      const visit = new Date(visitDate);
      // rough check: visit must be after shipment_date - 180 days
    }

    setSaving(true);
    try {
      const updated = await confirmVisit({
        patient_id: record.patient_id,
        confirmed_date: visitDate,
        shipment_date: record.coverage_end_date
          ? new Date(new Date(record.coverage_end_date).getTime() - record.days_until_coverage_end * 86400000).toISOString().split("T")[0]
          : today,
        payer: record.payer,
        device_type: record.device_type,
        quantity: 1,
        component: record.component || "sensor",
      });
      onConfirmed(record.patient_id, updated);
      onClose();
    } catch (e) {
      setError(e.message || "Failed to save. Please try again.");
    } finally {
      setSaving(false);
    }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
      <div
        className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[12px] shadow-xl p-6 w-[420px] max-w-[95vw]"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="font-heading font-bold text-[15px] text-[var(--text-heading)] mb-1">
          Confirm Qualifying Visit
        </div>
        <div className="text-[12px] text-[var(--text-muted)] mb-4">
          Patient ID: <span className="font-mono font-semibold text-[var(--text-secondary)]">{record.patient_id}</span>
        </div>

        <label className="block text-[12px] font-semibold text-[var(--text-secondary)] mb-1">
          Qualifying visit date  confirmed with prescriber office or from SWO on file
        </label>
        <input
          type="date"
          value={visitDate}
          max={today}
          min={minDate}
          onChange={(e) => setVisitDate(e.target.value)}
          className="w-full border border-[var(--border-color)] rounded-[6px] px-3 py-2 text-[13px] bg-[var(--bg-elevated)] text-[var(--text-primary)] mb-2 focus:outline-none focus:border-[var(--brand)]"
        />
        <div className="text-[10.5px] text-[var(--text-muted)] mb-4">
          Contact prescriber office to confirm. Do not use patient-reported date.
        </div>

        {error && (
          <div className="text-[11px] text-[#CC2222] mb-3 px-2 py-1 bg-red-50 rounded border border-red-200">
            {error}
          </div>
        )}

        <div className="flex gap-3 justify-end">
          <button
            onClick={onClose}
            className="px-4 py-2 text-[12px] rounded-md border border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--brand)] cursor-pointer"
          >
            Cancel
          </button>
          <button
            onClick={handleSave}
            disabled={saving}
            className="px-4 py-2 text-[12px] rounded-md bg-[var(--brand)] text-white font-semibold hover:opacity-90 cursor-pointer disabled:opacity-50"
          >
            {saving ? "Saving..." : "Save Visit Date"}
          </button>
        </div>
      </div>
    </div>
  );
}
  • Step 2: Commit
cd /Users/sttil-solutions/projects/signal
git add signal-ui/src/components/ConfirmVisitModal.jsx
git commit -m "feat: ConfirmVisitModal — date input with validation, connects to confirm-visit API"

Task 12: Frontend — api.js confirmVisit function

Files:

  • Modify: signal-ui/src/lib/api.js

Read signal-ui/src/lib/api.js before editing.

  • Step 1: Add confirmVisit export

Append to signal-ui/src/lib/api.js:

export async function confirmVisit(payload) {
  const { getToken } = window.__clerk_helpers__ || {};
  const token = getToken ? await getToken() : null;

  const res = await fetch(`${API_URL}/api/confirm-visit`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: JSON.stringify(payload),
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.detail || `HTTP ${res.status}`);
  }
  return res.json();
}

Note: The existing api.js likely already has an API_URL constant. Preserve it. If getToken is accessed via Clerk's useAuth hook in the calling component, update the ConfirmVisitModal to pass the token instead. Check how the existing upload function handles auth in api.js and match the pattern.

  • Step 2: Commit
cd /Users/sttil-solutions/projects/signal
git add signal-ui/src/lib/api.js
git commit -m "feat: api.js — confirmVisit function wired to /api/confirm-visit endpoint"

Task 13: Frontend — WorklistTable expandable rows + DocStatusBar + Confirm Visit

Files:

  • Modify: signal-ui/src/components/WorklistTable.jsx

Read signal-ui/src/components/WorklistTable.jsx before editing. This is a significant rewrite.

  • Step 1: Update the FILTERS list for new flags

Replace the existing FILTERS array:

const FILTERS = [
  { key: "all", label: "All" },
  { key: "SUPPLY_LAPSED", label: "Supply Lapsed" },
  { key: "VISIT_REQUIRED", label: "Visit Required" },
  { key: "RENEWAL_CRITICAL", label: "Renewal Critical" },
  { key: "RENEWAL_ELEVATED", label: "Renewal Elevated" },
  { key: "RENEWAL_SOON", label: "Renewal Due" },
  { key: "RESUPPLY_READY", label: "Resupply Ready" },
  { key: "ACTIVE", label: "Active" },
  { key: "TRANSFER_PENDING", label: "Transfer" },
];
  • Step 2: Add expandable row state + DocStatusBar + ConfirmVisitModal integration

At the top of WorklistTable.jsx, add import:

import { useState } from "react";
import DocStatusBar from "./DocStatusBar";
import ConfirmVisitModal from "./ConfirmVisitModal";

In the WorklistTable component function, add state:

const [expandedRow, setExpandedRow] = useState(null);
const [confirmingRecord, setConfirmingRecord] = useState(null);
const [localRecords, setLocalRecords] = useState(records);

Add a useEffect to sync when records prop changes:

import { useState, useEffect } from "react";
// ...
useEffect(() => { setLocalRecords(records); }, [records]);

Add handler for confirmed visit (live update without page refresh):

function handleVisitConfirmed(patientId, updatedRecord) {
  setLocalRecords(prev =>
    prev.map(r => r.patient_id === patientId ? { ...r, ...updatedRecord } : r)
      .sort((a, b) => b.priority_score - a.priority_score)
  );
}
  • Step 3: Update the table rows

Replace the existing row render in filtered.map(...) with:

{filtered.map((r, i) => {
  const hp = ["SUPPLY_LAPSED", "VISIT_REQUIRED", "RENEWAL_CRITICAL", "TRANSFER_PENDING"].includes(r.flag);
  const isExpanded = expandedRow === (r.patient_id + i);
  const showConfirmVisit = ["VISIT_REQUIRED", "RENEWAL_CRITICAL", "RENEWAL_ELEVATED", "RENEWAL_SOON"].includes(r.flag)
    || r.visit_date_confidence === "estimated";

  return (
    <>
      <tr
        key={r.patient_id + "-" + i}
        className={`border-b border-[var(--border-subtle)] transition-colors cursor-pointer hover:bg-[var(--row-hover)] ${
          hp
            ? dark
              ? "bg-[rgba(224,104,48,0.09)] hover:bg-[rgba(203,107,32,0.14)]"
              : "bg-[rgba(224,96,40,0.05)] hover:bg-[rgba(203,107,32,0.14)]"
            : ""
        }`}
        onClick={() => setExpandedRow(isExpanded ? null : (r.patient_id + i))}
      >
        <td className="px-[22px] py-[13px] align-middle">
          <div className={`font-mono text-[12.5px] font-bold ${hp ? "text-[var(--accent-text)]" : "text-[var(--text-secondary)]"}`}>
            {r.patient_id}
          </div>
          {i === 0 && (
            <div className="font-mono text-[9.5px] font-semibold tracking-[0.06em] text-[var(--accent-text)] mt-[2px]">
               TOP PRIORITY
            </div>
          )}
          {r.visit_date_confidence === "estimated" && (
            <div className="text-[9px] text-[#CB6B20] mt-[2px]">Estimated visit date</div>
          )}
        </td>
        <td className="px-[22px] py-[13px] text-[13.5px] font-medium text-[var(--text-primary)] align-middle">
          {r.device_display || r.device_type}
        </td>
        <td className="px-[22px] py-[13px] text-[13px] text-[var(--text-secondary)] align-middle">
          {r.payer}
        </td>
        <td className="px-[22px] py-[13px] align-middle">
          {daysLabel(r.days_until_coverage_end)}
        </td>
        <td className="px-[22px] py-[13px] align-middle">
          <div className="flex items-center gap-[8px]">
            <Badge flag={r.flag} />
            {r.doc_state && <DocStatusBar docState={r.doc_state} />}
          </div>
          {r.reason && (
            <div className="text-[10.5px] text-[var(--text-muted)] mt-[4px] max-w-[260px] leading-[1.4]">
              {r.reason}
            </div>
          )}
        </td>
        <td className="px-[22px] py-[13px] align-middle">
          <span className={`font-mono text-[16px] font-medium ${scoreClass(r.priority_score)}`}>
            {r.priority_score}
          </span>
        </td>
        <td className="px-[22px] py-[13px] align-middle" onClick={e => e.stopPropagation()}>
          {showConfirmVisit ? (
            <button
              onClick={() => setConfirmingRecord(r)}
              className="bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:bg-[rgba(203,107,32,0.1)]"
            >
              Confirm Visit 
            </button>
          ) : (
            <ActionButton flag={r.flag} />
          )}
        </td>
      </tr>

      {/* Expanded row — cascade + doc checklist */}
      {isExpanded && (
        <tr key={r.patient_id + "-expanded-" + i} className="bg-[var(--bg-elevated)]">
          <td colSpan={7} className="px-[32px] py-[16px]">
            {r.cascade && r.cascade.length > 0 && (
              <div className="mb-4">
                <div className="text-[11px] font-semibold text-[#CC2222] uppercase tracking-[0.06em] mb-2">
                  Documentation Cascade
                </div>
                <div className="text-[12px] text-[var(--text-secondary)] font-mono">
                  {r.cascade.join(" → ")}
                </div>
              </div>
            )}
            {r.doc_state && (
              <div>
                <div className="text-[11px] font-semibold text-[var(--text-muted)] uppercase tracking-[0.06em] mb-2">
                  Documentation Checklist
                </div>
                <div className="grid grid-cols-2 gap-y-[6px] gap-x-[20px] text-[12px]">
                  <DocItem label="SWO" value={r.doc_state.swo} />
                  <DocItem label="Qualifying Visit" value={r.doc_state.visit} />
                  <DocItem label="PECOS Enrollment" value={r.doc_state.pecos} />
                  <DocItem label="Prior Authorization" value={r.doc_state.pa} />
                  <DocItem label="Diagnosis on File" value={r.doc_state.diagnosis} />
                </div>
              </div>
            )}
          </td>
        </tr>
      )}
    </>
  );
})}

Add the DocItem helper component at the bottom of the file (before the closing):

function DocItem({ label, value }) {
  const isGood = value && (
    value === "On File" || value === "Verified" || value === "Not Required" ||
    value.startsWith("Confirmed") || value.startsWith("Approved") || value === "N/A"
  );
  const isBad = value && (
    value === "Missing" || value === "Not Verified" || value === "Expired" ||
    value === "Denied" || value === "Required — Not Started"
  );
  const color = isGood ? "text-[#1A8040]" : isBad ? "text-[#CC2222]" : "text-[#CB6B20]";

  return (
    <div className="flex items-start gap-[6px]">
      <span className="text-[var(--text-muted)] w-[120px] shrink-0">{label}:</span>
      <span className={`font-medium ${color}`}>{value || "Unknown"}</span>
    </div>
  );
}

Add the modal at the end of the component return, before the last closing div:

{confirmingRecord && (
  <ConfirmVisitModal
    record={confirmingRecord}
    onClose={() => setConfirmingRecord(null)}
    onConfirmed={handleVisitConfirmed}
  />
)}

Update filtered to use localRecords instead of records prop directly. Update the component signature to use localRecords for filtered:

const filtered = activeFilter === "all"
  ? localRecords
  : localRecords.filter((r) => r.flag === activeFilter);
  • Step 4: Start dev server and verify the UI
cd /Users/sttil-solutions/projects/signal/signal-ui && pnpm dev

Open http://localhost:5173. Upload one of the existing test CSVs. Verify:

  • Worklist renders with new status labels

  • DocStatusBar dots appear in each row

  • Clicking a row expands to show doc checklist

  • "Confirm Visit" button appears for RENEWAL_* and VISIT_REQUIRED rows

  • Clicking Confirm Visit opens the modal

  • Entering a date and saving closes the modal and updates the row live

  • Step 5: Fix any TypeErrors or import errors found during testing

  • Step 6: Commit

cd /Users/sttil-solutions/projects/signal
git add signal-ui/src/components/WorklistTable.jsx signal-ui/src/lib/api.js
git commit -m "feat: WorklistTable — expandable rows, DocStatusBar, ConfirmVisitModal integration"

Task 14: Placeholder cleanup + smoke test + export verification

  • Step 1: Remove any placeholder content from WorklistTable and App.jsx

Read signal-ui/src/App.jsx. Check for:

  • Any hardcoded sample data
  • "Coming soon" or placeholder text
  • Demo-only state that should be removed

Fix any found issues.

  • Step 2: Run smoke test against live backend
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/smoke_test.py -v

Fix any failures.

  • Step 3: End-to-end export test

With the dev server running:

  1. Upload test-data/sample-batch-01-ok.csv
  2. Verify records load
  3. Click Export
  4. Verify the downloaded CSV opens correctly and contains the new status labels
  • Step 4: Run full test suite
cd /Users/sttil-solutions/projects/signal && python -m pytest tests/ -v

All tests must pass.

  • Step 5: Final commit
cd /Users/sttil-solutions/projects/signal
git add -u
git commit -m "fix: placeholder cleanup + smoke test passing + export verified"

Task 15: Deploy frontend to Vercel

  • Step 1: Build locally to check for errors
cd /Users/sttil-solutions/projects/signal/signal-ui && pnpm build

Expected: Build succeeds with no errors.

  • Step 2: Push to trigger Vercel deploy
cd /Users/sttil-solutions/projects/signal && git push

Vercel auto-deploys from the connected git branch.

  • Step 3: Verify live at Vercel URL

Open https://signal-ui-xi.vercel.app and repeat the end-to-end test from Task 14 Step 3 on the live URL.


Self-Review — Spec Coverage Check

Spec Section Covered by Task
Visit date priority chain (CSV > Supabase > estimated) Task 2 (coverage_calculator), Task 6 (normalizer)
Confirm Visit modal + live update Task 11, 12, 13
90/60/45 priority tiers Task 2
SUPPLY_LAPSED / VISIT_REQUIRED labels Task 2, Task 9 (Badge)
5-item doc state machine Task 3
Cascade display Task 3 (logic), Task 13 (UI)
5-dot DocStatusBar Task 10, 13
Payer-dependent PA/PECOS requirements Task 3, Task 4
Transferred patient detection + status Task 2, 6
Confirmed visit persists across imports Task 5 (Supabase), Task 7 (endpoint)
PA/NJ payer strings in payer_rules.json Task 4
Medicare Advantage distinguished from Medicare FFS Task 4, 6
Export verified Task 14
Placeholder removal Task 14
Browser smoke test Task 14

Not covered in this plan (separate plans):

  • CSV generator (pa-set, nj-set, state-change patients) → see csv-generator plan
  • Legal docs (LOI + NDA) → see legal-docs plan
  • Compliance docs (Privacy Policy etc.) → see compliance-docs plan
  • Watcher Agent → demo-mention only, not deployed this cycle