28 KiB
Sub-project 36 — API Routers Split: Design Spec
Date: 2026-07-06
Status: Merged into main (f005494 merge: SP36 api-routers-split into main on 2026-07-07)
Branch: sp36-api-routers-split (merged + deleted)
Aesthetic direction: No UI changes. Pure backend structural refactor — the public HTTP surface is byte-for-byte identical.
1. Scope
backend/src/cyclone/api.py has grown to 4,341 LOC across 63 route handlers and 2 exception handlers. The api_routers/ subpackage exists but holds only 5 small routers (acks, admin, claim_acks, health, ta1_acks, totalling ~700 LOC). SP21 split the persistence layer into a cyclone/store/ subpackage; SP36 finishes that era of work by splitting the HTTP surface into per-resource routers that mirror the store's domain boundaries.
The split is behaviour-preserving: every URL, every HTTP method, every status code, every response shape, every header is unchanged. The migration set, the store facade, the pubsub event contract, the auth boundary, the live-tail wire format, the parser pipeline, the CLI, and the frontend are all untouched. The work is moving code from one file to many files, registering the new files as FastAPI routers, and verifying the public surface is byte-for-byte identical.
In scope:
- Extract the 63 routes currently in
api.pyinto per-domain routers underapi_routers/:parse.py(5 routes: 837, 835, 999, ta1, 277ca)inbox.py(6 routes: lanes, candidates/match, candidates/dismiss, payer-rejected/acknowledge, rejected/resubmit, export.csv)batches.py(3 routes: list, get, export-837) + the three_batch_summary_*helpersclaims.py(5 routes: list, stream, get, serialize-837, line-reconciliation) +_compact_ack_links_for_claimreconciliation.py(4 routes: unmatched, batch-diff, match, unmatch)remittances.py(4 routes: list, summary, stream, get)dashboard.py(1 route: kpis)providers.py(3 routes: providers, config/providers, config/providers/{npi})activity.py(2 routes: list, stream)eligibility.py(2 routes: request, parse-271)clearhouse.py(3 routes: get, patch, submit)config.py(2 routes: config/payers, config/payers/{id}/configs)payers.py(1 route: {id}/summary)admin.py(existing, absorb 20 routes: audit-log ×2, db/rotate-key, backup ×10, scheduler ×6, reload-config)acks.py(existing, absorb 2 routes: 277ca-acks list/get)
- Move the 12 cross-router helper functions to
api_routers/_shared.py(private to the package, leading underscore). - Reduce
api.pyto a thin shell:app = FastAPI(...),lifespan, both@app.exception_handlers, and theinclude_routerloop driven by arouters: list[APIRouter]exported fromapi_routers/__init__.py. - Keep
_ndjson_lineand_tail_eventsinapi_helpers.py(already correct home; not duplicated). - Live-test after each router is extracted:
curla representative route, assert the expected status code, then run a backendpytestbaseline diff to confirm zero regressions. - Autoreview after each router is extracted: spawn a
pr-reviewersubagent on the staged diff before commit; the reviewer checks (a) route registration, (b) no orphan imports, (c) no leaked business logic, (d) no leaked auth-bypass. - Commit per-router with
feat(sp36): extract {router-name} routerprefix per the SP-N convention. - Track progress in
/tmp/refactor-cyclone.mdper the user's standing directive: each step logged with pre/post pytest counts, live-test result, and reviewer verdict. - Single atomic merge commit into
mainonce every router is extracted, the full backend test suite matches the Step-0 baseline, the full frontend test suite is green, and a one-route-per-prefix live matrix returns 2xx.
Out of scope:
- No new endpoints, no behavior changes, no status code changes, no response shape changes, no header changes. Refactor is byte-for-byte transparent to clients.
- No change to the auth boundary. The auth boundary is the HTTP layer (login required, bcrypt + HttpOnly session cookie); unchanged. The new routers mount under the same
matrix_gatedependency, same as today. - No change to the store facade,
db.py, anystore/module, the pubsub event bus, the parsers, the serializers, the CLI, the audit log, or the migration set. - No change to
api_helpers.pybeyond ensuring it's still importable from its existing path. (Routers that need_ndjson_line/_tail_eventscontinue to import fromcyclone.api_helpers.) - No splitting of
admin.pyintoadmin_audit.py/admin_db.py/admin_backup.py/admin_scheduler.py. That's a separate concern (~1,200 LOC of admin endpoints, but still one domain) and is filed as a possible follow-up SP. - No renaming of any endpoint. No path normalization.
- No frontend changes. No changes to
src/,vite.config.ts,package.json, or any UI asset. - No documentation changes other than the spec + plan files for SP36 itself. The cyclone-skills (
.superpowers/skills/cyclone-api-router/SKILL.md) will be re-read at the start of the next increment that adds an endpoint; the skills don't reference the specific module layout and don't need edits. - No new tests. The existing test suite (
backend/tests/test_api_*.py, siblingsrc/**/*.test.tsx) is the regression net. Pytest baseline (passed / failed-pre-existing / skipped counts) captured before, asserted equal after.
2. Decisions (locked during brainstorming)
D1. Routers mirror the store's domain boundaries, not URL prefixes alone
The store split (SP21) landed cyclone/store/{batches, claim_detail, acks, inbox, providers, kpis, ...}. The new routers should mirror that. A reader who knows the store should be able to guess where a route lives. Three of the routers that don't have a 1-to-1 store counterpart (reconciliation.py, eligibility.py, dashboard.py) still get their own files because they are independent domains at the HTTP surface; their handlers call into a mix of store methods + helper functions.
D2. admin.py stays one router even at ~1,200 LOC
The admin surface has 20 routes across 4 sub-domains (audit log, db key rotation, backups, scheduler) and a natural 4-way split would be cleaner. We keep it as one router for SP36 because (a) the routes all share the matrix_gate admin-only dep, (b) the SP's goal is per-domain router extraction, and (c) splitting admin recursively would balloon the diff. The follow-up that does the admin sub-split is filed as a separate concern.
D3. api_routers/__init__.py is the registration point
api.py does:
from cyclone.api_routers import routers
for r in routers:
app.include_router(r)
Cleaner than 19 individual include_router calls, easier to grep ("where is route X registered?"), and gives us a single line to flip for future feature flags. The auth router import (from cyclone.auth.routes import router as auth_router) keeps its explicit include in api.py because it's not under api_routers/.
D4. _shared.py is the home for cross-router helpers only
A helper moves to _shared.py if and only if at least two routers use it. Single-router helpers (_compact_ack_links_for_claim for claims.py, _batch_summary_* for batches.py) stay private to the router that uses them. The 12 cross-router helpers listed in Section 3.3 are the full _shared.py surface. If a future router needs a helper that's currently in a single-router file, that helper is promoted to _shared.py as part of that future change.
D5. No business logic in route handlers, but no logic change either
Route handlers before SP36 validate input, call the store, return the result. Route handlers after SP36 do the same thing in the same order. We do not refactor handlers as we move them — no extraction of common patterns, no introduction of dependencies-injection helpers, no factoring of duplicated try/except. The SP is a structural move, not a code-quality pass. Any "this looks like it could be cleaner" observations go in /tmp/refactor-cyclone.md as candidates for a future SP, not into this one.
D6. Live-test + autoreview + commit happens per router, not at the end
Each router extraction is one feat(sp36): extract {name} router commit. The cycle is: pytest baseline → cut → pytest diff → curl the moved routes → autoreview → commit → update /tmp/refactor-cyclone.md. This means a router can be reverted individually if a regression slips through, the diff for review is small and focused, and the live system stays green throughout. The single atomic merge at the end folds all 19 router commits (5 existing routers + 14 new) into main.
D7. The merge of SP35 into main happens before the SP36 branch is cut
SP35 (sp35-parse-input-guards) is sitting on a clean working tree with all four commits reviewable. SP36 branches from main, so SP35 must land first. This is a hard pre-condition: if SP35 isn't merged before the SP36 branch is cut, the SP36 branch will carry SP35's commits under it and the audit trail breaks.
D8. The one-shot verification commands in Section 5.3 are the merge gate
A short battery of grep / wc / curl commands runs at the very end, before the merge commit. Any failure of these gates the merge until resolved. The commands check: api.py is thin, no router is over 1,400 LOC, no session.add / s.add / session.commit call leaks into a router file, every route decorator is registered, and a one-route-per-prefix live matrix returns 2xx.
3. Architecture
3.1 Target file layout
backend/src/cyclone/
├── api.py ← thin shell: app, lifespan, exception handlers, include_router loop (~250 LOC)
├── api_helpers.py ← cross-cutting helpers (NDJSON, tail-events) — UNCHANGED
└── api_routers/
├── __init__.py ← exports `routers: list[APIRouter]`; auth-router stays in api.py
├── _shared.py ← 12 cross-router helpers (private to the package)
├── parse.py ← 5 routes (~800 LOC)
├── inbox.py ← 6 routes (~470 LOC)
├── batches.py ← 3 routes + _batch_summary_* helpers (~370 LOC)
├── claims.py ← 5 routes + _compact_ack_links_for_claim (~720 LOC)
├── reconciliation.py ← 4 routes (~115 LOC)
├── remittances.py ← 4 routes (~140 LOC)
├── dashboard.py ← 1 route (~30 LOC)
├── providers.py ← 3 routes (~250 LOC)
├── activity.py ← 2 routes (~150 LOC)
├── eligibility.py ← 2 routes (~85 LOC)
├── clearhouse.py ← 3 routes (~250 LOC)
├── config.py ← 2 routes (~100 LOC)
├── payers.py ← 1 route (~85 LOC)
├── admin.py ← existing, absorbs 16 routes (~1,200 LOC total)
├── acks.py ← existing, absorbs 2 277ca-acks routes
├── claim_acks.py ← existing, unchanged
├── ta1_acks.py ← existing, unchanged
└── health.py ← existing, unchanged
3.2 Endpoints per router (authoritative)
| Router | Method | URL | Source line in api.py |
|---|---|---|---|
| parse | POST | /api/parse-837 |
403 |
| parse | POST | /api/parse-835 |
650 |
| parse | POST | /api/parse-999 |
822 |
| parse | POST | /api/parse-ta1 |
986 |
| parse | POST | /api/parse-277ca |
1124 |
| acks (absorbed) | GET | /api/277ca-acks |
1281 |
| acks (absorbed) | GET | /api/277ca-acks/{ack_id} |
1313 |
| inbox | GET | /api/inbox/lanes |
1355 |
| inbox | POST | /api/inbox/candidates/{remit_id}/match |
1382 |
| inbox | POST | /api/inbox/candidates/dismiss |
1411 |
| inbox | POST | /api/inbox/payer-rejected/acknowledge |
1446 |
| inbox | POST | /api/inbox/rejected/resubmit |
1506 |
| inbox | GET | /api/inbox/export.csv |
1809 |
| batches | POST | /api/batches/{batch_id}/export-837 |
1600 |
| batches | GET | /api/batches |
2034 |
| batches | GET | /api/batches/{batch_id} |
2092 |
| claims | GET | /api/claims |
2103 |
| claims | GET | /api/claims/stream |
2152 |
| claims | GET | /api/claims/{claim_id} |
2199 |
| claims | GET | /api/claims/{claim_id}/serialize-837 |
2262 |
| claims | GET | /api/claims/{claim_id}/line-reconciliation |
2314 |
| reconciliation | GET | /api/reconciliation/unmatched |
2511 |
| reconciliation | GET | /api/batch-diff |
2528 |
| reconciliation | POST | /api/reconciliation/match |
2575 |
| reconciliation | POST | /api/reconciliation/unmatch |
2620 |
| remittances | GET | /api/remittances |
2652 |
| remittances | GET | /api/remittances/summary |
2693 |
| remittances | GET | /api/remittances/stream |
2724 |
| remittances | GET | /api/remittances/{remittance_id} |
2763 |
| dashboard | GET | /api/dashboard/kpis |
2780 |
| providers | GET | /api/providers |
2808 |
| providers | GET | /api/config/providers |
3361 |
| providers | GET | /api/config/providers/{npi} |
4100 |
| activity | GET | /api/activity |
2838 |
| activity | GET | /api/activity/stream |
2865 |
| eligibility | POST | /api/eligibility/request |
3013 |
| eligibility | POST | /api/eligibility/parse-271 |
3039 |
| clearhouse | GET | /api/clearhouse |
3099 |
| clearhouse | PATCH | /api/clearhouse |
3108 |
| clearhouse | POST | /api/clearhouse/submit |
3174 |
| config | GET | /api/config/payers |
4174 |
| config | GET | /api/config/payers/{payer_id}/configs |
4179 |
| payers | GET | /api/payers/{payer_id}/summary |
4230 |
| admin | GET | /api/admin/audit-log |
3372 |
| admin | GET | /api/admin/audit-log/verify |
3414 |
| admin | POST | /api/admin/db/rotate-key |
3454 |
| admin | POST | /api/admin/backup/create |
3614 |
| admin | GET | /api/admin/backup/list |
3668 |
| admin | GET | /api/admin/backup/status |
3697 |
| admin | POST | /api/admin/backup/{backup_id}/verify |
3711 |
| admin | POST | /api/admin/backup/{backup_id}/restore/initiate |
3730 |
| admin | POST | /api/admin/backup/{backup_id}/restore/confirm |
3758 |
| admin | POST | /api/admin/backup/prune |
3810 |
| admin | POST | /api/admin/backup/scheduler/start |
3838 |
| admin | POST | /api/admin/backup/scheduler/stop |
3849 |
| admin | POST | /api/admin/backup/scheduler/tick |
3860 |
| admin | POST | /api/admin/scheduler/start |
3894 |
| admin | POST | /api/admin/scheduler/stop |
3902 |
| admin | POST | /api/admin/scheduler/tick |
3910 |
| admin | POST | /api/admin/scheduler/pull-inbound |
3923 |
| admin | GET | /api/admin/scheduler/status |
4045 |
| admin | GET | /api/admin/scheduler/processed-files |
4052 |
| admin | POST | /api/admin/reload-config |
4316 |
3.3 _shared.py surface (12 helpers)
| Helper | Routers that use it | Source line in api.py |
|---|---|---|
_actor_user_id(request) |
most | 54 |
_resolve_payer(name) |
parse (837, 999, ta1, 277ca) | 333 |
_resolve_payer_835(name) |
parse (835) | 345 |
_transaction_set_id_from_segments(segments) |
parse (999, ta1) | 357 |
_build_and_persist_ack(batch_id) |
parse (after 837) | 573 |
_reconciliation_summary_for_batch(batch_id) |
parse (after 837) | 615 |
_ta1_synthetic_source_batch_id(icn) |
parse (ta1) | 975 |
_serialize_ta1(result) |
acks | 1324 |
_serialize_ta1_from_row(row) |
acks | 1341 |
_batch_summary_claim_count(rec) |
batches | 1858 |
_batch_summary_claim_ids(rec) |
batches | 1867 |
_batch_summary_billing_outcomes(...) |
batches | 1912 |
3.4 Stays in api.py
app = FastAPI(...)instantiationlifespan(init_db, scheduler, bus, sessions, etc.)@app.exception_handler(HTTPException)at 313@app.exception_handler(Exception)at 386- The trailing
from cyclone.auth.routes import router as auth_router; app.include_router(auth_router)block - The
__all__ = ["app"]line at the end
3.5 Stays in api_helpers.py
_ndjson_line_tail_events
3.6 External import surface — what callers see
A grep over the codebase for from cyclone.api import and import cyclone.api will return the same set of symbols after SP36 lands as before:
from cyclone.api import app— used bypython -m cyclone serve, by tests (from cyclone import apithenapp.state.*), by the testconftest.py.app.state.event_bus,app.state.db,app.state.scheduler— read by tests and byapi_routers/(viaRequest).
The new cyclone.api_routers package exports routers: list[APIRouter]. Anything in api_routers/ that needs to be imported from outside (e.g. a router-level fixture in a future test) is reachable as from cyclone.api_routers.parse import router, etc.
4. Data flow & error handling
Data flow (per request):
- FastAPI receives the request, runs the
matrix_gatedependency (auth + role check), then dispatches to the matching route in the matching router. - The route handler validates inputs (path params, query params, body via Pydantic if a body model exists).
- The handler calls
cyclone.store.<method>(...)for write paths and reconciliation, or opens adb.SessionLocal()() for one-off reads(existing pattern; this is how the dashboard KPIs, the activity feed, the configuration endpoints, and the live-tail snapshot reads work today). - The result is serialized via
to_ui_<entity>(fromcyclone.store.ui) for entity-shaped responses, or via a small local serializer for non-entity shapes (e.g. the dashboard KPI shape, the reconciliation summary, the eligibility 271 response). - The handler returns
JSONResponse,StreamingResponse(NDJSON), orResponse(CSV for/api/inbox/export.csv). - Streaming endpoints (
/api/claims/stream,/api/remittances/stream,/api/activity/stream) keep the snapshot-then-live-subscription two-phase pattern._ndjson_lineand_tail_eventscome fromapi_helpers.py; the matching router imports them. - The event bus contract is unchanged. Every write through
cyclone.store.add(...)still publishesclaim_written/remittance_written/activity_recordedon the in-processEventBus. The stream-endpoint routers subscribe to those exact event names. The SP doesn't introduce new event kinds, doesn't rename existing kinds, doesn't change payloads.
Error handling:
- Routers raise
HTTPExceptionfor client errors. Same status codes, samedetailstrings, sameerrorenvelope ({error, detail}). No change. - The two global exception handlers stay in
api.py(D2 of brainstorming):@app.exception_handler(HTTPException)re-emits with the cyclone error envelope,@app.exception_handler(Exception)logs and returns 500. - Streaming endpoints catch domain exceptions and yield
{"type": "error", "data": {"message": ...}}NDJSON chunks instead of raising — existing pattern, preserved per router. StoreNotFound/StoreConflict/StoreInvalidStateare translated to 404 / 409 / 409 by the existing translation path. No change.- The auth path (
matrix_gatedep) returns 401/403 unchanged.
No behavior change anywhere in the data flow or error path. The SP is a structural move, not a correctness pass.
5. Testing approach
5.1 Per-router cycle (live-test + autoreview + commit)
For each router extracted (one feat(sp36): extract {name} router commit):
- Pre-flight pytest baseline. From the project root:
Capture the
cd backend && .venv/bin/pytest --tb=line -q 2>&1 | tee /tmp/refactor-{N}-pre.txt | tail -3XXX passed, YY failed, ZZ skippedline. Expect roughly~1,176 passed, ~1 failed (pre-existing isolation flake), ~10 skippedper the SP21 store-split spec baseline. - Cut. Move the route handler(s) + their single-router helpers to the new router module, register the router in
api_routers/__init__.py, delete the moved code fromapi.py, update imports. No logic change. - Post-cut pytest diff. Same command as (1), output to
/tmp/refactor-{N}-post.txt. Diff:Required: zero diff. Any delta = revert the cut and investigate.diff <(awk '/passed|failed|skipped/ {print}' /tmp/refactor-{N}-pre.txt) \ <(awk '/passed|failed|skipped/ {print}' /tmp/refactor-{N}-post.txt) - Live test. Server is running in
cyclone-backend-1on the host. Hit one representative route from the moved router. Required status codes:- GET endpoints → 200 (or the documented non-200, e.g. 401 if auth is mocked off in the test environment)
- POST endpoints → 200, 201, 400, 401, 403, 409 (whatever the existing happy path / known-error path is)
- Streaming endpoints → 200 with
Content-Type: application/x-ndjsonand a valid firstsnapshot_endchunk
- Autoreview. Spawn a
pr-reviewersubagent on the staged diff. The reviewer prompt enumerates: (a) the new router is registered inapi_routers/__init__.py; (b) no orphan imports left inapi.py; (c) nosession.add/s.add/session.commit/ directdb.SessionLocal()() call that does a writeappears in any router file (reads are OK); (d) the router'sdependencies=[Depends(matrix_gate)]matches the original; (e) docstrings are preserved. Reviewer returns a one-line verdict (PASS / FAIL: ). On FAIL, fix and re-review. - Commit.
feat(sp36): extract {name} routerper the SP-N convention. - Log to
/tmp/refactor-cyclone.md:[step N] extracted {name} pre: passed={X} failed={Y} skipped={Z} post: passed={X} failed={Y} skipped={Z} live: GET /api/{route} → {code} | POST /api/{route} → {code} reviewer: PASS | FAIL: <reason>
5.2 Pre-merge integration test
After all 19 routers are extracted, the full suite runs:
cd backend && .venv/bin/pytest --tb=line -q— full backend suite, must match Step 0 baseline to the digitcd backend && .venv/bin/pytest tests/test_api_*.py -v— every API integration test, all greencd .. && npm test— frontend suite, all greencd .. && npm run typecheck— frontend typecheck cleancd .. && npm run build— frontend build greencd .. && npm run lint— frontend lint clean
5.3 Pre-merge one-shot verification (D8 of brainstorming)
These commands run, and all must pass, before the merge commit is created:
wc -l backend/src/cyclone/api.py— expect ≤ 300wc -l backend/src/cyclone/api_routers/*.py | sort -n— expect no router over 1,400 LOC (admin is the largest at ~1,200)grep -rn "session.add\|s\.add(\|session\.commit" backend/src/cyclone/api_routers/— expect zero hits (writes go throughcyclone.storeonly)grep -rn "from cyclone.api_routers" backend/src/cyclone/api.py— expect one hit (theroutersimport in__init__block)python -c "from cyclone.api_routers import routers; print(len(routers))"— expect ≥ 18 (5 existing + 14 new, minus whatever routers ended up merged during the SP)- Live matrix — for every URL prefix in Section 3.2,
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.0.49:8080/api/{prefix}/...returns 2xx (or the documented error code) git grep -n "@app\." backend/src/cyclone/api.py— expect zero hits (every route decorator now lives in a router)
5.4 No new tests
The existing test suite is the regression net. We do not add new tests, do not modify existing tests, do not add new fixtures. The pre/post pytest baseline diff is the test for the SP.
6. Threat model (post-SP24 alignment)
The auth boundary is the HTTP layer: login required, bcrypt + HttpOnly session cookie. The new routers mount under matrix_gate (same as today), so every moved route stays auth-gated. No route is exposed that wasn't exposed before. No auth check is weakened.
The file-system threat model is unchanged: SQLCipher at rest (when the macOS Keychain entry + sqlcipher3 are both present, otherwise plain SQLite), secrets in the macOS Keychain via keyring, no secrets on disk in plaintext. SP36 doesn't touch the DB layer, the secrets module, or the configuration loader.
LAN-only by design. The auth boundary is the network boundary; the production deployment is the host firewall + compose port publishing. Don't expose the published ports to the WAN — unchanged.
7. Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| A route handler silently changes behavior during the cut (typo, off-by-one, wrong import) | medium | high | Pre/post pytest diff is the test. The diff must be zero; any delta reverts. |
| A helper gets duplicated instead of moved (one copy in old location, one in new) | low | medium | Autoreview explicitly checks for orphan imports in api.py and for def _helper_name definitions appearing twice. |
The api_routers/__init__.py registration misses a router and a route 404s in production |
low | high | Pre-merge one-shot verification §5.3 includes a live matrix that hits every URL prefix. |
The matrix_gate dep is dropped from one route during the cut |
low | critical | Autoreview explicitly checks dependencies=[Depends(matrix_gate)] matches the original on every moved route. |
| Two routers end up importing each other (creates a cycle) | low | medium | The convention is "routers import only cyclone.store, cyclone.api_helpers, cyclone.api_routers._shared" — autoreview checks for any other cross-router import. |
A from cyclone.api import somewhere in the codebase breaks because we removed a top-level symbol |
low | high | The external import surface (D6 of brainstorming) is the test: git grep "from cyclone.api import" before and after must be identical in symbol set. |
| The merge of SP35 into main fails or introduces conflicts | low | high | D7 of brainstorming: SP35 merge is a hard pre-condition; if it doesn't land cleanly, SP36 doesn't start. |
| A streaming endpoint regresses silently (returns the right status code but the wrong first chunk) | medium | medium | The live test for streaming endpoints asserts Content-Type: application/x-ndjson and parses the first snapshot_end line, not just the HTTP code. |
| The frontend test suite picks up a backend change (e.g. a renamed JSON field that the UI consumes) | very low | medium | The refactor is byte-for-byte transparent to clients; if a frontend test fails, that's a real regression and we investigate. |
8. Rollout
- Merge SP35 into main. Fast-forward or merge commit (no squash, no rebase). The branch
sp35-parse-input-guardshas 4 reviewable commits and a clean working tree. - Restart compose if needed.
docker compose restart cyclone-backend-1after the SP35 merge so the running container reflects main. - Create the SP36 branch.
git checkout -b sp36-api-routers-splitfrom main. - Extract routers, one per commit. Order: lowest-risk first (admin, health, claim_acks, ta1_acks already exist; then the small leaf routers
dashboard.py,eligibility.py,payers.py,config.py; then the medium onesreconciliation.py,remittances.py,activity.py,clearhouse.py,providers.py; then the larger onesinbox.py,batches.py,claims.py; finallyparse.pysince it's the most coupled to the store facade). Each step follows §5.1. - Run the §5.2 integration tests.
- Run the §5.3 one-shot verification.
- Open the PR. Title:
SP36 API Routers Split. Body: links to this spec, the SP36 plan (when written), and a checklist of every router extracted. - Merge after review. Single atomic
merge: SP36 api-routers-split into maincommit, no squash, no rebase.
9. Open questions
None at spec time. The brainstorming Q&A resolved all of:
- Which file to split?
api.py. Confirmed. - What shape? Per-resource routers mirroring the store's domain boundaries. Confirmed.
- Branch from where? Merge SP35 first, then from main. Confirmed.
- Where do the cross-router helpers go?
api_routers/_shared.py(private). Confirmed. admin.py— one router or split further? One router for this SP, follow-up later. Confirmed.- How granular are the commits? One router per commit. Confirmed.