Signal/docs/readiness-model-design-phase2-2026-06-26.md

15 KiB

Readiness Model Design Brief — Phase 2 for Claude

Date: 2026-06-26 Status: Design output from Pi. Claude builds from this. Author: Pi (designed) → Claude (builds) Prerequisite context: docs/readiness-model-brief-2026-06-23.md (Phase 1 spec — dedup + verdict engine ALREADY SHIPPED by Claude) E2E verification: tests/signal_e2e_test.py (33 black-box tests, Pi runs, all green)


1. State of play

What shipped (Phase 1, done and tested):

  • core/dedup.py — CSV row → MergedShipment → CoverageLine grouping. Locked dedup key (patient_id, device_type, DOS). Plan type conflict resolution, quantity merge, component split handling, transfer flag carry-forward. 42 tests (30 Claude unit + 12 Pi E2E).
  • core/readiness.py — Verdict engine. Two-axis grading (required_state + input_state). False green killed. All 5 line statuses reachable. Plan type never guessed. Config-based required-ness (payer_rules.json). 46 tests (27 Claude unit + 19 Pi E2E).
  • Code is additive only — not wired into the live pipeline. The old coverage_calculator.py still runs.

What remains to build (Phase 2):

  1. Rules-to-addressable-config (citation IDs on every rule)
  2. Synonym vocabularies in config (move from Python sets to payer_rules.json)
  3. Patient rollup (worst-gradeable-line aggregation)
  4. Device-keyed override + recompute + rollup
  5. Require-plan-type enforcement + remove name-guesser/default in LIVE path
  6. Frontend nesting + "plan type needed" UX (Pi design, Claude builds)

This brief covers items 1-5 (backend). Item 6 is a separate Pi design brief for the frontend.


2. E2E findings that require design decisions

2A. PECOS "Pending" synonym gap

The test test_readiness_on_track proved that SWO and PA accept "Pending" as a legitimate supplied state (their quality functions include a PENDING fallback for unrecognized values). PECOS does not — it returns BAD for any unknown value.

Current behavior:

  • csv_swo_status="Pending" → quality PENDING (not GOOD, not BAD)
  • csv_pa_status="Pending" → quality PENDING (not GOOD, not BAD)
  • csv_pecos_verified="Pending" → quality BAD (fail-safe forces to BAD for required Medicare items)

Design decision: Add "pending" to _PECOS_GOOD? Or keep it BAD because PECOS verification is binary (enrolled/not enrolled, no in-progress state)?

My recommendation: Keep PECOS "Pending" as BAD. PECOS enrollment is a binary check done by the supplier at setup. "Pending" means they have not checked yet, or the check is incomplete. Both cases are a real gap. SWO and PA have legitimate in-progress states (document pending review, authorization in process). PECOS does not. Document this in supplier training: PECOS values must be Yes/Enrolled/Active or No/Inactive — "Pending" will flag as At Risk.

2B. _load_rules mutability hazard

_load_rules() uses @lru_cache(maxsize=1) and returns the mutable dict directly. Any caller that modifies the returned dict poisons the shared cache for all subsequent callers (the test_readiness_known_plan_missing_config bug).

Design decision: Should _load_rules return a frozen/deep-copied dict, or leave it mutable (faster, no copy overhead)?

My recommendation: Leave it mutable. The dict is only ever read in production (no production code mutates it). The test bug is a testing mistake, not a production risk. Add a docstring warning: "Returns the live cached dict. Do not mutate outside testing."

2C. Commercial PA requirement confirmed

payer_rules.json sets pa_required: true for commercial plans. This was tested and verified — missing PA for a commercial patient correctly produces ACTION_NEEDED. No code change needed. Document for supplier training: commercial plan patients need PA just like MA and Medicaid.


3. Phase 2 build sequence (dependency-ordered)

Item 1: Rules-to-addressable-config (citation IDs)

Why: Every verdict must cite the rule that fired (section 5 of Phase 1 brief). Prerequisite for cascade entries and audit trail.

What to build:

Add a rule_id to every rule value in payer_rules.json:

"payer_rules": {
  "medicare": {
    "visit_renewal_days": { "value": 180, "rule_id": "R001" },
    "refill_window_days": { "value": 30, "rule_id": "R002" },
    "pa_required": { "value": false, "rule_id": "R003" },
    "pecos_required": { "value": true, "rule_id": "R004" }
  }
}

Update _load_rules() to understand the new format (backward compatible with flat values for now — the old flat format is also accepted so the CURRENT payer_rules.json still works).

Update _required_from_config() to return the rule_id alongside RequiredState.

Update evaluate_readiness() to include rule_id in each DocItem (add a rule_id: Optional[str] field).

Rule ID registry (stable — never change an ID after shipping):

