Add readiness verdict engine (kills the false green)
Grade one coverage line's five documentation items on two separated axes the old code conflated: required_state (from plan_type + payer_rules.json) and input_state (SUPPLIED | ABSENT). 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, never granted by absence of contradiction. - plan_type never guessed: None / unmapped / casing-variant -> "Plan Type Needed", structurally cannot be green; PECOS/PA sit NOT_EVALUATED while universal items (SWO/visit/diagnosis per LCD L33822) still grade. - Required-ness reads from payer_rules.json (single source of truth), removing the latent Python-set-vs-JSON divergence. - Config-surface fail-safe: a known plan missing/partial in config cannot grade green (NOT_EVALUATED), and the green gate checks every item. - Diagnosis accepts a real ICD-10 code as on-file; only explicit negatives gap. Additive only: not wired into the live pipeline. 27 tests green (full suite 73), code-auditor reviewed (H1/H2/M1/M2/M3/M4/L1 fixed). PROVISIONAL, flagged for Pi/Kisa: gap->label mapping (At Risk/Action Needed/ On Track) and the visit-recency scope boundary (timing layer, not readiness). Spec: docs/readiness-model-brief-2026-06-23.md section 4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c07d054ca5
commit
206bf718bd
3 changed files with 544 additions and 1 deletions
|
|
@ -18,7 +18,7 @@ Canonical, git-tracked record of **what shipped, what didn't, and the reason or
|
|||
| 6 | Convex-Signal spike **design** | Pi | **SHIPPED** | docs/convex-signal-spike-design-2026-06-24.md. 40h/3-phase plan; Module map + Repository interface + Convex schema + Auth seam (Convex default, Clerk swappable) + Phase 1 (core readiness loop) = what Claude builds first. | Claude builds Phase 1 from it |
|
||||
| 7 | `signal-convex` repo creation | Claude (GitHub) + Kisa/token (Forgejo) | **PENDING** | Dependency: Pi design + Kisa approval (create once, correctly). Claude can do GitHub via `gh`; Forgejo has no API token here. | after design approved |
|
||||
| 8 | Convex BAA email (plan/cost/signing) | Kisa | **IN PROGRESS** | Kisa handling | Kisa sends |
|
||||
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **IN PROGRESS** | Step 3 grouping half SHIPPED 2026-06-26: `core/dedup.py` (CSVRow→MergedShipment→CoverageLine) + 30 tests green, code-auditor reviewed (no High; M1/M2/L1/L2/L3 fixed). Added additive `plan_type` field to ShipmentRecord (default None; live grading untouched). NOT yet wired into the live pipeline. Remaining: required-vs-input verdict engine, rules-to-addressable-config (IDs), device-keyed override+recompute+rollup, require-plan-type enforcement + remove name-guesser/default, frontend nesting + "plan type needed" state. | Next: build the verdict engine + rules-to-config; WIRE orchestration in `api/main.py` or a new pipeline module, NOT inside `coverage_calculator` (dedup imports from it → circular import). Each step code-auditor-reviewed before ship. |
|
||||
| 9 | Readiness-model build (rename coverage→readiness, rules-to-config, grouping+verdict, device override, require-plan-type, frontend nesting) | Claude | **IN PROGRESS** | Step 3 SHIPPED 2026-06-26/27 (additive, not yet wired): (a) `core/dedup.py` grouping (CSVRow→MergedShipment→CoverageLine), 30 tests, auditor-reviewed; (b) `core/readiness.py` verdict engine — required-vs-input two-axis grading that KILLS the false green (required+absent never satisfied; clear-to-ship only on positive evidence; plan unknown→"Plan Type Needed"; required-ness read from payer_rules.json not Python sets), 27 tests, auditor-reviewed (H1/H2 config-surface false greens + M2/M3/M4 fixed). Added additive `plan_type` to ShipmentRecord. **OPEN for Kisa/Pi:** provisional gap→label mapping (At Risk/Action Needed/On Track) + H3 scope boundary (visit recency = timing layer, not readiness verdict — confirm). Remaining: rules-to-addressable-config (citation IDs), patient rollup, device-keyed override+recompute+rollup, require-plan-type enforcement + remove name-guesser/default in the LIVE path, frontend nesting + "plan type needed" UX (→ Pi). | Next: hand frontend/label UX to Pi (brief staged); build patient rollup + WIRE orchestration in `api/main.py`/new pipeline module, NOT inside `coverage_calculator` (circular import). Each step auditor-reviewed. |
|
||||
| 10 | NocoDB leads / CRM cleanup | Claude + Kisa | **BLOCKED** | Reason: leads table has no primary key (Signal-base data source `bdmhi02nks1pg6g` is corrupted — a fresh table there also came up PK-less; content base is healthy). SQLite can't add a PK to a populated table (Route 1 failed). Interim: capture still works (inserts via n8n `signal-cgm-lead`); the 2 test rows can't be deleted. | resolved by the data-platform decision (item 11) |
|
||||
| 11 | Data-platform decision (leads + future PHI backend) | Kisa (with Claude/Pi analysis) | **OPEN** | Options analyzed: repaired-NocoDB / Supabase Postgres / Neon / AWS (already set up, BAA+RDS+S3 parked) / Convex ($25/mo incl. BAA). Leads = non-PHI (stay outside PHI boundary). Decision informed by the Convex spike. | run spike → decide |
|
||||
| 12 | Research-versioning Layer-1 (`research-autopush.sh` + Stop-hook entry) | Claude | **NOT STARTED** | Dependency: Pi design exists (`docs/research-versioning-design-2026-06-24.md`). Lower priority than product. | next infra slot |
|
||||
|
|
|
|||
286
python-backend/core/readiness.py
Normal file
286
python-backend/core/readiness.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
"""
|
||||
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
|
||||
257
tests/test_readiness.py
Normal file
257
tests/test_readiness.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
"""
|
||||
Tests for the readiness verdict engine — the "kill the false green" logic.
|
||||
|
||||
Source spec: docs/readiness-model-brief-2026-06-23.md section 4 ("The verdict rule").
|
||||
|
||||
Two axes, never conflated into one string:
|
||||
required_state: does this plan need this item? (from plan_type + rules)
|
||||
input_state: did the client supply data? (SUPPLIED | ABSENT)
|
||||
|
||||
Per DocItem:
|
||||
NOT_REQUIRED + any input -> satisfied (green-eligible)
|
||||
REQUIRED + SUPPLIED + good -> satisfied
|
||||
REQUIRED + SUPPLIED + bad -> gap
|
||||
REQUIRED + ABSENT -> NOT_EVALUATED, blocks green, never satisfied
|
||||
|
||||
A line is "Clear to Ship" ONLY if EVERY required item is satisfied. Green is
|
||||
earned by positive evidence, never granted by the absence of contradiction.
|
||||
plan_type unknown -> "Plan Type Needed", structurally cannot be green.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python-backend"))
|
||||
|
||||
from datetime import date
|
||||
|
||||
from core.readiness import (
|
||||
evaluate_readiness,
|
||||
RequiredState,
|
||||
InputState,
|
||||
LineStatus,
|
||||
)
|
||||
|
||||
VISIT = date(2026, 5, 1)
|
||||
|
||||
|
||||
def items_by_type(verdict):
|
||||
return {i.doc_type: i for i in verdict.doc_items}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The four-rule verdict table (brief section 4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_not_required_item_is_satisfied_even_when_absent():
|
||||
"""Medicare FFS does not require PA -> PA is satisfied with no input at all."""
|
||||
v = evaluate_readiness("medicare", csv_pa_status=None)
|
||||
pa = items_by_type(v)["pa"]
|
||||
assert pa.required_state == RequiredState.NOT_REQUIRED
|
||||
assert pa.satisfied is True
|
||||
|
||||
|
||||
def test_required_supplied_good_is_satisfied():
|
||||
v = evaluate_readiness("medicare", csv_swo_status="On File")
|
||||
swo = items_by_type(v)["swo"]
|
||||
assert swo.required_state == RequiredState.REQUIRED
|
||||
assert swo.input_state == InputState.SUPPLIED
|
||||
assert swo.satisfied is True
|
||||
|
||||
|
||||
def test_required_supplied_bad_is_gap():
|
||||
v = evaluate_readiness("medicare", csv_swo_status="Expired")
|
||||
swo = items_by_type(v)["swo"]
|
||||
assert swo.satisfied is False
|
||||
|
||||
|
||||
def test_required_absent_blocks_green():
|
||||
"""THE false-green killer: a required item with no data is never satisfied."""
|
||||
v = evaluate_readiness("medicare", csv_swo_status=None)
|
||||
swo = items_by_type(v)["swo"]
|
||||
assert swo.required_state == RequiredState.REQUIRED
|
||||
assert swo.input_state == InputState.ABSENT
|
||||
assert swo.satisfied is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Line status — clear to ship is earned, not defaulted
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _all_docs_good(**overrides):
|
||||
base = dict(
|
||||
csv_swo_status="On File",
|
||||
csv_visit_date=VISIT,
|
||||
csv_pecos_verified="Yes",
|
||||
csv_pa_status="Not Required",
|
||||
csv_diagnosis_on_file="Yes",
|
||||
)
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def test_clear_to_ship_when_every_required_item_satisfied():
|
||||
v = evaluate_readiness("medicare", **_all_docs_good())
|
||||
assert v.line_status == LineStatus.CLEAR_TO_SHIP
|
||||
|
||||
|
||||
def test_missing_required_item_is_not_clear_to_ship():
|
||||
"""Old code greened this (absent column = no gap). New code must not."""
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_swo_status=None))
|
||||
assert v.line_status != LineStatus.CLEAR_TO_SHIP
|
||||
assert v.line_status == LineStatus.ACTION_NEEDED
|
||||
|
||||
|
||||
def test_supplied_bad_required_item_is_at_risk():
|
||||
v = evaluate_readiness("medicare_advantage", **_all_docs_good(csv_pa_status="Denied"))
|
||||
assert v.line_status == LineStatus.AT_RISK
|
||||
|
||||
|
||||
def test_pending_only_is_on_track():
|
||||
"""No hard gap, no missing data, but an item still in progress -> On Track."""
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_swo_status="Pending"))
|
||||
assert v.line_status == LineStatus.ON_TRACK
|
||||
|
||||
|
||||
def test_bad_outranks_absent_in_severity():
|
||||
"""A denied item (At Risk) outranks a missing item (Action Needed)."""
|
||||
v = evaluate_readiness(
|
||||
"medicare_advantage",
|
||||
**_all_docs_good(csv_pa_status="Denied", csv_diagnosis_on_file=None),
|
||||
)
|
||||
assert v.line_status == LineStatus.AT_RISK
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan type unknown — never guess, cannot be green
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_plan_type_none_is_plan_type_needed():
|
||||
v = evaluate_readiness(None, **_all_docs_good())
|
||||
assert v.line_status == LineStatus.PLAN_TYPE_NEEDED
|
||||
|
||||
|
||||
def test_plan_type_unknown_string_is_plan_type_needed():
|
||||
"""An unmapped plan value is treated as unknown, never defaulted/guessed."""
|
||||
v = evaluate_readiness("unknown", **_all_docs_good())
|
||||
assert v.line_status == LineStatus.PLAN_TYPE_NEEDED
|
||||
|
||||
|
||||
def test_plan_unknown_leaves_pecos_and_pa_not_evaluated():
|
||||
v = evaluate_readiness(None, **_all_docs_good())
|
||||
items = items_by_type(v)
|
||||
assert items["pecos"].required_state == RequiredState.NOT_EVALUATED
|
||||
assert items["pa"].required_state == RequiredState.NOT_EVALUATED
|
||||
|
||||
|
||||
def test_plan_unknown_still_grades_universal_items():
|
||||
"""SWO, visit, diagnosis are universal (LCD L33822) -> still graded when plan unknown."""
|
||||
v = evaluate_readiness(None, csv_swo_status="On File")
|
||||
swo = items_by_type(v)["swo"]
|
||||
assert swo.required_state == RequiredState.REQUIRED
|
||||
assert swo.satisfied is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Required-ness comes from payer_rules.json, not duplicated Python sets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pecos_required_for_medicare():
|
||||
v = evaluate_readiness("medicare", csv_pecos_verified=None)
|
||||
assert items_by_type(v)["pecos"].required_state == RequiredState.REQUIRED
|
||||
|
||||
|
||||
def test_pecos_not_required_for_medicaid():
|
||||
"""Medicaid uses state enrollment, not PECOS (deferred) -> PECOS not required."""
|
||||
v = evaluate_readiness("medicaid", csv_pecos_verified=None)
|
||||
assert items_by_type(v)["pecos"].required_state == RequiredState.NOT_REQUIRED
|
||||
|
||||
|
||||
def test_pa_required_for_medicaid():
|
||||
v = evaluate_readiness("medicaid", csv_pa_status=None)
|
||||
assert items_by_type(v)["pa"].required_state == RequiredState.REQUIRED
|
||||
|
||||
|
||||
def test_pa_not_required_for_medicare_ffs():
|
||||
v = evaluate_readiness("medicare", csv_pa_status=None)
|
||||
assert items_by_type(v)["pa"].required_state == RequiredState.NOT_REQUIRED
|
||||
|
||||
|
||||
def test_all_five_items_present():
|
||||
v = evaluate_readiness("medicare", **_all_docs_good())
|
||||
assert set(items_by_type(v).keys()) == {"swo", "visit", "pecos", "pa", "diagnosis"}
|
||||
|
||||
|
||||
def test_diagnosis_supplied_negative_is_gap():
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_diagnosis_on_file="No"))
|
||||
dx = items_by_type(v)["diagnosis"]
|
||||
assert dx.input_state == InputState.SUPPLIED
|
||||
assert dx.satisfied is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hardening (code-auditor findings H1, H2, M1, M2, M3, M4, L1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_known_plan_missing_config_does_not_false_green():
|
||||
"""H1/H2: a known plan absent from payer_rules.json must NOT grade green.
|
||||
|
||||
PECOS/PA cannot be evaluated without their rule config -> NOT_EVALUATED,
|
||||
which must block 'Clear to Ship'. Fails safe, never open.
|
||||
"""
|
||||
v = evaluate_readiness(
|
||||
"medicaid", rules={"payer_rules": {}},
|
||||
**_all_docs_good(csv_pa_status="Denied"),
|
||||
)
|
||||
assert v.line_status != LineStatus.CLEAR_TO_SHIP
|
||||
assert items_by_type(v)["pa"].required_state == RequiredState.NOT_EVALUATED
|
||||
assert items_by_type(v)["pecos"].required_state == RequiredState.NOT_EVALUATED
|
||||
|
||||
|
||||
def test_diagnosis_icd10_code_counts_as_on_file():
|
||||
"""M2: a real ICD-10 code in the diagnosis field is positive evidence, not a gap."""
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_diagnosis_on_file="E11.9"))
|
||||
assert items_by_type(v)["diagnosis"].satisfied is True
|
||||
|
||||
|
||||
def test_diagnosis_explicit_negative_is_bad():
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_diagnosis_on_file="Missing"))
|
||||
assert items_by_type(v)["diagnosis"].satisfied is False
|
||||
|
||||
|
||||
def test_pecos_enrolled_synonym_is_good():
|
||||
"""M3: standard PECOS portal statuses should read as verified."""
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_pecos_verified="Enrolled"))
|
||||
assert items_by_type(v)["pecos"].satisfied is True
|
||||
|
||||
|
||||
def test_swo_active_synonym_is_good():
|
||||
v = evaluate_readiness("medicare", **_all_docs_good(csv_swo_status="Active"))
|
||||
assert items_by_type(v)["swo"].satisfied is True
|
||||
|
||||
|
||||
def test_plan_type_case_and_whitespace_normalized():
|
||||
"""M4: a correctly-mapped plan with stray case/space must still grade."""
|
||||
v = evaluate_readiness(" Medicare ", **_all_docs_good())
|
||||
assert v.line_status == LineStatus.CLEAR_TO_SHIP
|
||||
|
||||
|
||||
def test_staff_confirmed_visit_carries_value_for_citation():
|
||||
"""L1: a staff-confirmed visit (no CSV date) still records its date for the citation."""
|
||||
v = evaluate_readiness(
|
||||
"medicare", confirmed_visit_date=VISIT,
|
||||
csv_swo_status="On File", csv_pa_status="Not Required",
|
||||
csv_diagnosis_on_file="Yes", csv_pecos_verified="Yes",
|
||||
)
|
||||
assert items_by_type(v)["visit"].value == VISIT.isoformat()
|
||||
|
||||
|
||||
def test_known_plan_types_match_payer_rules_config():
|
||||
"""M1 drift guard: the gradeable allow-list must equal the configured plans."""
|
||||
import json
|
||||
rules_path = (
|
||||
Path(__file__).parent.parent
|
||||
/ "python-backend" / "config" / "payer_rules.json"
|
||||
)
|
||||
configured = set(json.loads(rules_path.read_text())["payer_rules"].keys()) - {"default"}
|
||||
from core.readiness import KNOWN_PLAN_TYPES
|
||||
assert set(KNOWN_PLAN_TYPES) == configured
|
||||
Loading…
Reference in a new issue