""" readiness.py Signal — STTIL Solutions The readiness verdict engine: grades ONE coverage line's documentation against the five tracked items, separating two axes the old code conflated — required_state: does this plan need this item? (from plan_type + rules) input_state: did the client supply data? (SUPPLIED | ABSENT) This kills the "false green": a line is "Clear to Ship" only when EVERY required item is satisfied by positive evidence. A required item with no data is NOT_EVALUATED and blocks green — green is never granted by absence of a contradiction. plan_type is NEVER guessed; unknown -> "Plan Type Needed". Spec: docs/readiness-model-brief-2026-06-23.md section 4. Required-ness is read from payer_rules.json (single source of truth), NOT from duplicated Python sets, removing the latent set-vs-JSON divergence the brief flagged. PROVISIONAL (flagged for Pi/Kisa): the mapping of gaps to the four UI labels (At Risk / Action Needed / On Track) below is a reasonable default. The precise label semantics + frontend nesting + "plan type needed" UX are a Pi design task. PHI CONTRACT: operates only on plan_type and the five doc-status inputs. No names, SSNs, DOBs, or contact fields. """ from __future__ import annotations import json import logging from dataclasses import dataclass from datetime import date from enum import Enum from functools import lru_cache from pathlib import Path from typing import Optional logger = logging.getLogger(__name__) PAYER_RULES_PATH = Path(__file__).parent.parent / "config" / "payer_rules.json" # Plan types Signal can grade. Anything else (including None or "unknown") is # treated as "plan type needed" — never defaulted, never guessed. KNOWN_PLAN_TYPES = frozenset( {"medicare", "medicare_advantage", "medicaid", "commercial"} ) # Universal claim/coverage requirements per LCD L33822, independent of plan type. UNIVERSAL_ITEMS = ("swo", "visit", "diagnosis") DOC_TYPES = ("swo", "visit", "pecos", "pa", "diagnosis") class RequiredState(str, Enum): REQUIRED = "REQUIRED" NOT_REQUIRED = "NOT_REQUIRED" NOT_EVALUATED = "NOT_EVALUATED" # cannot decide (plan type unknown) class InputState(str, Enum): SUPPLIED = "SUPPLIED" ABSENT = "ABSENT" class Quality(str, Enum): GOOD = "GOOD" # positive evidence the item is satisfied PENDING = "PENDING" # supplied but in progress (not yet good, not bad) BAD = "BAD" # supplied and contradicting (denied / expired / no) NONE = "NONE" # nothing supplied class LineStatus(str, Enum): CLEAR_TO_SHIP = "Clear to Ship" ON_TRACK = "On Track" ACTION_NEEDED = "Action Needed" AT_RISK = "At Risk" PLAN_TYPE_NEEDED = "Plan Type Needed" @dataclass class DocItem: doc_type: str required_state: RequiredState input_state: InputState quality: Quality value: Optional[str] satisfied: bool @dataclass class ReadinessVerdict: plan_type: Optional[str] doc_items: list[DocItem] line_status: LineStatus @lru_cache(maxsize=1) def _load_rules() -> dict: with open(PAYER_RULES_PATH, "r") as f: return json.load(f) def _norm(value: Optional[str]) -> str: return (value or "").strip().lower() # --- per-item quality vocabularies ----------------------------------------- # Provisional synonym sets, fail-safe by design: an unrecognized value resolves # to PENDING/BAD (over-flag), never GOOD. These belong next to payer_rules.json # once the rules become addressable config; expand from the 25-CSV corpus. _SWO_GOOD = {"on file", "on-file", "active", "valid", "received", "signed", "complete"} _SWO_BAD = {"expired", "revoked", "cancelled", "canceled", "voided"} _PECOS_GOOD = {"yes", "verified", "true", "1", "enrolled", "active", "approved"} _PECOS_BAD = {"no", "not verified", "false", "0", "inactive", "denied", "revoked"} _PA_GOOD = {"approved", "not required", "authorized", "auth on file", "on file"} _PA_BAD = {"denied", "expired", "cancelled", "canceled", "revoked"} # Diagnosis carries a real ICD-10 code OR a yes/no flag, so presence = positive # evidence and only an EXPLICIT negative is a gap (inverted vs the others). _DIAGNOSIS_BAD = {"no", "missing", "none", "false", "0", "not on file", "absent", "n/a"} def _swo_quality(value: Optional[str]) -> Quality: s = _norm(value) if not s: return Quality.NONE if s in _SWO_GOOD: return Quality.GOOD if s in _SWO_BAD: return Quality.BAD return Quality.PENDING # "pending" or any other supplied state def _visit_quality(csv_visit_date: Optional[date], confirmed_visit_date: Optional[date]) -> Quality: # A confirmed/staff visit date on file is positive evidence. The estimated # shipment-minus-30 proxy is NOT client input and does not count here. # SCOPE BOUNDARY (brief section 3): this verdict checks the visit is ON FILE. # Whether it is still WITHIN the 6-month window is the separate timing layer # (CoverageFlag / CoverageLine.timing_flag), combined at the worklist level. if csv_visit_date or confirmed_visit_date: return Quality.GOOD return Quality.NONE def _diagnosis_quality(value: Optional[str]) -> Quality: s = _norm(value) if not s: return Quality.NONE return Quality.BAD if s in _DIAGNOSIS_BAD else Quality.GOOD def _pecos_quality(value: Optional[str]) -> Quality: s = _norm(value) if not s: return Quality.NONE if s in _PECOS_GOOD: return Quality.GOOD if s in _PECOS_BAD: return Quality.BAD return Quality.BAD # required Medicare item: unknown fails safe def _pa_quality(value: Optional[str]) -> Quality: s = _norm(value) if not s: return Quality.NONE if s in _PA_GOOD: return Quality.GOOD if s in _PA_BAD: return Quality.BAD return Quality.PENDING # "pending", "not started", etc. def _make_item(doc_type: str, required: RequiredState, quality: Quality, value: Optional[str]) -> DocItem: input_state = InputState.ABSENT if quality == Quality.NONE else InputState.SUPPLIED if required == RequiredState.NOT_REQUIRED: satisfied = True elif required == RequiredState.NOT_EVALUATED: satisfied = False # cannot confirm -> blocks green else: # REQUIRED satisfied = quality == Quality.GOOD return DocItem( doc_type=doc_type, required_state=required, input_state=input_state, quality=quality, value=value, satisfied=satisfied, ) def _required_from_config(plan_known: bool, plan_cfg: Optional[dict], flag_key: str, plan_type: Optional[str]) -> RequiredState: """Required-ness for a plan-dependent item, read from config. A missing config (known plan absent from payer_rules.json, or the flag key missing) means we CANNOT decide -> NOT_EVALUATED, which blocks green. We never treat a config gap as 'not required' (that was the false-green hole). """ if not plan_known: return RequiredState.NOT_EVALUATED if not plan_cfg or flag_key not in plan_cfg: logger.warning( "plan_type '%s' missing '%s' in payer_rules.json -> cannot grade %s", plan_type, flag_key, flag_key, ) return RequiredState.NOT_EVALUATED return RequiredState.REQUIRED if bool(plan_cfg[flag_key]) else RequiredState.NOT_REQUIRED def evaluate_readiness( plan_type: Optional[str], *, csv_swo_status: Optional[str] = None, csv_visit_date: Optional[date] = None, confirmed_visit_date: Optional[date] = None, csv_pecos_verified: Optional[str] = None, csv_pa_status: Optional[str] = None, csv_diagnosis_on_file: Optional[str] = None, rules: Optional[dict] = None, ) -> ReadinessVerdict: """Grade one coverage line's five documentation items and roll up a line status.""" rules = rules if rules is not None else _load_rules() # Normalize the client-mapped plan type (casing/whitespace only — never guess). plan_type = _norm(plan_type) or None plan_known = plan_type in KNOWN_PLAN_TYPES # Plan-dependent required-ness from config (single source of truth, never sets). plan_cfg = rules.get("payer_rules", {}).get(plan_type) if plan_known else None pecos_required = _required_from_config(plan_known, plan_cfg, "pecos_required", plan_type) pa_required = _required_from_config(plan_known, plan_cfg, "pa_required", plan_type) visit_date = csv_visit_date or confirmed_visit_date items = [ _make_item("swo", RequiredState.REQUIRED, _swo_quality(csv_swo_status), csv_swo_status), _make_item("visit", RequiredState.REQUIRED, _visit_quality(csv_visit_date, confirmed_visit_date), visit_date.isoformat() if visit_date else None), _make_item("diagnosis", RequiredState.REQUIRED, _diagnosis_quality(csv_diagnosis_on_file), csv_diagnosis_on_file), _make_item("pecos", pecos_required, _pecos_quality(csv_pecos_verified), csv_pecos_verified), _make_item("pa", pa_required, _pa_quality(csv_pa_status), csv_pa_status), ] return ReadinessVerdict( plan_type=plan_type, doc_items=items, line_status=_line_status(items, plan_known), ) def _line_status(items: list[DocItem], plan_known: bool) -> LineStatus: """Roll the five items into one line status. Clear to Ship is reachable ONLY when every required item is satisfied. Severity (worst first): Plan Type Needed / At Risk > Action Needed > On Track. PROVISIONAL label mapping (flagged for Pi): At Risk = a supplied-and-bad required item (active contradiction); Action Needed = a required item with no data to chase; On Track = required items only in progress (pending). """ if not plan_known: return LineStatus.PLAN_TYPE_NEEDED # A known plan whose rules are missing from config cannot be fully graded # (PECOS/PA NOT_EVALUATED). Fail safe: surface it, never grade it green. if any(i.required_state == RequiredState.NOT_EVALUATED for i in items): return LineStatus.PLAN_TYPE_NEEDED # Green is earned only when EVERY item is satisfied (NOT_REQUIRED items are # satisfied by definition; required items only by positive evidence). if all(i.satisfied for i in items): return LineStatus.CLEAR_TO_SHIP required = [i for i in items if i.required_state == RequiredState.REQUIRED] if any(i.quality == Quality.BAD for i in required): return LineStatus.AT_RISK if any(i.input_state == InputState.ABSENT for i in required): return LineStatus.ACTION_NEEDED return LineStatus.ON_TRACK