Rule ID Name Description
R001 visit_renewal_days Visit renewal window in days (standard: 180)
R002 refill_window_days Refill window in days before coverage end (standard: 30)
R003 pa_required Prior authorization required for this plan type
R004 pecos_required PECOS enrollment verification required for this plan type
R100 swo_required SWO is a universal claim requirement per LCD L33822
R101 visit_required Qualifying visit is a universal requirement per LCD L33822
R102 diagnosis_required Diagnosis (ICD-10) is a universal requirement per LCD L33822

R100-R102 are always REQUIRED regardless of plan type. They live in the code as constants (no plan-type dependency), but get rule IDs for citation consistency.

Tests: test_evaluate_readiness_returns_rule_ids — verify each DocItem has the correct rule_id.

Item 2: Move synonym vocabularies to config

Why: The synonym sets live in Python hardcoded sets:

_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_BAD = {"no", "missing", "none", "false", "0", "not on file", "absent", "n/a"}

These need stable rule IDs and belong in payer_rules.json alongside the other rules. This is the "extract rules to addressable config" step from the Phase 1 brief (section 5).

Add to payer_rules.json:

"vocabularies": {
  "swo_good": { "values": ["on file", "on-file", "active", "valid", "received", "signed", "complete"], "rule_id": "V001" },
  "swo_bad": { "values": ["expired", "revoked", "cancelled", "canceled", "voided"], "rule_id": "V002" },
  "pecos_good": { "values": ["yes", "verified", "true", "1", "enrolled", "active", "approved"], "rule_id": "V003" },
  "pecos_bad": { "values": ["no", "not verified", "false", "0", "inactive", "denied", "revoked"], "rule_id": "V004" },
  "pa_good": { "values": ["approved", "not required", "authorized", "auth on file", "on file"], "rule_id": "V005" },
  "pa_bad": { "values": ["denied", "expired", "cancelled", "canceled", "revoked"], "rule_id": "V006" },
  "diagnosis_bad": { "values": ["no", "missing", "none", "false", "0", "not on file", "absent", "n/a"], "rule_id": "V007" }
}

The vocabularies go into their own top-level key (not nested under payer_rules) because they are NOT payer-specific — they apply to all plan types. The READER functions (e.g., _swo_quality, _pecos_quality) read from the vocab section.

Update quality functions: Each quality function accepts an optional rules dict (matching the existing pattern on evaluate_readiness). If rules has a vocabularies section, read synonym sets from there. If not, fall back to the Python hardcoded sets (backward compatible).

Document the PECOS design decision: "Pending" is intentionally not in pecos_good — it produces BAD, which is correct for required Medicare items where PECOS verification should be binary.

Tests: Existing synonym tests still pass (fallback). Add test_vocabularies_in_config_override_python_sets — config-sourced synonyms take precedence.

Item 3: Patient rollup

Why: The worklist shows one row per patient, not per coverage line (section 3 of Phase 1 brief). Patient-level status rolls up from coverage lines.

What to build:

Create a PatientRollup dataclass:

@dataclass
class PatientRollup:
    patient_id: str
    lines: list[CoverageLine]       # all coverage lines for this patient
    rollup_status: LineStatus       # worst gradeable line determines this
    cgm_line_count: int             # gradeable CGM lines
    total_line_count: int           # all lines (including non-gradeable)
    plan_type_needed: bool          # any line in PLAN_TYPE_NEEDED

Rollup rules (from section 4 of Phase 1 brief):

  • Light = worst gradeable line. Severity: Plan Type Needed > At Risk > Action Needed > On Track > Clear to Ship.
  • Any gradeable line in "Plan Type Needed" caps the patient no greener than Action Needed.
  • Non-CGM (gradeable=False) lines are excluded from the light and all tab counts.
  • Tabs count patients, not rows.

Design note on Plan Type Needed capping: A patient with one line in Clear to Ship and another in Plan Type Needed should show as Action Needed (not Plan Type Needed — that would be misleading because the patient IS being served, just not all devices are gradeable). This prevents "Plan Type Needed" from overwhelming the worklist when the supplier has one missing plan type mapping for a secondary device.

Tests: test_patient_rollup_single_line, test_patient_rollup_mixed_statuses, test_patient_rollup_plan_type_needed_capping, test_patient_rollup_non_cgm_excluded.

Item 4: Device-keyed override + recompute + rollup

Why: Staff overrides (e.g., confirming a visit date, marking SWO as on-file) need to apply to the correct device, recompute that line, and re-roll-up the patient (section 6 of Phase 1 brief).

What to build:

Create Override dataclass:

@dataclass(frozen=True)
class OverrideKey:
    org_id: str
    patient_hash: str
    device_type: str
    doc_type: str          # "visit" | "swo" | "pa" | "pecos" | "diagnosis"

