Captured on Fable before access ends. Two grounded, execute-from starter templates under docs/build-plans/: - 01-readiness-model-wiring.md: wire dedup+readiness into the live pipeline via a post-process orchestrator (avoids the dedup->coverage_calculator circular import); flags that the normalizer does not yet ingest plan_type, so every live verdict reads 'Plan Type Needed' until a plan_type column is mapped. - 02-readiness-model-completion.md: P2-P6 (citation IDs, patient rollup, device override, plan-type enforcement, frontend nesting). Readiness+dedup engine verified 57 tests green (2026-07-06); corrects the stale '73 tests' figure in current-state.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 KiB
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 becomescurrent_shipment, earlier onesprior_shipments(dedup.py:218-250).- A fresh
CoverageLineis 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).LineStatusvalues 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), andpayer_rules.jsoncarriespecos_required/pa_requiredfor 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 ofcore.dedup/core.readinessonly intests/test_dedup.py,tests/test_readiness.py, andtests/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:443POST /api/upload->normalize_csv(main.py:457) ->calculate_batch(records, as_of, confirmed_visits)(main.py:492-494) -> oneRecordOutper result via_to_record_out(main.py:502-521). GROUNDED. - Single-record path (Confirm Visit recompute):
main.py:670POST /api/confirm-visit-> builds oneShipmentRecordfrom 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_batchreturnslist[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.CoverageResultcarries 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 fromcompute_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_labelcomes fromFLAG_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)
- Circular-import constraint.
dedup.py:35doesfrom core.coverage_calculator import ShipmentRecord, CoverageFlag. Thereforecoverage_calculator.pycan never importdedup/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 fromapi/main.py. GROUNDED (import verified; conclusion follows necessarily). - The normalizer never populates
plan_type.ShipmentRecordhas had aplan_typefield since the 2026-06-23 lock (coverage_calculator.py:79-82), butnormalize_csvconstructs records WITHOUT it (normalizer.py:315-331) andHEADER_MAPhas 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 hasplan_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 bytest_dedup_missing_order_numberandtest_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
- Repo at or after the 2026-06-27 clean checkpoint;
python -m pytest tests/green (verified 108 passed on 2026-07-06). - Railway CLI authenticated; backend service
signal-api-production-91c2. Deploy command:railway up --detachfrom the project root (forces fresh build). SIGNAL_API_KEYvalue available for curl-based live verification (X-API-Key header path, main.py:117-118).- 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.
- 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
- 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)whereplan_type_idisplan_type or "unknown"(mirrorsCoverageLineKey, dedup.py:85-90). Internally:dedup()->assign_to_coverage_lines()-> per line,evaluate_readiness()online.current_shipment's csv_* fields, passingconfirmed_visit_datelooked up bysha256(patient_id)from the confirmed_visits dict (same keying as coverage_calculator.py:387-388).readiness_for_record(record, confirmed_visit_date) -> ReadinessVerdictfor the single-record Confirm Visit path (one record is its own line; callevaluate_readinessdirectly).- Log only hashed patient_ids (reuse the
_hash_pidpattern, dedup.py:40-42).
- Add additive response fields in
api/main.py:- New Pydantic model
ReadinessItemOut(doc_type, required_state, input_state, quality, value, satisfied). RecordOutgainsplan_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.UploadResponsegainsreadiness_stats: dict | None = None(counts per LineStatus value) so live verification is measurable without the UI.
- New Pydantic model
- Wire the batch path in
upload_csvimmediately aftercalculate_batch(main.py:492-494): computeverdicts = readiness_by_line(records, confirmed_visits); when building each RecordOut (main.py:502-521), resolve the row's original ShipmentRecord via the existingrecord_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 likedoc_statedegrades today (main.py:317-319). - Wire the single-record path in
confirm_visit: addplan_type: str | None = NonetoConfirmVisitRequest(main.py:649-667), pass it into theShipmentRecordconstruction (main.py:735-750; the field already exists on ShipmentRecord), computereadiness_for_record(record, confirmed_visit_date=confirmed), and pass it to_to_record_outat main.py:766. The frontend already echoes RecordOut fields back on confirm-visit (RecordOut comment, main.py:192-194); addingplan_typeto 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). - (Decision-gated, Open Question 1) Minimal plan_type ingestion in
api/normalizer.py: add aplan_typeentry 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_readinessalready 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). - 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. - Independent review: hand the diff to code-auditor before ship (generator and
reviewer are never the same agent). Then run the
signal-e2e-testskill so the readiness verification is not a closed loop. - Deploy and live-verify per section 9. Then update
docs/ship-status-ledger.mdandcontext/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 ofsha256(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_readinesscounts 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_readinessconsumes exactly the fieldsMergedShipmentcarries (csv_swo_status, csv_visit_date, csv_pecos_verified, csv_pa_status, csv_diagnosis_on_file, plan_type) plusconfirmed_visit_date. Verified field-by-field against dedup.py:64-83 and readiness.py:216-226. GROUNDED.CoverageLine.plan_type == "unknown"feeds straight intoevaluate_readiness, which routes it to Plan Type Needed because "unknown" is not in KNOWN_PLAN_TYPES (readiness.py:47-49, 231-232; tested bytest_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)
core/worklist_readiness.py (new)
"""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)
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
# 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,
# 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:
- 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 (legacystatus_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.) - Plan Type Needed. Same record with
plan_type=None(today's default for every normalized record). Assertreadiness_status == "Plan Type Needed"and PECOS/PA items are NOT_EVALUATED while universal items still grade. (Engine twins: readiness tests at lines 128-146.) - 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). Assertreadiness_status == "Clear to Ship". If Open Question 1 is approved, also prove it END TO END throughnormalize_csvwith a plan_type column; if not approved, prove it at thereadiness_by_linelevel with constructed records and note the end-to-end gap explicitly. - 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).
- 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. - 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_statstotals equal the number of distinct graded lines' rows accounted for (define precisely in the test). - PHI check. Assert no raw patient_id appears in log output captured from
readiness_by_line(caplog); hashed only. - 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).
- Deploy:
railway up --detachfrom/Users/sttil-solutions/projects/signal. Watch build logs (railway logs) until healthy. curl -s https://signal-api-production-91c2.up.railway.app/health->status: ok.- 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_statuskey; - with no plan_type column: 100% of graded records read "Plan Type Needed" (count them with jq, compare to total);
readiness_statspresent and its counts sum correctly.
- every record has a
- 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_labelstill shows the legacy green label. This is the false-green kill, observed live. - Confirm Visit round trip: POST
/api/confirm-visitfor the Action Needed patient's visit case (test 5 shape) and assert the returned record'sreadiness_statusflips as specified. - 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.
railway logsspot check: any new log lines show hashed patient_ids only.- 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.valuecarries 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_pidpattern, dedup.py:40-42). - Locked labels:
readiness_statusemits theLineStatusvalues 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.pymust never importdedup/readiness/worklist_readiness(dedup.py:35 imports FROM it). The new module is only imported byapi/main.pyand tests. _load_rulescache hazard:@lru_cachereturns 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_daysin 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 redeployonly 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:
core/worklist_readiness.pyexists; imports of dedup/readiness inpython-backend/are exactly: worklist_readiness (plus tests).- Both call-sites wired:
/api/upload(batch, main.py:492 region) and/api/confirm-visit(single, main.py:751 region) return readiness fields. - 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).
- Back-compat invariant proven (section 8 case 6): legacy response fields byte-identical for a fixture CSV.
- code-auditor review passed (independent agent, findings resolved or accepted).
- signal-e2e-test skill run against the wired pipeline, results recorded.
- Deployed via
railway up --detach; all live-verify steps in section 9 pass with measured counts, not impressions. docs/ship-status-ledger.mdentry appended;context/current-state.mdupdated via state-sync with one author-stamped decision line.- 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
- 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.
- 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.
readiness_statsin the upload response now? Cheap, additive, makes live verification measurable. Recommendation: yes. GRAFTED.- 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.
- 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.