# Orphan Data Recovery Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use > superpowers:subagent-driven-development (recommended) or > superpowers:executing-plans to implement this plan task-by-task. Steps > use checkbox (`- [ ]`) syntax for tracking. **Goal:** Recover stranded billing data from `ingest/`, fix the synthetic-batch read-side bomb, and verify the dashboard KPI tiles come back to life after the recovery run. **Architecture:** Three surgical changes — one defensive read-side fallback in `store/batches.py`, one new offline CLI `cyclone recover-ingest`, one verification script. No new HTTP endpoints, no schema changes, no SFTP interaction. Uses `CycloneStore.add()` as the canonical write path so live-tail pages light up immediately. **Tech Stack:** Python 3.11+, FastAPI/uvicorn (for FastAPI bootstrap helper to share `EventBus`), SQLAlchemy (existing models), Click (existing CLI), Pydantic v2 (existing models). **Spec:** [`docs/superpowers/specs/2026-07-07-cyclone-orphan-data-recovery-design.md`](../specs/2026-07-07-cyclone-orphan-data-recovery-design.md) --- ## File structure ``` backend/src/cyclone/ store/batches.py # A: defensive stub in _row_to_record cli.py # B: add `cyclone recover-ingest` submission/ recover.py # B: shared parse-then-store logic (new, small) backend/tests/ test_store_batches_synthetic.py # A: unit (the defensive stub needs synthetic rows with NULL raw_result_json — these are ORM-level test fixtures for the read path, not EDI samples) test_api_batches_synthetic.py # A: integration test_cli_recover_ingest.py # B: cli + integration (uses existing co_medicaid_* fixtures; no new minimal_* EDI mirrors) backend/scripts/ verify_dashboard_recovery.py # post-recovery sanity check (operator-only) docs/superpowers/specs/ 2026-07-07-cyclone-dashboard-mess-postmortem.md # deep-dive writeup ``` ## Task 0: Setup — branch + verify - [ ] Confirm `main` is the working branch and is clean: `git status -sb`. - [ ] Create the branch: `git checkout -b sp25-orphan-data-recovery`. - [ ] Confirm DB file location: `sqlite3 ~/.local/share/cyclone/cyclone.db ".tables"` returns the 22 tables. ## Task 1: A — failing test for `_row_to_record` defensive stub - [ ] Add `backend/tests/test_store_batches_synthetic.py` with two cases: 1. `test_row_to_record_handles_null_raw_json` — insert a Batch with `raw_result_json=None, kind='837p', input_filename=''`, call `store.list_batches()`, assert the stub returns a `BatchRecord` with `result.summary.total_claims==0` and empty `result.claims`. 2. `test_row_to_record_handles_null_raw_json_835` — same but `kind='835'`, plus assert `result.claims==[]`. - [ ] Run `cd backend && .venv/bin/pytest tests/test_store_batches_synthetic.py -v` — both tests must fail (`ValidationError` on `summary`). ## Task 2: A — implement stub in `_row_to_record` - [ ] In `backend/src/cyclone/store/batches.py`, replace the body of `_row_to_record` (lines ~47-79) with a defensive version: - If `row.raw_result_json` is None or `{}`, build a stub `ParseResult` (837p) with `envelope=None, claims=[], summary=BatchSummary(input_file=row.input_filename, control_number=row.transaction_set_control_number, total_claims=0, passed=0, failed=0)` and a `Date.today()`-based `transaction_date`, **or** a stub `ParseResult835` (835) with `envelope=Envelope(sender_id="",receiver_id="",control_number=row.transaction_set_control_number,transaction_date=date.today()), financial_info=FinancialInfo(... zeros ...), trace=ReassociationTrace(...), payer=Payer835(...), payee=Payee835(...), claims=[], summary=`. - Otherwise behave as today. - [ ] Re-run `tests/test_store_batches_synthetic.py` — both tests now pass. - [ ] Run the full backend suite: `cd backend && .venv/bin/pytest -q`. Must stay green. ## Task 3: A — verify against the live DB - [ ] Restart-free smoke: `curl -sS 'http://127.0.0.1:8000/api/batches?limit=5' | python -m json.tool` — expect 200, `items[].claimCount==0` for the 4 synthetic rows, `inputFilename==""`. ## Task 4: B — failing test for `recover-ingest` against real co_medicaid fixtures - [ ] Add `backend/tests/test_cli_recover_ingest.py` with three cases. Test inputs come from existing `backend/tests/fixtures/co_medicaid_837p.txt` + `co_medicaid_835.txt` — real production EDI already in the repo. **No synthetic EDI fixtures are introduced.** The ingest/ files are reserved for the operator-only recovery run (Task 6), not unit-tested. 1. `test_ingest_835_persists_remittance` — invoke `recover-ingest` via Click's `CliRunner` with `co_medicaid_835.txt`; assert a Remittance row was created with `total_paid > 0`, plus a `processed_inbound_files` row with `sftp_block_name='manual-recover'`. 2. `test_ingest_837p_persists_claims` — invoke with `co_medicaid_837p.txt`; assert ≥1 Claim row + a Batch row that references it. 3. `test_ingest_idempotent` — invoke twice with the same file; second run is a no-op (skip messages, no extra rows). - [ ] Run the test file: must fail (no `recover-ingest` command yet). ## Task 5: B — implement `cyclone recover-ingest` - [ ] Add `backend/src/cyclone/submission/recover.py` with one function: - `def recover_file(path: Path, *, sftp_block_name: str = "manual-recover") -> dict` — detects kind from `ST01*` (837 → '837p'; 835 → '835'), calls the matching parser, constructs a `BatchRecord`, calls `CycloneStore.add(record, event_bus=local_bus)`, returns `{"file": str(path), "status": "ok"|"duplicate"|"failed", "batch_id": str|None, "error": str|None}`. - Wraps the dedup check around `processed_inbound_files` `(sftp_block_name, path.name)`. - [ ] In `backend/src/cyclone/cli.py`, add the Click command `recover-ingest`: - Options: `--file PATH` (repeatable), `--sftp-block-name` (default `manual-recover`). - Validates that each `--file` exists. - Calls `recover_file` per file; exits 0 if at least one ingested OK, 2 if a file parse failed, 1 otherwise. - [ ] Re-run `tests/test_cli_recover_ingest.py` — all three pass. - [ ] Run the full backend suite: `cd backend && .venv/bin/pytest -q`. Stays green. ## Task 6: C — line-reconciliation is automatic; verify with the live DB - [ ] Capture the pre-recovery `/api/dashboard/kpis` snapshot into a file: - `curl -sS 'http://127.0.0.1:8000/api/dashboard/kpis' > /tmp/recovery_pre.json`. - [ ] Run the recovery against the 5 real files: - `.venv/bin/python -m cyclone.cli recover-ingest --file ingest/tp11525703-837P-20260701162932052-1of1.txt --file ingest/tp11525703-837P-20260701162935524-1of1.txt --file ingest/tp11525703-837P-20260701162938977-1of1.txt --file ingest/tp11525703-837P-20260701162942746-1of1.txt --file ingest/tp11525703-835_M019771179-20260706005516577-1of1.x12`. - Expect: 4× 837P → 4 new `Batch` rows + 338 `Claim` rows; 1× 835 → 1 new `Batch` + 1 `Remittance` + ~1,148 `service_line_payments` rows + N `Match` rows (auto-reconciled). - [ ] Capture the post-recovery `/api/dashboard/kpis` snapshot: `curl ... > /tmp/recovery_post.json`. - [ ] Open `backend/scripts/verify_dashboard_recovery.py` (write the script during this step): - Parses both JSON files; asserts `count_post >= count_pre + 338`, `billed_post >= $57,986`, `pending_post >= 339`. - Optionally prints the top-provisioners delta so the operator eyeballs the result. ## Task 7: write the postmortem doc - [ ] Create `docs/superpowers/specs/2026-07-07-cyclone-dashboard-mess-postmortem.md`: - Header: `# 2026-07-07 Cyclone Dashboard postmortem`. - Sections: `## What we saw / ## Root cause / ## What we fixed / ## KPI values before & after / ## Open watch-items`. - Embed the before/after KPI JSON snapshots as fenced code blocks under `## KPI values before & after`. ## Task 8: final verification + merge - [ ] `cd backend && .venv/bin/pytest -q` — green. - [ ] `npm test` in repo root — green. - [ ] `git status -sb` — clean working tree. - [ ] `git log --oneline -10` — at minimum: docs(spec), docs(plan), `feat(sp25):`, postmortem commit. - [ ] Commit the chain (the merge commit is the last one): - `git commit -m 'docs(spec): design for SP25 orphan data recovery (Step 1)'` - `git commit -m 'docs(plan): implementation plan for SP25 orphan data recovery (Step 1)'` - `git commit -am 'feat(sp25): defensive read-side stub for synthetic-batch rows + recover-ingest CLI (Steps 2-5)'` - `git commit -m 'docs: dashboard mess postmortem (Step 7)'` - [ ] Merge into `main` with `--no-ff`: - `git checkout main && git merge --no-ff sp25-orphan-data-recovery -m 'merge: SP25 orphan data recovery into main'` - Confirm a single atomic merge commit with all the above in its `git log`.