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.pystill runs.
What remains to build (Phase 2):
- Rules-to-addressable-config (citation IDs on every rule)
- Synonym vocabularies in config (move from Python sets to payer_rules.json)
- Patient rollup (worst-gradeable-line aggregation)
- Device-keyed override + recompute + rollup
- Require-plan-type enforcement + remove name-guesser/default in LIVE path
- 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:
-
In
coverage_calculator.py, add a path that routes through the new dedup+readiness pipeline whenplan_typeis available onShipmentRecord. Gate on: ifrecord.plan_type is not None, use the new path. Ifrecord.plan_type is None, the old path remains (for backward compatibility with existing CSV uploads that lack the mapped field). -
Deprecate
_normalize_payerwith 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." -
Add a
ReadinessVerdictfield toCoverageResultso the frontend can read readiness status alongside coverage flags. This is a DATA MODEL change:CoverageResultgains an optionalreadiness: ReadinessVerdictfield 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:
-
Phase 2a (this build): New dedup+readiness runs alongside old pipeline. Both paths execute.
CoverageResultcarriesreadinessas 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. -
Phase 2b (after frontend is ready): Frontend switches to new readiness data. Old path still runs but no one looks at it.
-
Phase 2c (after validation): Old pipeline removed.
plan_typebecomes REQUIRED on CSV import._normalize_payerdeleted.
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
- PECOS "Pending" treatment. My recommendation: keep as BAD (binary field, no in-progress state). Confirm?
- Plan Type Needed capping rule. My recommendation: cap at Action Needed, not Plan Type Needed, when only one of multiple lines is unknown. Confirm?
- Migration timeline. No fixed date — Kisa decides when to cut over. Start supplier training on plan_type mapping when the frontend is ready.
- 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.