@dataclass
class Override:
    key: OverrideKey
    status: str            # e.g., "Confirmed", "On File", "Approved"
    status_date: date
    expiry_date: Optional[date]
    confirmed_by: str
    confirmed_at: datetime

Add a function apply_overrides(coverage_lines: dict[CoverageLineKey, CoverageLine], overrides: list[Override]):

  • For each line, check if any override exists whose OverrideKey matches.
  • If VISIT doc_type override exists, apply to ALL lines for that patient (visit is patient-scoped).
  • If any other doc_type override exists, apply only to lines with matching device_type (line-scoped).
  • After applying overrides, re-grade the affected line(s).
  • After re-grading affected lines, re-roll-up the patient.

Scope boundary from Phase 1 brief (section 10): Only two override types in pilot scope: visit (patient-scoped) + SWO/PA (line-scoped). No general override framework. No override for PECOS or diagnosis this phase.

Tests: test_override_visit_applies_to_all_patient_lines, test_override_swo_applies_only_to_one_device, test_override_triggers_recompute, test_recompute_triggers_rerollup.

Item 5: Require-plan-type enforcement + remove name-guesser in LIVE path

Why: The old _normalize_payer in coverage_calculator.py keyword-matches payer names and can route an unknown plan to a default grade path (section 8 + blocker 1 of Phase 1 brief). This violates the "never guess" decision. The new dedup+readiness path already enforces this, but the LIVE pipeline still uses the OLD path.

What to build:

  1. In coverage_calculator.py, add a path that routes through the new dedup+readiness pipeline when plan_type is available on ShipmentRecord. Gate on: if record.plan_type is not None, use the new path. If record.plan_type is None, the old path remains (for backward compatibility with existing CSV uploads that lack the mapped field).

  2. Deprecate _normalize_payer with a loud warning log: "WARNING: plan_type not provided — falling back to payer-name guess. This path will be removed. Supply plan_type in CSV import."

  3. Add a ReadinessVerdict field to CoverageResult so the frontend can read readiness status alongside coverage flags. This is a DATA MODEL change: CoverageResult gains an optional readiness: ReadinessVerdict field that is None when using the old path.

Tests: test_live_path_routes_to_new_engine_when_plan_type_present, test_live_path_falls_back_when_plan_type_missing, test_deprecation_warning_logged.


4. Migration strategy (zero-downtime)

The old path and new path must coexist during migration:

  1. Phase 2a (this build): New dedup+readiness runs alongside old pipeline. Both paths execute. CoverageResult carries readiness as an optional field. Frontend ignores it until wired (item 6, separate Pi brief). Old path still serves the worklist. Supplier training for plan_type mapping begins.

  2. Phase 2b (after frontend is ready): Frontend switches to new readiness data. Old path still runs but no one looks at it.

  3. Phase 2c (after validation): Old pipeline removed. plan_type becomes REQUIRED on CSV import. _normalize_payer deleted.

No cutoff date — Kisa decides when to cut over. Code must support both paths simultaneously.


5. API surface for frontend

After Phase 2, the /worklist endpoint returns:

{
  "patient_id": "P001",
  "rollup_status": "ACTION_NEEDED",
  "lines": [
    {
      "device_type": "dexcom_g7",
      "plan_type": "medicare",
      "gradeable": true,
      "line_status": "Clear to Ship",
      "doc_items": [
        {"doc_type": "swo", "status": "On File", "required": true, "satisfied": true, "rule_id": "R100"},
        {"doc_type": "visit", "status": "2026-05-15", "required": true, "satisfied": true, "rule_id": "R101"},
        {"doc_type": "pecos", "status": "Yes", "required": true, "satisfied": true, "rule_id": "R004"},
        {"doc_type": "pa", "status": "Not Required", "required": false, "satisfied": true, "rule_id": "R003"},
        {"doc_type": "diagnosis", "status": "E11.9", "required": true, "satisfied": true, "rule_id": "R102"}
      ]
    }
  ]
}

Frontend renders per the separate UX design brief (item 6, Pi-side).


6. Open design questions for Kisa

  1. PECOS "Pending" treatment. My recommendation: keep as BAD (binary field, no in-progress state). Confirm?
  2. Plan Type Needed capping rule. My recommendation: cap at Action Needed, not Plan Type Needed, when only one of multiple lines is unknown. Confirm?
  3. Migration timeline. No fixed date — Kisa decides when to cut over. Start supplier training on plan_type mapping when the frontend is ready.
  4. Phase 2c (delete old pipeline). Only after the new path has served real customers for at least 2 weeks without regression. Confirm?

End of Readiness Model Design Brief — Phase 2.

Claude: Build items 1-5 in order. Each step is auditor-reviewed before the next begins. Pi: Will design the frontend UX (item 6) as a separate brief when you reach Phase 2b.