# Build Plan 01 — Wire the Readiness Model into the Live Pipeline

**Date:** 2026-07-06
**Author:** lead-signal (Claude), grounded against the working tree on this date
**Status:** PLAN ONLY. No production code has been modified. Execute-from-here document
for a future session (possibly a less capable model). Every load-bearing claim is
tagged GROUNDED (verified in a file or by running a command) or GRAFTED (assumption
or recommendation).
**Prior design docs this builds on (read them, do not re-derive them):**
- `docs/readiness-model-brief-2026-06-23.md` (Phase 1 spec, locked decisions)
- `docs/readiness-model-design-phase2-2026-06-26.md` (Pi's Phase 2 design, esp. section 4 migration strategy)
- `docs/dedup-design-2026-06-24.md` (locked dedup key and tiebreakers)

---

## 1. Objective

Wire the already-built, already-tested readiness engines (`core/dedup.py` +
`core/readiness.py`) into the live FastAPI pipeline as an ADDITIVE readiness verdict
on both scoring paths (CSV batch upload and Confirm Visit recompute), changing no
existing worklist behavior, per the Phase 2a coexistence strategy.

---

## 2. Current state (GROUNDED)

### The engines exist, tested, and are wired to NOTHING

- `python-backend/core/dedup.py` (251 lines). GROUNDED.
  - `dedup(records: list[ShipmentRecord]) -> list[MergedShipment]` groups rows by the
    LOCKED key `(patient_id, device_type, shipment_date-as-date)` (dedup.py:185-215).
  - `assign_to_coverage_lines(merged) -> dict[CoverageLineKey, CoverageLine]` groups
    MergedShipments by `(patient_id, device_type, plan_type or "unknown")`; latest DOS
    becomes `current_shipment`, earlier ones `prior_shipments` (dedup.py:218-250).
  - A fresh `CoverageLine` is intentionally ungraded: `line_status=None`,
    `timing_flag=None` (dedup.py:246-247). Grading is a separate step. GROUNDED.
- `python-backend/core/readiness.py` (287 lines). GROUNDED.
  - `evaluate_readiness(plan_type, *, csv_swo_status, csv_visit_date,
    confirmed_visit_date, csv_pecos_verified, csv_pa_status, csv_diagnosis_on_file,
    rules) -> ReadinessVerdict` (readiness.py:216-255). Grades ONE line's five doc
    items on two axes (required_state x input_state).
  - `ReadinessVerdict = {plan_type, doc_items: list[DocItem], line_status: LineStatus}`
    (readiness.py:92-96). `DocItem = {doc_type, required_state, input_state, quality,
    value, satisfied}` (readiness.py:82-89).
  - `LineStatus` values are EXACTLY the locked worklist labels plus the sanctioned
    unknown-plan state: "Clear to Ship", "On Track", "Action Needed", "At Risk",
    "Plan Type Needed" (readiness.py:74-79). GROUNDED. Do not invent or rename any.
  - Required-ness is read from `config/payer_rules.json` (readiness.py:197-213, 234-237),
    and `payer_rules.json` carries `pecos_required` / `pa_required` for all four known
    plan types (payer_rules.json:32-72). GROUNDED.
  - A required-and-absent item blocks green (readiness.py:183-185, 278-279); unknown or
    missing plan config yields "Plan Type Needed" (readiness.py:268-274). GROUNDED.
- **Neither module is imported anywhere in `python-backend/`.** Grep on 2026-07-06 finds
  imports of `core.dedup` / `core.readiness` only in `tests/test_dedup.py`,
  `tests/test_readiness.py`, and `tests/signal_e2e_test.py`. GROUNDED.

### Real test status (run 2026-07-06 from repo root, `python -m pytest`)

| Suite | Result |
|---|---|
| `tests/test_dedup.py` | 30 passed |
| `tests/test_readiness.py` | 27 passed |
| `tests/signal_e2e_test.py` (Pi's black-box E2E) | 35 passed |
| Full `tests/` directory | **108 passed** |

GROUNDED (I ran them). Note: `context/current-state.md` (2026-06-27 entry) says
"73 tests green"; the grounded per-file counts today are the table above. The 57
engine unit tests (30 + 27) match the 2026-06-26 state entry. Treat the table as
current truth; do not quote 73.

### The live pipeline scores through coverage_calculator only

- `python-backend/api/main.py:25` — `from core.coverage_calculator import
  ShipmentRecord, calculate_batch`. GROUNDED.
- **Batch path (CSV upload -> worklist):** `main.py:443` `POST /api/upload` ->
  `normalize_csv` (main.py:457) -> `calculate_batch(records, as_of, confirmed_visits)`
  (main.py:492-494) -> one `RecordOut` per result via `_to_record_out`
  (main.py:502-521). GROUNDED.
- **Single-record path (Confirm Visit recompute):** `main.py:670` `POST
  /api/confirm-visit` -> builds one `ShipmentRecord` from echoed request fields
  (main.py:735-750) -> `calculate_coverage(record, confirmed_visit_date=confirmed)`
  (main.py:751, import at main.py:679) -> `_to_record_out` (main.py:766). GROUNDED.
- `calculate_batch` returns `list[CoverageResult]`, ONE PER INPUT ROW, sorted by
  priority (coverage_calculator.py:368-396). The worklist is row-keyed, not
  patient-keyed (the known bug, Phase 1 brief section 8). GROUNDED.
- `CoverageResult` carries the TIMING axis only (`CoverageFlag`: SUPPLY_LAPSED,
  VISIT_REQUIRED, RENEWAL_*, RESUPPLY_READY, ACTIVE, NO_RECENT_SHIPMENT,
  TRANSFER_PENDING; coverage_calculator.py:42-51, 86-100). It has no doc-completeness
  verdict. The doc display state comes separately from `compute_doc_state`
  (main.py:321-330), which is driven by `_normalize_payer_type` -> the payer-name
  guesser (`_normalize_payer`, imported at main.py:257). GROUNDED.
- The live `status_label` comes from `FLAG_LABELS` (main.py:136-146), which is a
  LEGACY label set ("Docs Required", "Escalate", "New Patient", "Confirm Appointment",
  "Begin Outreach", "Clear to Ship", "On Track", "No Recent Shipment"). It is NOT the
  locked four-label verdict set. This plan does not touch it; replacing it is the P6
  frontend-nesting step owned by another agent. GROUNDED.

### Two wiring realities discovered by reading the code (both load-bearing)

1. **Circular-import constraint.** `dedup.py:35` does `from core.coverage_calculator
   import ShipmentRecord, CoverageFlag`. Therefore `coverage_calculator.py` can never
   import `dedup`/`readiness` (circular). The Phase 2 doc's item 5 sketch ("add a path
   in coverage_calculator.py") cannot be implemented literally as written. The seam
   must live OUTSIDE coverage_calculator: a new orchestration module called from
   `api/main.py`. GROUNDED (import verified; conclusion follows necessarily).
2. **The normalizer never populates `plan_type`.** `ShipmentRecord` has had a
   `plan_type` field since the 2026-06-23 lock (coverage_calculator.py:79-82), but
   `normalize_csv` constructs records WITHOUT it (normalizer.py:315-331) and
   `HEADER_MAP` has no plan_type aliases (normalizer.py:20-88). Header matching is
   exact-normalized (lowercase, `-`/`_` -> space; normalizer.py:169-189), so a client
   column named "plan_type"/"Plan Type" is currently UNMAPPED and dropped. Beware the
   near-collision: bare "plan", "plan_name", "plan name" already alias to PAYER
   (normalizer.py:46-50). Consequence: on day one of wiring, every record has
   `plan_type=None`, so every readiness verdict reads "Plan Type Needed". That is
   doctrinally correct (never guess) but means no green is reachable end to end until
   plan_type enters via CSV mapping. GROUNDED. See Open Question 1.

### Doctrine locks this plan must honor (from the source-of-truth docs)

- Dedup key LOCKED: `(patient_id, device_type, date_of_service)`; order_number is
  display-only, never in the key (dedup design doc section 1; implemented at
  dedup.py:189, tested by `test_dedup_missing_order_number` and
  `test_dedup_order_numbers_collected_and_deduped`). GROUNDED.
- Verdict rules LOCKED: green only by positive evidence on every required item;
  required-and-absent blocks green; unknown plan reads "Plan Type Needed", never a
  guess (Phase 1 brief section 4; implemented and tested:
  `test_required_absent_blocks_green`, `test_plan_type_none_is_plan_type_needed`,
  `test_known_plan_missing_config_does_not_false_green`). GROUNDED.
- Migration strategy LOCKED by Pi's Phase 2 design section 4: Phase 2a = both paths
  execute, readiness carried as an optional field, frontend ignores it until wired,
  no cut-over date without Kisa. This plan IS Phase 2a. GROUNDED (doc read).
- Grade the CURRENT shipment only (latest DOS per line); prior shipments are collapsed
  history (Phase 1 brief section 7; dedup.py:226, 238-245). GROUNDED.

---

## 3. Prerequisites / dependencies

1. Repo at or after the 2026-06-27 clean checkpoint; `python -m pytest tests/` green
   (verified 108 passed on 2026-07-06).
2. Railway CLI authenticated; backend service `signal-api-production-91c2`. Deploy
   command: `railway up --detach` from the project root (forces fresh build).
3. `SIGNAL_API_KEY` value available for curl-based live verification (X-API-Key header
   path, main.py:117-118).
4. Decision from Kisa on Open Question 1 (plan_type CSV ingestion now vs with P5).
   The wiring ships either way; the decision changes only whether "Clear to Ship" is
   reachable end to end on day one.
5. No dependency on the Convex spike, Supabase schema changes, or the frontend.
   Frontend is untouched in this step.

Downstream items explicitly NOT in this plan (owned by the P2-P6 agent; reference
only): rules-to-config citation IDs, synonym vocabularies to config, patient rollup,
device-keyed override, plan-type enforcement + guesser removal, frontend nesting.

---

## 4. Implementation approach

**Seam decision: the readiness verdict POST-PROCESSES (augments) the
coverage_calculator output. It does not wrap it and does not replace it.** Grounds:

- The two engines grade DIFFERENT axes. coverage_calculator = timing urgency
  (CoverageFlag); readiness = documentation completeness (LineStatus). readiness.py's
  own scope-boundary comment says the timing layer is "combined at the worklist
  level" (readiness.py:139-142). Neither output is a superset of the other. GROUNDED.
- The Phase 2 design section 4 mandates coexistence (Phase 2a): both paths execute,
  readiness rides as an optional field, old path still serves the worklist. GROUNDED.
- Wrapping inside coverage_calculator is impossible without a circular import
  (section 2 above). GROUNDED.
- Replacing would change output cardinality (dedup collapses rows to lines) and break
  the row-keyed frontend, `_compute_stats` (main.py:397-416), and the export echo
  (main.py:559-620) in one step. Cardinality-preserving augmentation keeps this step
  additive and reversible; the cardinality change belongs to P3 rollup + P6 frontend.
  GRAFTED (design judgment, consistent with the locked migration strategy).

**How the augment works, concretely:** dedup the batch's ShipmentRecords into
CoverageLines, grade each line's CURRENT shipment with `evaluate_readiness`, then
attach each line's verdict to EVERY row-level RecordOut belonging to that line. Row
count, ordering, stats, legacy labels: all unchanged. Rows from prior shipments of a
line carry the same line verdict (graded from the latest DOS), which is the locked
grade-the-current-shipment rule surfacing at row level. GRAFTED (mechanism), built on
GROUNDED locks.

### Ordered steps

1. **Create `python-backend/core/worklist_readiness.py`** (new module, the only new
   import surface). Two functions:
   - `readiness_by_line(records, confirmed_visits) -> dict[tuple, ReadinessVerdict]`
     keyed by `(patient_id, device_type, plan_type_id)` where `plan_type_id` is
     `plan_type or "unknown"` (mirrors `CoverageLineKey`, dedup.py:85-90).
     Internally: `dedup()` -> `assign_to_coverage_lines()` -> per line,
     `evaluate_readiness()` on `line.current_shipment`'s csv_* fields, passing
     `confirmed_visit_date` looked up by `sha256(patient_id)` from the
     confirmed_visits dict (same keying as coverage_calculator.py:387-388).
   - `readiness_for_record(record, confirmed_visit_date) -> ReadinessVerdict` for the
     single-record Confirm Visit path (one record is its own line; call
     `evaluate_readiness` directly).
   - Log only hashed patient_ids (reuse the `_hash_pid` pattern, dedup.py:40-42).
2. **Add additive response fields in `api/main.py`:**
   - New Pydantic model `ReadinessItemOut` (doc_type, required_state, input_state,
     quality, value, satisfied).
   - `RecordOut` gains `plan_type: Optional[str] = None`,
     `readiness_status: Optional[str] = None`,
     `readiness_items: Optional[list[ReadinessItemOut]] = None`. All default None so
     old clients, the export echo (`ExportRequest.records: list[RecordOut]`,
     main.py:559-561), and the frontend are unaffected.
   - `UploadResponse` gains `readiness_stats: dict | None = None` (counts per
     LineStatus value) so live verification is measurable without the UI.
3. **Wire the batch path** in `upload_csv` immediately after `calculate_batch`
   (main.py:492-494): compute `verdicts = readiness_by_line(records,
   confirmed_visits)`; when building each RecordOut (main.py:502-521), resolve the
   row's original ShipmentRecord via the existing `record_lookup` (main.py:496-500),
   derive its line key, and pass the matching verdict into `_to_record_out` (new
   optional parameter). If the record lookup misses, readiness fields stay None,
   exactly like `doc_state` degrades today (main.py:317-319).
4. **Wire the single-record path** in `confirm_visit`: add
   `plan_type: str | None = None` to `ConfirmVisitRequest` (main.py:649-667), pass it
   into the `ShipmentRecord` construction (main.py:735-750; the field already exists
   on ShipmentRecord), compute `readiness_for_record(record,
   confirmed_visit_date=confirmed)`, and pass it to `_to_record_out` at main.py:766.
   The frontend already echoes RecordOut fields back on confirm-visit (RecordOut
   comment, main.py:192-194); adding `plan_type` to RecordOut in step 2 makes the
   round trip whole once the frontend echoes it (frontend change itself is P6; until
   then the field arrives None and the recompute honestly reads Plan Type Needed).
5. **(Decision-gated, Open Question 1) Minimal plan_type ingestion in
   `api/normalizer.py`:** add a `plan_type` entry to HEADER_MAP with aliases such as
   `"plan_type", "plan type", "plan category", "coverage type", "insurance_type",
   "insurance type"`. NEVER include bare `"plan"`, `"plan_name"`, or `"plan name"`
   (those are payer aliases today, normalizer.py:46-50; stealing them would silently
   re-map existing customer CSVs). Carry the value verbatim, lowercased and trimmed
   only; `evaluate_readiness` already normalizes and routes anything outside the four
   known values to Plan Type Needed (readiness.py:231-232, 47-49). This is
   client-supplied mapping, not derivation from the payer name, so it does not
   violate the never-guess lock. GRAFTED (recommendation; Kisa gates).
6. **Tests** (new file `tests/test_worklist_readiness.py` + API-level tests): see
   section 8. Run the FULL suite; all 108 existing tests must still pass.
7. **Independent review:** hand the diff to code-auditor before ship (generator and
   reviewer are never the same agent). Then run the `signal-e2e-test` skill so the
   readiness verification is not a closed loop.
8. **Deploy and live-verify** per section 9. Then update
   `docs/ship-status-ledger.md` and `context/current-state.md` (state-sync) and
   commit. Do not update the pilot-readiness checklist unless an item genuinely
   flips; count PASS items before quoting any percentage.

---

## 5. Files to touch

| File | Change |
|---|---|
| `python-backend/core/worklist_readiness.py` | NEW. Orchestration module: `readiness_by_line`, `readiness_for_record`. Imports dedup + readiness. Nothing imports it except api/main.py and tests. |
| `python-backend/api/main.py` | Add `ReadinessItemOut`; extend `RecordOut` (+3 optional fields) and `UploadResponse` (+1 optional field); extend `ConfirmVisitRequest` (+optional plan_type); new optional `readiness` param on `_to_record_out`; call `readiness_by_line` in `upload_csv` after line 492; call `readiness_for_record` in `confirm_visit` before line 766; small `_compute_readiness_stats` helper. |
| `python-backend/api/normalizer.py` | ONLY if Open Question 1 is approved: `plan_type` aliases in HEADER_MAP + pass-through into ShipmentRecord construction (normalizer.py:315-331) + it appears in `mapping_summary` automatically via the existing header loop. |
| `tests/test_worklist_readiness.py` | NEW. Unit tests for the orchestration module + API-shape tests (section 8). |
| `docs/ship-status-ledger.md` | Append the ship entry after live verification. |
| `context/current-state.md` | Update ACTIVE/NEXT + one author-stamped decision via state-sync. |

Files explicitly NOT touched: `core/coverage_calculator.py` (circular-import
constraint plus additive-only discipline), `core/dedup.py`, `core/readiness.py`
(already audited; do not modify while wiring), `core/doc_state_machine.py`,
`config/payer_rules.json`, anything in `signal-ui/` (P6), persistence/Supabase schema
(verdicts are recomputed per upload, not persisted, this step).

---

## 6. Integration points

### A. Batch path, `api/main.py:492` (`POST /api/upload`)

Today:
```
records (normalize_csv) ──> calculate_batch ──> CoverageResult per row ──> RecordOut per row
```
After wiring:
```
records ──┬─> calculate_batch ──────────────> CoverageResult per row ──┐
          └─> readiness_by_line (dedup ──> lines ──> evaluate_readiness)┴─> RecordOut per row
                                                                            + readiness_status
                                                                            + readiness_items
                                                                            + plan_type
```
- Inputs already in scope at the call-site: `records` (main.py:457),
  `confirmed_visits` (main.py:486, dict of `sha256(patient_id) -> date`).
- Row-to-line resolution: the existing `record_lookup` (main.py:496-500) maps a
  CoverageResult back to its ShipmentRecord; the line key is
  `(record.patient_id, record.device_type, record.plan_type or "unknown")`.
- Legacy outputs (`flag`, `status_label`, `priority_score`, sort order, `stats`,
  row count) are byte-identical to before. The verdict rides alongside.

### B. Single-record path, `api/main.py:751` (`POST /api/confirm-visit`)

- `calculate_coverage(record, confirmed_visit_date=confirmed)` stays exactly as is
  (timing axis).
- Add `verdict = readiness_for_record(record, confirmed_visit_date=confirmed)` and
  pass it to `_to_record_out(result, record=record, confirmed_visit_date=confirmed,
  readiness=verdict)` at main.py:766.
- `evaluate_readiness` counts a confirmed visit as positive evidence
  (readiness.py:135-144), so confirming a visit flips the visit item to satisfied in
  the same response, no second upload needed.

### C. What plugs into what (function-level contract)

- `evaluate_readiness` consumes exactly the fields `MergedShipment` carries
  (csv_swo_status, csv_visit_date, csv_pecos_verified, csv_pa_status,
  csv_diagnosis_on_file, plan_type) plus `confirmed_visit_date`. Verified
  field-by-field against dedup.py:64-83 and readiness.py:216-226. GROUNDED.
- `CoverageLine.plan_type == "unknown"` feeds straight into `evaluate_readiness`,
  which routes it to Plan Type Needed because "unknown" is not in KNOWN_PLAN_TYPES
  (readiness.py:47-49, 231-232; tested by
  `test_plan_type_unknown_string_is_plan_type_needed`). GROUNDED.

---

## 7. Skeleton snippets (illustrative only, do NOT paste blindly; re-verify line numbers at execution time)

> **EXECUTION CORRECTION (2026-07-07, found in adversarial review):** the sketches
> below key row-to-verdict resolution off `rec.plan_type` (the row's OWN value).
> That is WRONG when a row's plan_type differs from its dedup group's MERGED
> plan_type (conflicting groups merge to None) — it can attach another line's
> verdict, a reachable false green. The shipped implementation resolves rows
> through the dedup group key `(patient_id, device_type, DOS-as-date)` via
> `ReadinessIndex` in `core/worklist_readiness.py`. Follow the shipped code,
> not these sketches.

### `core/worklist_readiness.py` (new)

```python
"""Orchestrates dedup + readiness for the live pipeline (Phase 2a coexistence).
PHI CONTRACT: patient_id only; hash it in any log line."""
import hashlib
from datetime import date
from typing import Optional

from core.coverage_calculator import ShipmentRecord
from core.dedup import dedup, assign_to_coverage_lines, UNKNOWN_PLAN_TYPE
from core.readiness import evaluate_readiness, ReadinessVerdict

LineKey = tuple[str, str, str]  # (patient_id, device_type, plan_type_id)


def _confirmed_for(patient_id: str, confirmed_visits: dict[str, date]) -> Optional[date]:
    return confirmed_visits.get(hashlib.sha256(patient_id.encode()).hexdigest())


def readiness_for_record(record: ShipmentRecord,
                         confirmed_visit_date: Optional[date] = None) -> ReadinessVerdict:
    return evaluate_readiness(
        record.plan_type,
        csv_swo_status=record.csv_swo_status,
        csv_visit_date=record.csv_visit_date,
        confirmed_visit_date=confirmed_visit_date,
        csv_pecos_verified=record.csv_pecos_verified,
        csv_pa_status=record.csv_pa_status,
        csv_diagnosis_on_file=record.csv_diagnosis_on_file,
    )


def readiness_by_line(records: list[ShipmentRecord],
                      confirmed_visits: Optional[dict[str, date]] = None,
                      ) -> dict[LineKey, ReadinessVerdict]:
    confirmed_visits = confirmed_visits or {}
    lines = assign_to_coverage_lines(dedup(records))
    verdicts: dict[LineKey, ReadinessVerdict] = {}
    for key, line in lines.items():
        cur = line.current_shipment          # grade the CURRENT shipment only (locked)
        verdicts[(key.patient_id, key.device_type, key.plan_type_id)] = evaluate_readiness(
            line.plan_type if line.plan_type != UNKNOWN_PLAN_TYPE else None,
            csv_swo_status=cur.csv_swo_status,
            csv_visit_date=cur.csv_visit_date,
            confirmed_visit_date=_confirmed_for(key.patient_id, confirmed_visits),
            csv_pecos_verified=cur.csv_pecos_verified,
            csv_pa_status=cur.csv_pa_status,
            csv_diagnosis_on_file=cur.csv_diagnosis_on_file,
        )
    return verdicts
```

### `api/main.py` additions (sketch)

```python
class ReadinessItemOut(BaseModel):
    doc_type: str
    required_state: str      # REQUIRED | NOT_REQUIRED | NOT_EVALUATED
    input_state: str         # SUPPLIED | ABSENT
    quality: str             # GOOD | PENDING | BAD | NONE
    value: Optional[str] = None
    satisfied: bool

# RecordOut gains (all additive, default None):
#   plan_type: Optional[str] = None
#   readiness_status: Optional[str] = None      # one of the 5 LineStatus values, verbatim
#   readiness_items: Optional[list[ReadinessItemOut]] = None
# UploadResponse gains:
#   readiness_stats: dict | None = None
```

```python
# upload_csv, right after main.py:492-494:
from core.worklist_readiness import readiness_by_line
verdicts = readiness_by_line(records, confirmed_visits)

# inside the RecordOut build loop: resolve rec = record_lookup.get(...) as today, then
line_verdict = None
if rec is not None:
    line_verdict = verdicts.get(
        (rec.patient_id, rec.device_type, rec.plan_type or "unknown"))
# pass readiness=line_verdict into _to_record_out; inside _to_record_out:
#   readiness_status=verdict.line_status.value if verdict else None,
#   plan_type=(getattr(record, "plan_type", None) if record else None),
#   readiness_items=[ReadinessItemOut(..., required_state=i.required_state.value, ...)
#                    for i in verdict.doc_items] if verdict else None,
```

```python
# confirm_visit, before main.py:766:
from core.worklist_readiness import readiness_for_record
verdict = readiness_for_record(record, confirmed_visit_date=confirmed)
return _to_record_out(result, record=record, confirmed_visit_date=confirmed,
                      readiness=verdict)
# and ConfirmVisitRequest gains: plan_type: str | None = None
# passed into the ShipmentRecord(... plan_type=body.plan_type) construction.
```

---

## 8. Test strategy & acceptance criteria

New file `tests/test_worklist_readiness.py`. The engine-level behaviors are already
covered (57 unit tests + 35 E2E); the new tests target the WIRING, i.e. the seams
this plan adds. GROUNDED engine coverage, GRAFTED wiring tests to write:

1. **False green is blocked at the API layer (the headline acceptance case).**
   One record, `plan_type="medicare"`, SWO "On File", csv_visit_date present,
   diagnosis "Yes", **PECOS absent (None)**, shipment recent enough that the legacy
   flag reads RESUPPLY_READY (legacy `status_label` "Clear to Ship" per FLAG_LABELS,
   main.py:143). Assert: `readiness_status == "Action Needed"` on the same RecordOut.
   This is the exact false-green the model exists to kill, visible in one response:
   legacy label green-ish, readiness verdict honest. (Engine-level twin:
   `test_required_absent_blocks_green`, tests/test_readiness.py:67.)
2. **Plan Type Needed.** Same record with `plan_type=None` (today's default for every
   normalized record). Assert `readiness_status == "Plan Type Needed"` and PECOS/PA
   items are NOT_EVALUATED while universal items still grade. (Engine twins:
   readiness tests at lines 128-146.)
3. **Clear to Ship is reachable.** `plan_type="medicare"`, all five items with
   positive evidence (PA not required for Medicare FFS per payer_rules.json:36).
   Assert `readiness_status == "Clear to Ship"`. If Open Question 1 is approved, also
   prove it END TO END through `normalize_csv` with a plan_type column; if not
   approved, prove it at the `readiness_by_line` level with constructed records and
   note the end-to-end gap explicitly.
4. **Dedup wiring.** Two CSV rows, same (patient, device, DOS), different
   order_numbers -> one line verdict; both output rows (if both survive to output)
   carry the identical verdict. Two rows same patient+device with DIFFERENT DOS
   (a resupply history) -> still ONE line; assert the verdict is graded from the
   LATEST DOS row's csv fields (change a doc field between the two rows to prove
   which one drove the verdict).
5. **Confirm Visit recompute.** Record with plan_type medicare, everything good
   except no visit evidence -> upload path gives "Action Needed"; POST
   /api/confirm-visit with a valid confirmed date -> response
   `readiness_status == "Clear to Ship"` and the visit item satisfied.
6. **Back-compat invariants.** For a fixture CSV, the wired response equals the
   pre-wiring response on every legacy field: same row count, same order, same
   `flag`/`status_label`/`priority_score`/`stats`. Readiness fields are the only
   additions. Also: `readiness_stats` totals equal the number of distinct graded
   lines' rows accounted for (define precisely in the test).
7. **PHI check.** Assert no raw patient_id appears in log output captured from
   `readiness_by_line` (caplog); hashed only.
8. **Full suite.** `python -m pytest tests/` must show 108 + new tests passed, zero
   failures, before deploy.

**Acceptance criteria (all must hold):** cases 1 and 2 pass exactly as stated (these
prove the false-green kill and the never-guess doctrine in the live pipeline); case 6
proves the step was truly additive; full suite green; code-auditor sign-off recorded.

---

## 9. Live-verify steps (drive the real app, measure, never eyeball)

Backend-only change, so verification is API-level against the live Railway service.
The UI renders nothing new by design (Phase 2a: frontend ignores the new fields).

1. Deploy: `railway up --detach` from `/Users/sttil-solutions/projects/signal`.
   Watch build logs (`railway logs`) until healthy.
2. `curl -s https://signal-api-production-91c2.up.railway.app/health` -> `status: ok`.
3. Upload a synthetic fixture CSV (use the signal-e2e-test skill's generator; NEVER
   real data):
   `curl -s -X POST .../api/upload -H "X-API-Key: $SIGNAL_API_KEY" -F "file=@fixture.csv"`.
   Measure, do not skim:
   - every record has a `readiness_status` key;
   - with no plan_type column: 100% of graded records read "Plan Type Needed"
     (count them with jq, compare to total);
   - `readiness_stats` present and its counts sum correctly.
4. If plan_type ingestion shipped (Open Question 1): upload the fixture WITH a
   plan_type column containing one engineered Clear to Ship row (medicare, all
   evidence positive) and one engineered false-green row (medicare, PECOS blank,
   timing green). Assert with jq: the first reads "Clear to Ship", the second reads
   "Action Needed" while its legacy `status_label` still shows the legacy green
   label. This is the false-green kill, observed live.
5. Confirm Visit round trip: POST `/api/confirm-visit` for the Action Needed
   patient's visit case (test 5 shape) and assert the returned record's
   `readiness_status` flips as specified.
6. Regression: re-run the exact pre-wiring fixture and diff the response against a
   saved pre-deploy response, ignoring only the new keys. Zero legacy diffs allowed.
7. `railway logs` spot check: any new log lines show hashed patient_ids only.
8. Record results (counts, not adjectives) in `docs/ship-status-ledger.md`.

---

## 10. Constraints & gotchas

- **PHI (non-negotiable):** patient_id is the sole crosswalk key. `DocItem.value`
  carries only client status strings or ISO dates, never identity fields. Never add
  name/DOB/SSN/contact anywhere, including test fixtures. Hash patient_id in every
  new log line (`_hash_pid` pattern, dedup.py:40-42).
- **Locked labels:** `readiness_status` emits the `LineStatus` values verbatim: At
  Risk, Action Needed, On Track, Clear to Ship, plus Plan Type Needed for unknown
  plans. Never invent a sixth, never rename, and never mix in the resupply-lifecycle
  set (Supply Lapsed, Renewal Due, Resupply Ready, Active, Outreach Worklist), which
  is a separate locked vocabulary for the pilot guide and copy. The legacy
  FLAG_LABELS map (main.py:136-146) stays untouched until P6.
- **Locked dedup key:** `(patient_id, device_type, date_of_service)`. order_number is
  display-only. Do not "improve" the key while wiring.
- **Circular import:** `coverage_calculator.py` must never import
  `dedup`/`readiness`/`worklist_readiness` (dedup.py:35 imports FROM it). The new
  module is only imported by `api/main.py` and tests.
- **`_load_rules` cache hazard:** `@lru_cache` returns the live mutable dict
  (readiness.py:99-102). Tests must never mutate it (documented hazard, Phase 2
  design section 2B).
- **Normalizer alias collision:** "plan", "plan_name", "plan name" mean PAYER today
  (normalizer.py:46-50). plan_type aliases must not include them.
- **Language:** "resupply", never "refill", in any new copy, field names, or test
  names. (`refill_window_days` in payer_rules.json is an existing code key; leave it,
  renaming config keys is not this step.)
- **Prior-shipment rows carry the current shipment's verdict.** A six-month history
  upload shows N legacy rows for one line, all with the same line verdict graded from
  the latest DOS. By design (grade the current shipment only). Expect it; do not
  "fix" it here; the row collapse is P3/P6.
- **Verdicts are not persisted.** `persist_upload` (main.py:538-546) stores shipment
  records and coverage results only. Readiness is recomputed per request this phase.
  Persisting verdicts is a schema decision that goes through lead-sttil's data
  handling check first.
- **Deploy mechanics:** backend `railway up --detach` (fresh build; `railway
  redeploy` only reuses the old image and will NOT pick up code changes). Frontend is
  untouched this step; if it ever needs a deploy, Vercel is manual (`vercel --prod`,
  the GitHub webhook is broken).
- **Auth on verify:** use the X-API-Key header path; Clerk JWT is the frontend path.

---

## 11. Definition of Done

All of the following, in order, with evidence recorded:

1. `core/worklist_readiness.py` exists; imports of dedup/readiness in
   `python-backend/` are exactly: worklist_readiness (plus tests).
2. Both call-sites wired: `/api/upload` (batch, main.py:492 region) and
   `/api/confirm-visit` (single, main.py:751 region) return readiness fields.
3. Full test suite green (108 existing + new wiring tests), run and count recorded,
   including acceptance cases 1 (false green blocked) and 2 (Plan Type Needed).
4. Back-compat invariant proven (section 8 case 6): legacy response fields
   byte-identical for a fixture CSV.
5. code-auditor review passed (independent agent, findings resolved or accepted).
6. signal-e2e-test skill run against the wired pipeline, results recorded.
7. Deployed via `railway up --detach`; all live-verify steps in section 9 pass with
   measured counts, not impressions.
8. `docs/ship-status-ledger.md` entry appended; `context/current-state.md` updated
   via state-sync with one author-stamped decision line.
9. No production change outside the files listed in section 5; no PHI fields added;
   no label invented; dedup key untouched.

---

## 12. Open questions / decisions needed from Kisa

1. **Plan-type CSV ingestion now, or wait for P5?** (The single biggest call.)
   Without it, every live verdict reads "Plan Type Needed" on day one: honest and
   doctrine-correct, but no green is reachable end to end and the new field looks
   inert. My recommendation: include the minimal verbatim column mapping (section 4
   step 5) in this wiring. It is a client-supplied mapped field, not a guess, so it
   honors the lock; the full enforcement, guesser removal, and mapping-review UX stay
   with P5's owner. GRAFTED recommendation; Kisa gates.
2. **Confirm the seam:** readiness AUGMENTS the coverage result (Phase 2a
   coexistence), it does not replace the worklist scoring yet. This follows Pi's
   locked migration strategy, but the wiring makes it real, so I want it said out
   loud and confirmed.
3. **`readiness_stats` in the upload response now?** Cheap, additive, makes live
   verification measurable. Recommendation: yes. GRAFTED.
4. **Cut-over timing** (Phase 2b/2c: frontend switches, old path retired) remains
   entirely Kisa's decision per the Phase 2 design, section 4. Nothing in this plan
   pre-commits it.
5. **State-file correction:** current-state.md says "73 tests green" (2026-06-27
   entry); today's grounded counts are 30 + 27 unit, 35 E2E, 108 total. Approve
   recording the corrected counts at the next state-sync so no one re-quotes 73.
