10 Commits

14 changed files with 657 additions and 250 deletions
+35 -23
View File
@@ -5,23 +5,27 @@ description: "Cyclone FastAPI router conventions (api_routers/, api_helpers.py,
# cyclone-api-router # cyclone-api-router
Cyclone splits its FastAPI surface two ways: small resource-group Cyclone splits its FastAPI surface two ways: resource-group routers
routers live in `backend/src/cyclone/api_routers/<topic>.py` and are live in `backend/src/cyclone/api_routers/<topic>.py` and are mounted
mounted bare into `api.py`; the high-traffic streaming and parse bare into `api.py`; the parse endpoints (`/api/parse-837`,
endpoints still live as top-level decorators in `api.py` itself. This `/api/parse-835`, plus the SP40 `/api/admin/validate-837` etc.) stay
skill codifies the conventions so additions stay consistent with the as top-level decorators in `api.py` because they span multiple store
four routers already shipped (`acks`, `admin`, `health`, `ta1_acks`). modules. This skill codifies the conventions so additions stay
consistent with the routers already shipped.
As of this writing: **4 router modules** under As of this writing: **22 router modules** under
`backend/src/cyclone/api_routers/`, **one shared helpers module** at `backend/src/cyclone/api_routers/` (`acks`, `activity`, `admin`,
`backend/src/cyclone/api_helpers.py` (248 lines, NDJSON primitives + `batches`, `claim_acks`, `claims`, `clearhouse`, `config`,
content negotiation + `tail_events`), and ~30 routes still inlined `dashboard`, `eligibility`, `health`, `inbox`, `parse`, `payers`,
in `backend/src/cyclone/api.py`. The next refactor target is the `providers`, `rebill`, `reconciliation`, `remittances`, `submission`,
parse endpoints. `ta1_acks`, plus a `_shared.py` for cross-router helpers), **one
shared helpers module** at `backend/src/cyclone/api_helpers.py`
(NDJSON primitives + content negotiation + `tail_events`), and
`api.py` itself is now 378 LOC (down from ~3,145 pre-SP36).
## Auth gate (SP24) ## Auth gate (SP23)
Every router declared in `backend/src/cyclone/api_routers/` **must** carry `dependencies=[Depends(matrix_gate)]` at the `APIRouter(...)` declaration — not on each individual endpoint. The gate lives at `backend/src/cyclone/auth/deps.py:107` and the role matrix is at `backend/src/cyclone/auth/permissions.py`. The roles are `admin / user / viewer`; `matrix_gate` returns 401 when there's no session and 403 when the role is below the endpoint's required role. When `AUTH_DISABLED` is True (conftest autouse fixture flips it; `CYCLONE_AUTH_DISABLED=1` in prod-by-mistake), the gate short-circuits to a synthetic admin — see the SP24 spec for the threat-model implications. New routers get the gate by default; the auth-aware convention is `router = APIRouter(dependencies=[Depends(matrix_gate)])`. Every router declared in `backend/src/cyclone/api_routers/` **must** carry `dependencies=[Depends(matrix_gate)]` at the `APIRouter(...)` declaration — not on each individual endpoint. The gate lives at `backend/src/cyclone/auth/deps.py:107` and the role matrix is at `backend/src/cyclone/auth/permissions.py`. The roles are `admin / user / viewer`; `matrix_gate` returns 401 when there's no session and 403 when the role is below the endpoint's required role. When `AUTH_DISABLED` is True (conftest autouse fixture flips it; `CYCLONE_AUTH_DISABLED=1` in prod-by-mistake), the gate short-circuits to a synthetic admin — see the SP23 spec for the threat-model implications. New routers get the gate by default; the auth-aware convention is `router = APIRouter(dependencies=[Depends(matrix_gate)])`.
## When to use ## When to use
@@ -45,11 +49,13 @@ Every router declared in `backend/src/cyclone/api_routers/` **must** carry `depe
1. **No new top-level routes in `api.py` for resource groups.** Any 1. **No new top-level routes in `api.py` for resource groups.** Any
endpoint grouped under a resource (`/api/<resource>` and its endpoint grouped under a resource (`/api/<resource>` and its
`/{id}` detail) lives in `backend/src/cyclone/api_routers/<topic>.py` `/{id}` detail) lives in `backend/src/cyclone/api_routers/<topic>.py`
as an `APIRouter`. The streaming list endpoints (`/api/claims/stream`, as an `APIRouter`. The streaming list endpoints
`/api/remittances/stream`, `/api/activity/stream`) and the parse (`/api/claims/stream`, `/api/remittances/stream`,
endpoints (`/api/parse-*`) currently stay in `api.py` because they `/api/activity/stream`, `/api/acks/stream`, `/api/ta1-acks/stream`)
span multiple store modules — don't move them unless you're also were split into their per-resource routers as part of SP36; only
restructuring the store split. the cross-resource parse endpoints (`/api/parse-*` and the SP40
`validate-837`) stay in `api.py` because they span multiple store
modules.
2. **Reuse `api_helpers.py`.** NDJSON primitives (`ndjson_line`, 2. **Reuse `api_helpers.py`.** NDJSON primitives (`ndjson_line`,
`ndjson_stream_list`, `ndjson_stream_837`, `ndjson_stream_835`), `ndjson_stream_list`, `ndjson_stream_837`, `ndjson_stream_835`),
content negotiation (`client_wants_json`, `wants_ndjson`), the content negotiation (`client_wants_json`, `wants_ndjson`), the
@@ -80,8 +86,9 @@ Every router declared in `backend/src/cyclone/api_routers/` **must** carry `depe
`StreamingResponse(media_type="application/x-ndjson")` and feed it `StreamingResponse(media_type="application/x-ndjson")` and feed it
either `ndjson_stream_list(items, total, returned, has_more)` (for either `ndjson_stream_list(items, total, returned, has_more)` (for
list pages) or `tail_events(request, bus, kinds)` (for live-tail list pages) or `tail_events(request, bus, kinds)` (for live-tail
pages). See `acks.py:62-66` for the list-stream skeleton and pages). See `api_routers/acks.py:62-66` for the list-stream
`api.py:1357-1401` for the full live-tail pattern (snapshot → skeleton and `api_routers/claims.py` (the `/api/claims/stream`
handler) for the full live-tail pattern (snapshot →
`snapshot_end` → subscription → heartbeats). See `cyclone-tail` `snapshot_end` → subscription → heartbeats). See `cyclone-tail`
for the wire format. for the wire format.
6. **Tests.** Every new endpoint gets a `test_api_<topic>_<verb>.py` 6. **Tests.** Every new endpoint gets a `test_api_<topic>_<verb>.py`
@@ -170,12 +177,17 @@ just after middleware registration and just before the first
# Resource-group routers. Each module owns its own APIRouter and is # Resource-group routers. Each module owns its own APIRouter and is
# registered below. New resources go in `cyclone.api_routers.<name>` # registered below. New resources go in `cyclone.api_routers.<name>`
# and are wired in here. # and are wired in here.
from cyclone.api_routers import acks, admin, health, ta1_acks # noqa: E402 from cyclone.api_routers import ( # noqa: E402
acks, activity, admin, batches, claim_acks, claims, clearhouse,
config, dashboard, eligibility, health, inbox, parse, payers,
providers, rebill, reconciliation, remittances, submission, ta1_acks,
)
app.include_router(health.router) app.include_router(health.router)
app.include_router(acks.router) app.include_router(acks.router)
app.include_router(ta1_acks.router) app.include_router(activity.router)
app.include_router(admin.router) app.include_router(admin.router)
# ... 18 more include_router calls in api.py ...
``` ```
## Anti-patterns ## Anti-patterns
+11 -1
View File
@@ -5,7 +5,17 @@ description: "Cyclone CLI subcommand conventions (cli.py — Click group + subco
# cyclone-cli # cyclone-cli
The operator-facing CLI is a **Click** group at `cli.py:45` (`@click.group()` for `main`), mounted as the `cyclone` console script in `pyproject.toml:54` (`cyclone = "cyclone.cli:main"`). Seven subcommands ship today: `parse-837`, `parse-835`, `validate-npi`, `validate-tax-id`, plus the `backup` group (`init-passphrase`, `create`, `list`, `verify`, `restore`, `prune`, `status`). `serve` lives separately in `__main__.py:19` (dispatches `uvicorn cyclone.api:app`). The operator-facing CLI is a **Click** group at `cli.py:45` (`@click.group()` for `main`), mounted as the `cyclone` console script in `pyproject.toml:54` (`cyclone = "cyclone.cli:main"`). The CLI has grown well past the original seven subcommands; the full inventory is:
- **Parser / validator commands**: `parse-837`, `parse-835`, `validate-npi`, `validate-tax-id`, `validate-837` (SP40, Edifabric fail-closed pre-upload gate).
- **DB plumbing**: `seed` (deterministic sample claims), `backfill-rendering-npi`, `backfill-999-rejections` (SP33).
- **Submission flow**: `submit-batch` (SP37, canonical parse → DB-write → SFTP-upload), `resubmit-rejected-claims` (SP33), `recover-ingest` (re-parse a local path), `pull-inbound` (scheduler trigger).
- **Rebill + reissue**: `rebill-from-835` (SP41), `reissue-claims` (SP24).
- **Ack orphans + resubmissions**: `ack-orphans status / reconcile`, `resubmissions status`.
- **User management** (`users` group): `create`, `list`, `disable`, `reset-password`, `set-role`.
- **Backup group** (`backup`): `init-passphrase`, `create`, `list`, `verify`, `restore`, `prune`, `status`.
`serve` lives separately in `__main__.py:19` (dispatches `uvicorn cyclone.api:app`).
## When to use ## When to use
+1 -1
View File
@@ -7,7 +7,7 @@ description: "Cyclone EDI parser/validator conventions (837P/835/999/270/271/277
Cyclone parses seven X12 EDI transaction types (837P, 835, 999, 270, 271, 277CA, TA1) into typed Pydantic models, then runs per-claim / per-batch validator rules that surface as R-coded `ValidationIssue` records. This skill codifies the conventions so new parsers and rules stay consistent with the seven that already exist. Cyclone parses seven X12 EDI transaction types (837P, 835, 999, 270, 271, 277CA, TA1) into typed Pydantic models, then runs per-claim / per-batch validator rules that surface as R-coded `ValidationIssue` records. This skill codifies the conventions so new parsers and rules stay consistent with the seven that already exist.
As of this writing: **7 parser modules** under `backend/src/cyclone/parsers/parse_<edi>.py`, **~25 per-claim rules** numbered `R010``R100` and `R200``R210` in `validator.py`, plus a parallel set of **835-specific rules** prefixed `R835_*` in `validator_835.py`. The next increment is **SP22**. As of this writing: **7 parser modules** under `backend/src/cyclone/parsers/parse_<edi>.py` (837P, 835, 999, TA1, 270, 271, 277CA), **~25 per-claim rules** numbered `R010``R100` and `R200``R210` in `validator.py`, plus a parallel set of **835-specific rules** prefixed `R835_*` in `validator_835.py`. The most recently shipped increment touching this surface is **SP41** (in-window rebill pipeline); the next free increment after this SP42 doc-pass is **SP43**.
## When to use ## When to use
@@ -5,9 +5,9 @@ description: "Cyclone React page conventions (TanStack Query, use<X> hook, drawe
# cyclone-frontend-page # cyclone-frontend-page
Cyclone pages live at `src/pages/<Name>.tsx`: a `use<X>` hook in `src/hooks/use<X>.ts` does the fetching (and optionally the live-tail subscription), `<Layout>` + `<PageHeader>` + `<Sidebar>` (`src/components/`) provide the app shell, and any right-side detail (claim, remittance) lives in `src/components/<DrawerName>/` whose open/close state is mirrored to the URL via `useDrawerUrlState`. This skill codifies the conventions so additions stay consistent with the eleven pages already shipped. Cyclone pages live at `src/pages/<Name>.tsx`: a `use<X>` hook in `src/hooks/use<X>.ts` does the fetching (and optionally the live-tail subscription), `<Layout>` + `<PageHeader>` + `<Sidebar>` (`src/components/`) provide the app shell, and any right-side detail (claim, remittance) lives in `src/components/<DrawerName>/` whose open/close state is mirrored to the URL via `useDrawerUrlState`. This skill codifies the conventions so additions stay consistent with the twelve pages already shipped (Login added in SP23).
As of this writing: **11 pages** under `src/pages/` (9 of 11 with a `*.test.tsx` sibling — Dashboard, Upload, and BatchDiff are not yet covered; be the first when you refactor them), **~30 hooks** under `src/hooks/`, and **2 drawer modules** at `src/components/{ClaimDrawer,RemitDrawer}/`. The next increment is **SP22**. As of this writing: `**12 pages** under `src/pages/` (10 of 12 with a `*.test.tsx` sibling — Dashboard and BatchDiff are not yet covered; be the first when you refactor them; the Login page does not need a sibling), **~30 hooks** under `src/hooks/`, and **4 drawer modules** at `src/components/{ClaimDrawer,RemitDrawer,ProviderDrawer,AckDrawer}/`. The most recently shipped increment is **SP41** (in-window rebill pipeline); the next free increment after this SP42 doc-pass is **SP43**.
## When to use ## When to use
+3 -7
View File
@@ -10,15 +10,11 @@ a spec, a plan, an implementation branch, and a single atomic merge commit
into `main`. This skill encodes the conventions so every increment follows into `main`. This skill encodes the conventions so every increment follows
the same shape and the commit history stays auditable. the same shape and the commit history stays auditable.
As of this writing: **17 specs** in `docs/superpowers/specs/`, **13 plans** As of this writing: SP numbers used through **SP41** (the in-window rebill pipeline); **SP42** is this doc-pass. **SP23** shipped the Ubuntu + Docker + RBAC + auth LAN-bind product fork; **SP24** is the reissue-claims + auth-docs alignment; SPs **2540** cover the resubmissions, `claim_acks`, rendering/service NPI, transaction-set control numbers, additional live-tail streams, dashboards, and rebill prerequisites shipped between SP24 and SP41. The next free increment after SP42 is **SP43**.
in `docs/superpowers/plans/`, and SP numbers used through **SP22**. **SP23**
is the Ubuntu + Docker + RBAC product fork (awaiting user decision);
**SP24** is the auth-posture alignment (docs-only). The next free increment
is **SP25** after SP24 lands.
## Auth-aware spec template (SP24) ## Auth-aware spec template (post-SP23)
The threat-model section in the canonical SP-N spec template (`## 1. Scope`, second-to-last bullet) used to read "no second party to authenticate; no second host to harden against." **That phrasing is stale as of 2026-06-23**the auth work landed in `main` and every backend endpoint requires login. New specs should instead state the auth boundary explicitly: "the auth boundary is HTTP (login required, bcrypt + HttpOnly session cookie); file-system threats remain the local-only threat model (SQLCipher at rest, macOS Keychain). SP23 changes the threat model to LAN-bound remote operator." Reference: [`docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md`](../../../docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md). The threat-model section in the canonical SP-N spec template (`## 1. Scope`, second-to-last bullet) used to read "no second party to authenticate; no second host to harden against." **That phrasing is stale since SP23 landed (2026-06-23)**every backend endpoint now requires login (`cyclone.auth.*`, `Depends(matrix_gate)` on every `APIRouter`), the React app ships a `Login` page, RBAC roles `admin / user / viewer` gate individual endpoints, and the host still binds `0.0.0.0:8000` (reachability controlled by the host firewall / compose port publishing). The dev/test escape hatch is `CYCLONE_AUTH_DISABLED=1`, which logs a WARNING at boot. New specs should instead state the auth boundary explicitly: "the auth boundary is HTTP (login required, bcrypt + HttpOnly session cookie); file-system threats remain the LAN-only threat model (SQLCipher at rest, macOS Keychain). SP23 expanded the threat model to a remote operator on the LAN." Reference: [`docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md`](../../../docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md).
## When to use ## When to use
+29 -31
View File
@@ -6,17 +6,9 @@ description: "Cyclone store write-paths, the pubsub event contract (claim_writte
# cyclone-store # cyclone-store
Cyclone persists every parsed X12 batch through one facade, Cyclone persists every parsed X12 batch through one facade,
`CycloneStore` (`backend/src/cyclone/store.py:882`). Every write `CycloneStore` (now exposed via `backend/src/cyclone/store/__init__.py` after the SP21 split landed; the public import surface `from cyclone.store import …` is unchanged). Every write inserts the row AND publishes a pubsub event on the in-process `EventBus` (`backend/src/cyclone/pubsub.py:20`) so live-tail pages see new rows the moment they land. The event contract is the seam between persistence and streaming — a wrong event name silently goes stale.
inserts the row AND publishes a pubsub event on the in-process
`EventBus` (`backend/src/cyclone/pubsub.py:20`) so live-tail pages
see new rows the moment they land. The event contract is the seam
between persistence and streaming — a wrong event name silently
goes stale.
As of this writing: the store is a **single 2412-line module** and As of this writing: the store is a **16-module subpackage** under `backend/src/cyclone/store/` (SP21 split landed). Event kinds in use today: `claim_written`, `remittance_written`, `activity_recorded`, plus the SP-acks-tail additions `ack_written` and `ta1_ack_written` for the `/api/acks/stream` and `/api/ta1-acks/stream` endpoints. Next: **SP43** (after the SP42 doc-pass merges).
the SP21 split into `backend/src/cyclone/store/` is in progress.
Three event kinds: `claim_written`, `remittance_written`,
`activity_recorded`. Next: **SP22**.
## When to use ## When to use
@@ -45,12 +37,11 @@ Three event kinds: `claim_written`, `remittance_written`,
2. **Every write publishes an event.** The event name matches the 2. **Every write publishes an event.** The event name matches the
entity: `claim_written`, `remittance_written`, entity: `claim_written`, `remittance_written`,
`activity_recorded` (the trailing `_recorded` signals a `activity_recorded` (the trailing `_recorded` signals a
non-canonical row — activity events are derived, not first-class; non-canonical row — activity events are derived, not first-class),
current names at `store.py:1072,1081,1096`). New entities get plus `ack_written` and `ta1_ack_written` for the SP-acks-tail
`<entity>_written`; activity-style side rows get streams. New entities get `<entity>_written`; activity-style side
`<entity>_recorded`. Publish is **best-effort** failures are rows get `<entity>_recorded`. Publish is **best-effort**
logged but never roll back the persisted batch failures are logged but never roll back the persisted batch.
(`store.py:1097-1098`).
3. **Snapshot shape.** Each entity has a `to_ui_<entity>` serializer 3. **Snapshot shape.** Each entity has a `to_ui_<entity>` serializer
(plain Python function returning a dict) — currently (plain Python function returning a dict) — currently
`to_ui_claim`, `to_ui_remittance`, `to_ui_claim_from_orm`, `to_ui_claim`, `to_ui_remittance`, `to_ui_claim_from_orm`,
@@ -61,13 +52,15 @@ Three event kinds: `claim_written`, `remittance_written`,
that row (`store.py:1052-1054`). that row (`store.py:1052-1054`).
4. **SP21 boundaries.** Post-split, each domain lives in its own 4. **SP21 boundaries.** Post-split, each domain lives in its own
module under `backend/src/cyclone/store/`: `__init__.py` (facade module under `backend/src/cyclone/store/`: `__init__.py` (facade
+ `CycloneStore` class), `exceptions.py`, `records.py`, + `CycloneStore` class), `acks.py`, `backfill.py`, `backups.py`,
`orm_builders.py`, `ui.py`, `write.py`, `batches.py`, `batches.py`, `claim_acks.py`, `claim_detail.py`, `exceptions.py`,
`claim_detail.py`, `acks.py`, `backups.py`, `inbox.py`, `inbox.py`, `kpis.py`, `orm_builders.py`, `providers.py`,
`providers.py`. Cross-module writes go through `CycloneStore` `records.py`, `resubmissions.py`, `submission_dedup.py`, `ui.py`,
facade methods, not direct module access. The facade re-exports `write.py` — 16 modules total. Cross-module writes go through
every name callers currently import from `cyclone.store`. Full `CycloneStore` facade methods, not direct module access. The
list: `docs/superpowers/plans/2026-06-21-cyclone-store-split.md:25-37`. facade re-exports every name callers currently import from
`cyclone.store`. Full split plan:
`docs/superpowers/plans/2026-06-21-cyclone-store-split.md`.
5. **No business logic in route handlers.** A route handler validates 5. **No business logic in route handlers.** A route handler validates
input, calls `store.<method>(...)`, passes the `event_bus`, input, calls `store.<method>(...)`, passes the `event_bus`,
returns the serialized result. Reconciliation, idempotency returns the serialized result. Reconciliation, idempotency
@@ -77,7 +70,7 @@ Three event kinds: `claim_written`, `remittance_written`,
### A `CycloneStore.add` write — publishes events from inserted rows ### A `CycloneStore.add` write — publishes events from inserted rows
Taken from `backend/src/cyclone/store.py:898-1107`. The method opens Taken from `backend/src/cyclone/store/write.py` (post-SP21). The method opens
a session, inserts rows, then runs a sync `_publish_events_sync` a session, inserts rows, then runs a sync `_publish_events_sync`
after commit so subscribers can immediately re-fetch consistent data. after commit so subscribers can immediately re-fetch consistent data.
@@ -118,12 +111,13 @@ FastAPI handlers to await.
### Backend `/api/<resource>/stream` endpoint — subscribes to the event ### Backend `/api/<resource>/stream` endpoint — subscribes to the event
Taken from `backend/src/cyclone/api.py:1380-1401`. Two phases — eager Streaming endpoints live in `backend/src/cyclone/api_routers/<topic>.py`
post-SP36 (e.g. `api_routers/claims.py`). Two phases — eager
snapshot, then live subscription — wrapped in `StreamingResponse` snapshot, then live subscription — wrapped in `StreamingResponse`
with `media_type="application/x-ndjson"`. with `media_type="application/x-ndjson"`.
```python ```python
@app.get("/api/claims/stream") @router.get("/api/claims/stream")
async def claims_stream(request: Request, ...) -> StreamingResponse: async def claims_stream(request: Request, ...) -> StreamingResponse:
bus: EventBus = request.app.state.event_bus bus: EventBus = request.app.state.event_bus
@@ -139,6 +133,10 @@ async def claims_stream(request: Request, ...) -> StreamingResponse:
return StreamingResponse(gen(), media_type="application/x-ndjson") return StreamingResponse(gen(), media_type="application/x-ndjson")
``` ```
The same shape is repeated in `api_routers/remittances.py`,
`api_routers/activity.py`, `api_routers/acks.py`, and
`api_routers/ta1_acks.py` (five streaming endpoints total).
`_ndjson_line` and `_tail_events` live in `_ndjson_line` and `_tail_events` live in
`backend/src/cyclone/api_helpers.py`. The `["remittance_written"]` `backend/src/cyclone/api_helpers.py`. The `["remittance_written"]`
and `["activity_recorded"]` subscriptions are at `api.py:1891,2002`. and `["activity_recorded"]` subscriptions are at `api.py:1891,2002`.
@@ -172,11 +170,11 @@ def test_publishes_claim_written_event(client: TestClient) -> None:
equivalent `get_*` facade method). equivalent `get_*` facade method).
- **Don't introduce a new event name without updating the subscriber - **Don't introduce a new event name without updating the subscriber
list.** Today every `<entity>_written` event has exactly one list.** Today every `<entity>_written` event has exactly one
consumer — the matching `/api/<resource>/stream` endpoint, consumer — the matching `/api/<resource>/stream` endpoint, now
currently in `backend/src/cyclone/api.py` (claims at `:1398`, split across `api_routers/{claims,remittances,activity,acks,ta1_acks}.py`
remittances at `:1891`, activity at `:2002`). Any future split (post-SP36 split). Any new event must wire its subscription in
into `backend/src/cyclone/api_routers/` must wire the same the matching router and add the resource name to the hook
subscription. triplet in the corresponding page.
- **Don't merge a write method with its event publication into - **Don't merge a write method with its event publication into
separate places.** `_publish_events_sync` lives next to `add` in separate places.** `_publish_events_sync` lives next to `add` in
`store.py:1042-1107` so reviewers see both halves of the contract `store.py:1042-1107` so reviewers see both halves of the contract
+21 -20
View File
@@ -5,14 +5,14 @@ description: "Cyclone live-tail streaming wire format and the useTailStream / us
# cyclone-tail # cyclone-tail
Cyclone keeps the Claims, Remittances, and Activity pages live without Cyclone keeps the Claims, Remittances, Activity, Acks, and Ta1Acks pages live without
polling: every store write publishes an internal EventBus event, the polling: every store write publishes an internal EventBus event, the
page opens a `GET /api/<resource>/stream` HTTP/1.1 chunked-NDJSON page opens a `GET /api/<resource>/stream` HTTP/1.1 chunked-NDJSON
connection, and new rows land in the table the moment they hit the connection, and new rows land in the table the moment they hit the
database. This skill codifies the wire format, the hook triplet, and database. This skill codifies the wire format, the hook triplet, and
the backoff/stall machinery so additions stay consistent with the the backoff/stall machinery so additions stay consistent with the
three streaming pages already shipped (`Claims`, `Remittances`, five streaming pages already shipped (`Claims`, `Remittances`,
`ActivityLog`). `ActivityLog`, `Acks`, `Ta1Acks`).
## When to use ## When to use
@@ -42,8 +42,8 @@ three streaming pages already shipped (`Claims`, `Remittances`,
`item_dropped` (`{"id": "..."}` queue-overflow notice), `error` `item_dropped` (`{"id": "..."}` queue-overflow notice), `error`
(`{"message": "..."}` promoted to a thrown error by the hook). (`{"message": "..."}` promoted to a thrown error by the hook).
Defined at `src/lib/tail-stream.ts:22-44`; emitted by Defined at `src/lib/tail-stream.ts:22-44`; emitted by
`backend/src/cyclone/api.py:1357-2006`. See the streaming routers in `backend/src/cyclone/api_routers/{claims,remittances,activity,acks,ta1_acks}.py`
`references/wire-format.md`. (post-SP36 split). See `references/wire-format.md`.
2. **Hook triplet.** Streaming pages compose three pieces: 2. **Hook triplet.** Streaming pages compose three pieces:
`useTailStream(resource)` (`src/hooks/useTailStream.ts:80` — opens `useTailStream(resource)` (`src/hooks/useTailStream.ts:80` — opens
the stream, drives the backoff/stall state machine, dispatches the stream, drives the backoff/stall state machine, dispatches
@@ -52,8 +52,9 @@ three streaming pages already shipped (`Claims`, `Remittances`,
(`src/hooks/useMergedTail.ts:25` — returns (`src/hooks/useMergedTail.ts:25` — returns
`baseItems + tailSlice` dedup'd against `baseItems`), and a `baseItems + tailSlice` dedup'd against `baseItems`), and a
per-resource initial-fetch hook (`useClaims`, `useRemittances`, per-resource initial-fetch hook (`useClaims`, `useRemittances`,
`useActivity`). The page wires all three — see `useActivity`, `useAcks`, `useTa1Acks`). The page wires all three —
`src/pages/Claims.tsx:87-89`. see `src/pages/Claims.tsx:87-89`. The five resources share the
same triplet; only the resource string differs.
3. **Stall threshold.** 30 seconds of total silence — heartbeats 3. **Stall threshold.** 30 seconds of total silence — heartbeats
included — flips status to `stalled` and surfaces the included — flips status to `stalled` and surfaces the
`↻ Reconnect` button on `<TailStatusPill>`. Constant: `↻ Reconnect` button on `<TailStatusPill>`. Constant:
@@ -67,8 +68,8 @@ three streaming pages already shipped (`Claims`, `Remittances`,
`<TailStatusPill>` from `connecting` to `live` and to reset the `<TailStatusPill>` from `connecting` to `live` and to reset the
reconnect backoff counter (`useTailStream.ts:174-180`). reconnect backoff counter (`useTailStream.ts:174-180`).
5. **Content-Type.** Stream endpoints respond with 5. **Content-Type.** Stream endpoints respond with
`media_type="application/x-ndjson"` — see `media_type="application/x-ndjson"` — set in each
`backend/src/cyclone/api.py:1401,1894,2005`. Frontend sets `api_routers/<resource>.py` stream handler. Frontend sets
`Accept: application/x-ndjson` at `src/lib/tail-stream.ts:67-70`. `Accept: application/x-ndjson` at `src/lib/tail-stream.ts:67-70`.
Never `application/json` for a stream endpoint. Never `application/json` for a stream endpoint.
6. **Backoff.** Transient errors retry with `1s → 2s → 4s → 8s → 16s 6. **Backoff.** Transient errors retry with `1s → 2s → 4s → 8s → 16s
@@ -108,7 +109,7 @@ export function ClaimsPage() {
### Backend `/api/foo/stream` endpoint ### Backend `/api/foo/stream` endpoint
Pattern from `backend/src/cyclone/api.py:1357-1401`. Register BEFORE Pattern from `backend/src/cyclone/api_routers/claims.py`. Register BEFORE
`/api/foo/{foo_id}` so the literal `stream` segment doesn't match as `/api/foo/{foo_id}` so the literal `stream` segment doesn't match as
an id. Two phases — eager snapshot, then live subscription — wrapped an id. Two phases — eager snapshot, then live subscription — wrapped
in `StreamingResponse` with `media_type="application/x-ndjson"`. in `StreamingResponse` with `media_type="application/x-ndjson"`.
@@ -160,7 +161,7 @@ takes `status` + `lastEventAt` from `useTailStream` plus
- **Don't change the wire format on one endpoint without updating - **Don't change the wire format on one endpoint without updating
the others.** The parser (`src/lib/tail-stream.ts`) and the hook the others.** The parser (`src/lib/tail-stream.ts`) and the hook
dispatch switch (`useTailStream.ts:173-195`) are shared across all dispatch switch (`useTailStream.ts:173-195`) are shared across all
three live streams. Adding a new event type means updating five live streams. Adding a new event type means updating
`TailEvent`, `KNOWN_TYPES`, the dispatch `case`, the `armStall` `TailEvent`, `KNOWN_TYPES`, the dispatch `case`, the `armStall`
re-arm list, and the backend emitter — in that order. re-arm list, and the backend emitter — in that order.
- **Don't emit `item` events before `snapshot_end`.** The hook uses - **Don't emit `item` events before `snapshot_end`.** The hook uses
@@ -168,7 +169,7 @@ takes `status` + `lastEventAt` from `useTailStream` plus
`connecting` to `live` and to reset the reconnect backoff counter. `connecting` to `live` and to reset the reconnect backoff counter.
Emitting `item`s first lands rows in `useTailStore` but the UI Emitting `item`s first lands rows in `useTailStore` but the UI
still reads "Connecting" — operators see a flash of stale state. still reads "Connecting" — operators see a flash of stale state.
Emit snapshot, then `snapshot_end`, then live (`api.py:1386-1401`). Emit snapshot, then `snapshot_end`, then live.
- **Don't change the 30s stall threshold without updating the README - **Don't change the 30s stall threshold without updating the README
"Status pill" table.** The constant "Status pill" table.** The constant
(`STALL_TIMEOUT_MS = 30_000` at `useTailStream.ts:53`) is the (`STALL_TIMEOUT_MS = 30_000` at `useTailStream.ts:53`) is the
@@ -176,7 +177,7 @@ takes `status` + `lastEventAt` from `useTailStream` plus
edit at `README.md:118`. edit at `README.md:118`.
- **Don't add `useTailStream` calls from `useFoo.ts` hooks.** - **Don't add `useTailStream` calls from `useFoo.ts` hooks.**
Streaming connections belong on the page (`src/pages/Claims.tsx`, Streaming connections belong on the page (`src/pages/Claims.tsx`,
`Remittances.tsx`, `ActivityLog.tsx`). Hoisting into `useFoo` `Remittances.tsx`, `ActivityLog.tsx`, `Acks.tsx`, `Ta1Acks.tsx`). Hoisting into `useFoo`
couples the lifecycle to whoever mounts it and breaks couples the lifecycle to whoever mounts it and breaks
one-resource-one-page ownership. one-resource-one-page ownership.
@@ -185,16 +186,16 @@ takes `status` + `lastEventAt` from `useTailStream` plus
- **`cyclone-frontend-page`** — page components live in `src/pages/`; - **`cyclone-frontend-page`** — page components live in `src/pages/`;
load when adding or refactoring a streaming page to confirm the load when adding or refactoring a streaming page to confirm the
route + `PageHeader` + table conventions. route + `PageHeader` + table conventions.
- **`cyclone-api-router`** — endpoint conventions (`api_routers/` for - **`cyclone-api-router`** — endpoint conventions (`api_routers/`
resource-group routers, `api.py` for the live-tail endpoints); load hosts the streaming endpoints post-SP36, alongside the other
when adding or changing an HTTP endpoint that surfaces streamed or resource-group routers); load when adding or changing an HTTP
paginated data. endpoint that surfaces streamed or paginated data.
- **`cyclone-store`** — write-path conventions in `store.py` and the - **`cyclone-store`** — write-path conventions in `store.py` and the
pubsub event contract (`claim_written`, `remittance_written`, pubsub event contract (`claim_written`, `remittance_written`,
`activity_recorded`); load when adding a new entity whose writes `activity_recorded`); load when adding a new entity whose writes
should fan out to a stream endpoint. should fan out to a stream endpoint.
- **`cyclone-tests`** — frontend `*.test.tsx` siblings cover - **`cyclone-tests`** — frontend `*.test.tsx` siblings cover
`useTailStream`, `useMergedTail`, `TailStatusPill`; backend `useTailStream`, `useMergedTail`, `TailStatusPill`; backend
`test_api_stream_live.py` covers the three live-tail endpoints; `test_api_stream_live.py` covers the five live-tail endpoints
load when the increment changes wire-format behavior or adds a (claims, remittances, activity, acks, ta1-acks); load when the
streaming hook. increment changes wire-format behavior or adds a streaming hook.
+2 -2
View File
@@ -5,9 +5,9 @@ description: "Cyclone pytest + vitest fixture patterns, prodfiles layout, backen
# cyclone-tests # cyclone-tests
The Cyclone test suite is split two ways: **backend pytest** (89 test files under `backend/tests/`, 13 flat fixtures in `backend/tests/fixtures/`, one autouse `conftest.py` that resets the DB per-test) and **frontend vitest** (59 `*.test.ts(x)` siblings across `src/`, two rendering styles — `@testing-library/react` and a custom `createRoot`+`Probe` shim). This skill codifies the conventions so additions stay consistent with what's already there. The Cyclone test suite is split two ways: **backend pytest** (~173 test files under `backend/tests/`, ~21 flat fixtures in `backend/tests/fixtures/`, one autouse `conftest.py` that resets the DB per-test) and **frontend vitest** (85 `*.test.ts(x)` siblings across `src/`, two rendering styles — `@testing-library/react` and a custom `createRoot`+`Probe` shim). This skill codifies the conventions so additions stay consistent with what's already there.
As of this writing: **89 backend test files**, **13 flat backend fixtures**, **59 frontend `*.test.ts(x)` siblings**, and **23 prodfiles samples** across `docs/prodfiles/{837p-from-axiscare,835fromco,FromHPE,claims}/`. The next increment is **SP24** (SP23 is the Ubuntu+Docker fork, awaiting user decision). As of this writing: **~173 backend test files**, **~21 flat backend fixtures** (the original 13 plus additions for `reissue-claims`, `validate-837`, `submit-batch`, and SP41 rebill fixtures), **85 frontend `*.test.ts(x)` siblings**, and a slimmed-down `docs/prodfiles/references/` archive (the bulk of the prodfiles were moved out of the repo; remaining sample is `good-claim-com-2026-04-16.837`). Tests that previously reached into `docs/prodfiles/claims/` and `docs/prodfiles/FromHPE/` now skip when those dirs are absent (`tests/test_prodfiles_smoke.py`). The most recently shipped increment is **SP41** (in-window rebill pipeline); the next free increment is **SP43** after this SP42 doc-pass merges.
## Auth flag (SP24) ## Auth flag (SP24)
+6 -7
View File
@@ -94,7 +94,7 @@ Cyclone ships 8 skills under `.superpowers/skills/`. They auto-load by descripti
## The SP-N increment flow ## The SP-N increment flow
Every feature ships as a numbered **SP-N increment**: spec → plan → implementation branch → single atomic merge into `main`. As of the last backfill, SP numbers are used through **SP22**; **SP23** is reserved for the Ubuntu + Docker + RBAC product fork (`docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md`, awaiting user decision); the next free increment is **SP24**. Read `cyclone-spec` before starting a new one. Non-negotiable shape: Every feature ships as a numbered **SP-N increment**: spec → plan → implementation branch → single atomic merge into `main`. SP numbers are used through **SP41** (in-window rebill pipeline, merged `309e6c0`); the next free increment is **SP42**. SP23 (Ubuntu + Docker + auth + RBAC + LAN-bind product fork) shipped via `merge: SP23 Ubuntu Docker Deployment into main` (`07a7ecb`). Read `cyclone-spec` before starting a new one. Non-negotiable shape:
- **Branch:** `sp<N>-<short-kebab-topic>` (e.g. `sp22-line-reconciliation`). - **Branch:** `sp<N>-<short-kebab-topic>` (e.g. `sp22-line-reconciliation`).
- **Spec path:** `docs/superpowers/specs/YYYY-MM-DD-cyclone-<topic>-design.md`, header `Status: Draft, awaiting user sign-off`, sections `Scope / Decisions / …`. Specs contain zero code blocks. - **Spec path:** `docs/superpowers/specs/YYYY-MM-DD-cyclone-<topic>-design.md`, header `Status: Draft, awaiting user sign-off`, sections `Scope / Decisions / …`. Specs contain zero code blocks.
@@ -116,6 +116,8 @@ Endpoints (all accept the same query params as their non-streaming counterparts;
| GET | `/api/claims/stream` | `claim_written` | `-submission_date` | | GET | `/api/claims/stream` | `claim_written` | `-submission_date` |
| GET | `/api/remittances/stream` | `remittance_written` | `-received_date` | | GET | `/api/remittances/stream` | `remittance_written` | `-received_date` |
| GET | `/api/activity/stream` | `activity_recorded` | `-timestamp` (limit 50) | | GET | `/api/activity/stream` | `activity_recorded` | `-timestamp` (limit 50) |
| GET | `/api/acks/stream` | `ack_received` | `-received_date` |
| GET | `/api/ta1-acks/stream` | `ta1_ack_received` | `-received_date` |
Wire format: one JSON object per line, `{"type": ..., "data": ...}`. The first batch is the **snapshot** of currently-known rows, then `snapshot_end` with the count, then the **live** events. Known types: `item`, `snapshot_end`, `heartbeat` (keeps the connection alive on idle — clients flip to `stalled` after 30s of total silence), `item_dropped` (rare), `error`. Wire format: one JSON object per line, `{"type": ..., "data": ...}`. The first batch is the **snapshot** of currently-known rows, then `snapshot_end` with the count, then the **live** events. Known types: `item`, `snapshot_end`, `heartbeat` (keeps the connection alive on idle — clients flip to `stalled` after 30s of total silence), `item_dropped` (rare), `error`.
@@ -125,9 +127,9 @@ Frontend triplet for any live page: `use<X>(params)` (initial fetch) + `useTailS
## Backend at a glance ## Backend at a glance
`backend/src/cyclone/` is a single namespace. The two largest files are `api.py` (~3,548 LOC, the only large file) and `store.py` (~2,423 LOC, the `CycloneStore` facade — SP21 is in flight to split it into a `cyclone/store/` subpackage; the public API stays unchanged). Subpackages: `api_routers/` (acks, admin, health, ta1_acks), `clearhouse/` (Clearhouse + SftpClient), `edi/` (filenames), `parsers/` (X12 transaction parsers + models + validators + serializers), `submission/` (SP37 — canonical `submit_file` helper shared by `cyclone submit-batch` CLI + `POST /api/submit-batch` HTTP endpoint; owns parse → DB-write → SFTP-upload → audit per file), `workflow/` (placeholder for future sub-project 6). `backend/src/cyclone/` is a single namespace. After SP21 (store split, `4360ef7`) and SP36 (api-routers split, `f005494`), the largest files now live under `cyclone/api_routers/` (22 routers) and `cyclone/store/` (the SP21-split facade in 16 modules). `api.py` is a ~377-line shell that wires the FastAPI app, mounts the auth + admin + api_routers routers, and re-exports a few backward-compat shims for tests; `store.py` no longer exists. Subpackages: `api_routers/` (SP36 — 22 routers: acks, activity, admin, batches, claim_acks, claims, clearhouse, config, dashboard, eligibility, health, inbox, parse, payers, providers, rebill, reconciliation, remittances, submission, ta1_acks, plus `_shared.py`), `auth/` (SP24 — bcrypt + HttpOnly session cookies + RBAC `matrix_gate`; 10 modules), `clearhouse/` (Clearhouse + SftpClient), `edi/` (filenames), `handlers/` (SP27 — per-file-type inbound parse handlers), `parsers/` (X12 transaction parsers + models + validators + serializers), `rebill/` (SP41 — in-window rebill pipeline), `reissue/` (SP24 — offline 837P reissue workflow), `store/` (SP21 — split `CycloneStore` facade), `submission/` (SP37 — canonical `submit_file` helper shared by `cyclone submit-batch` CLI + `POST /api/submit-batch` HTTP endpoint; owns parse → DB-write → SFTP-upload → audit per file). The legacy `workflow/` directory is dead bytecode — see REQUIREMENTS.md R-27 (Open).
The store is the only read/write surface for the database; every mutating endpoint goes through it. All persistence flows through SQLAlchemy sessions via `db.SessionLocal()()`. SQLAlchemy ORM models live in `db.py`; 12 SQL migrations under `migrations/` (0001_initial through 0012_backups) are walked in order by `db_migrate.py`. The store is the only read/write surface for the database; every mutating endpoint goes through it. All persistence flows through SQLAlchemy sessions via `db.SessionLocal()()`. SQLAlchemy ORM models live in `db.py`; 23 SQL migrations under `migrations/` (0001_initial through 0023_visits) are walked in order by `db_migrate.py`.
The parser pipeline is a 5-stage `tokenize → segmentize → model → validate → write_to_store` flow used for every inbound X12 type. Per-transaction parsers: `parse_837.py`, `parse_835.py`, `parse_999.py`, `parse_ta1.py`, `parse_270.py`, `parse_271.py`, `parse_277ca.py`. Each has a matching Pydantic model module (`models.py`, `models_835.py`, …) and a writer (`writer.py` / `writer_835.py`). The 837P serializer (`serialize_837.py`) is the byte-faithful outbound counterpart used by both single-claim download (`/api/claims/{id}/serialize-837`) and the bulk rejected-resubmit bundle (`/api/inbox/rejected/resubmit?download=true`). The parser pipeline is a 5-stage `tokenize → segmentize → model → validate → write_to_store` flow used for every inbound X12 type. Per-transaction parsers: `parse_837.py`, `parse_835.py`, `parse_999.py`, `parse_ta1.py`, `parse_270.py`, `parse_271.py`, `parse_277ca.py`. Each has a matching Pydantic model module (`models.py`, `models_835.py`, …) and a writer (`writer.py` / `writer_835.py`). The 837P serializer (`serialize_837.py`) is the byte-faithful outbound counterpart used by both single-claim download (`/api/claims/{id}/serialize-837`) and the bulk rejected-resubmit bundle (`/api/inbox/rejected/resubmit?download=true`).
@@ -167,7 +169,7 @@ The canonical way to write 999/TA1/277CA/835 rows into the DB is `Scheduler.proc
## Frontend at a glance ## Frontend at a glance
`src/` is React 18 + TypeScript + Vite. Routes register in `src/App.tsx` (11 pages, all under a `<Layout>` route wrapper). Pages are pure renderers — every page pairs with a `use<X>` data hook in `src/hooks/` and renders a `<PageHeader>` + a table/list/KPI grid. Drawers (`ClaimDrawer/`, `RemitDrawer/`, plus the new `ProviderDrawer/` and `AckDrawer/`) are mounted by the page and their open/close state is mirrored to the URL via `useDrawerUrlState` so deep-links round-trip. Drill-stack navigation is provided by `<DrillStackProvider>` in `src/components/drill/`. `src/` is React 18 + TypeScript + Vite. Routes register in `src/App.tsx` (12 pages — Dashboard, Claims, Remittances, Providers, ActivityLog, Upload, Reconciliation, Inbox, Acks, Batches, BatchDiff, Login — all under a `<Layout>` route wrapper, with `Login` outside the auth gate). Pages are pure renderers — every page pairs with a `use<X>` data hook in `src/hooks/` and renders a `<PageHeader>` + a table/list/KPI grid. Drawers (`ClaimDrawer/`, `RemitDrawer/`, `ProviderDrawer/`, `AckDrawer/`) are mounted by the page and their open/close state is mirrored to the URL via `useDrawerUrlState` so deep-links round-trip. Drill-stack navigation is provided by `<DrillStackProvider>` in `src/components/drill/`.
State split: **server state** in TanStack Query (`@tanstack/react-query`); **ephemeral client state** in Zustand (`useTailStore` for live-tail append, plus the drill stack). The live-tail store is FIFO-capped at `TAIL_CAP = 10_000` per slice (`src/store/tail-store.ts:28`); `claims` and `remittances` are key-by-id with first-write-wins dedup, `activity` is an append-only array. State split: **server state** in TanStack Query (`@tanstack/react-query`); **ephemeral client state** in Zustand (`useTailStore` for live-tail append, plus the drill stack). The live-tail store is FIFO-capped at `TAIL_CAP = 10_000` per slice (`src/store/tail-store.ts:28`); `claims` and `remittances` are key-by-id with first-write-wins dedup, `activity` is an append-only array.
@@ -181,9 +183,6 @@ Path alias `@/` → `src/`. Configured in `vite.config.ts`, `vitest.config.ts`,
# Parser # Parser
python -m cyclone.cli parse-837 path/to/837p.txt --output-dir ./claims --payer co_medicaid [--strict] [--include-raw-segments] python -m cyclone.cli parse-837 path/to/837p.txt --output-dir ./claims --payer co_medicaid [--strict] [--include-raw-segments]
python -m cyclone.cli parse-835 path/to/835.txt --output-dir ./remits python -m cyclone.cli parse-835 path/to/835.txt --output-dir ./remits
python -m cyclone.cli parse-999 inbound_999.txt
python -m cyclone.cli parse-ta1 inbound_ta1.txt
python -m cyclone.cli parse-277ca inbound_277ca.txt
# Validators # Validators
python -m cyclone.cli validate-npi 1234567893 python -m cyclone.cli validate-npi 1234567893
+61 -43
View File
@@ -2,12 +2,17 @@
A self-hosted EDI claims management suite for a single billing office. A self-hosted EDI claims management suite for a single billing office.
Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1
and 005010X221A1), with a local-only FastAPI backend and a React UI for and 005010X221A1), with a FastAPI backend and a React UI for browsing,
browsing, filtering, and inspecting the parsed data. filtering, and inspecting the parsed data.
Local-only on purpose: binds to `127.0.0.1`, no auth, no internet exposure. LAN-only by design: always binds to `0.0.0.0:8000` and requires login
Built for one operator, one machine, one trading partner (Colorado (bcrypt + HttpOnly session cookie; first admin bootstrapped from
Medicaid, currently). `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`). Reachability is
controlled by the host firewall / compose port publishing, not by the
bind address. Built for one operator, one machine, one trading partner
(Colorado Medicaid, currently). Do not expose the published ports to
the public internet. See [SP23](docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md)
for the LAN-bind / Docker / RBAC product fork that shipped 2026-06-23.
## Install ## Install
@@ -185,12 +190,14 @@ happening — clients flip to `stalled` after 30s of total silence
### Endpoints ### Endpoints
| Method | Path | Subscribes to | Default sort | | Method | Path | Subscribes to | Default sort |
| ------ | -------------------------- | ------------------- | ------------------- | | ------ | -------------------------- | -------------------- | ------------------- |
| GET | `/api/claims/stream` | `claim_written` | `-submission_date` | | GET | `/api/claims/stream` | `claim_written` | `-submission_date` |
| GET | `/api/remittances/stream` | `remittance_written`| `-received_date` | | GET | `/api/remittances/stream` | `remittance_written` | `-received_date` |
| GET | `/api/activity/stream` | `activity_recorded` | `-timestamp` (limit 50) | | GET | `/api/activity/stream` | `activity_recorded` | `-timestamp` (limit 50) |
| GET | `/api/acks/stream` | `ack_received` | `-received_date` |
| GET | `/api/ta1-acks/stream` | `ta1_ack_received` | `-received_date` |
All three accept the same query params as their non-streaming All five accept the same query params as their non-streaming
counterparts (`status`, `payer`, `date_from`, …) so a frontend can counterparts (`status`, `payer`, `date_from`, …) so a frontend can
swap a one-shot fetch for a tail with no URL surgery. Responses swap a one-shot fetch for a tail with no URL surgery. Responses
are `Content-Type: application/x-ndjson`. are `Content-Type: application/x-ndjson`.
@@ -424,8 +431,8 @@ in a single walk.
`verify_chain` is the integrity check that backs the audit promise — `verify_chain` is the integrity check that backs the audit promise —
it is intentionally cheap (one indexed walk) and intentionally it is intentionally cheap (one indexed walk) and intentionally
side-effect-free so a scheduler can run it on a cron and alert on any side-effect-free so a scheduler can run it on a cron and alert on any
non-`{ok: true}` result. Chain verification is **not** access-gated non-`{ok: true}` result. Chain verification is gated by the standard
beyond the same `127.0.0.1` bind the rest of the API uses; for a `matrix_gate` dependency (admin role for the `/verify` subpath); for a
hostile multi-operator deployment, wrap the route in your reverse proxy. hostile multi-operator deployment, wrap the route in your reverse proxy.
## Encryption at Rest ## Encryption at Rest
@@ -743,12 +750,11 @@ backup API).
. .
├── backend/ ├── backend/
│ ├── src/cyclone/ │ ├── src/cyclone/
│ │ ├── api.py # FastAPI app, GET + parse routes, /api/{resource}/stream │ │ ├── api.py # FastAPI app shell (~377 LOC post-SP36; routes live in api_routers/)
│ │ ├── api_helpers.py # NDJSON / content-negotiation / live-tail helpers │ │ ├── api_helpers.py # NDJSON / content-negotiation / live-tail helpers
│ │ ├── pubsub.py # in-process EventBus (drop-oldest, per-kind fan-out) │ │ ├── pubsub.py # in-process EventBus (drop-oldest, per-kind fan-out)
│ │ ├── store.py # CycloneStore, mappers, publish-on-write
│ │ ├── db.py # SQLAlchemy engine, session factory, ORM models │ │ ├── db.py # SQLAlchemy engine, session factory, ORM models
│ │ ├── db_migrate.py # PRAGMA user_version migration runner │ │ ├── db_migrate.py # PRAGMA user_version migration runner (23 migrations)
│ │ ├── db_crypto.py # optional SQLCipher encryption at rest (SP12) │ │ ├── db_crypto.py # optional SQLCipher encryption at rest (SP12)
│ │ ├── audit_log.py # tamper-evident hash-chained audit_log (SP11) │ │ ├── audit_log.py # tamper-evident hash-chained audit_log (SP11)
│ │ ├── inbox_lanes.py # rejected / payer_rejected / candidates / unmatched / done_today │ │ ├── inbox_lanes.py # rejected / payer_rejected / candidates / unmatched / done_today
@@ -756,37 +762,45 @@ backup API).
│ │ ├── inbox_state_277ca.py # 277CA STC A4/A6/A7 → payer_rejected stamp (SP10) │ │ ├── inbox_state_277ca.py # 277CA STC A4/A6/A7 → payer_rejected stamp (SP10)
│ │ ├── providers.py # multi-NPI provider lookups (SP9) │ │ ├── providers.py # multi-NPI provider lookups (SP9)
│ │ ├── payers.py # payer / payer_config lookups (SP9) │ │ ├── payers.py # payer / payer_config lookups (SP9)
│ │ ├── secrets.py # macOS Keychain-backed secret fetcher │ │ ├── secrets.py # macOS Keychain-backed secret fetcher (4-tier: _FILE > env > Keychain > None)
│ │ ├── reconcile.py # pure-function 835→claim match + line-level match │ │ ├── reconcile.py # pure-function 835→claim match + line-level match (SP31 pcn-exact)
│ │ ├── __main__.py # `python -m cyclone serve` │ │ ├── scheduler.py # inbound MFT polling scheduler (SP16, SP27 per-op timeout)
│ │ ├── cli.py # click CLI │ │ ├── edifabric.py # Edifabric /v2/x12/{read,validate} HTTP client (SP40)
│ │ ── parsers/ # X12 tokenizer, models, validator, writers, 277CA, 999, TA1, 270, 271 │ │ ── backup.py / backup_service.py / backup_scheduler.py # SP17 encrypted backups
└── tests/ │ ├── __main__.py # `python -m cyclone serve` (auth bootstrap then dispatch)
├── fixtures/ # co_medicaid_*.txt, minimal_*.txt ├── cli.py # click CLI (15+ subcommands; see cyclone-cli skill)
├── test_api.py # parse-837/835 round-trip ├── api_routers/ # SP36 — 22 FastAPI routers + _shared.py helpers
├── test_api_gets.py # 6 GET endpoints ├── auth/ # SP24 — bcrypt + sessions + matrix_gate + admin users + rate_limit
├── test_store.py # store + mappers + iterators ├── clearhouse/ # SftpClient + InboundFile + SftpStat
├── test_api_streaming.py ├── edi/ # filename regex/builder helpers
├── test_api_stream_live.py # 3 live-tail endpoints + disconnect cleanup ├── handlers/ # SP27 — per-file-type inbound parse handlers
├── test_pubsub.py # EventBus + subscribe/unsubscribe ├── parsers/ # X12 tokenizer, models, validator, writers, 277CA, 999, TA1, 270, 271
├── test_api_parse_persists.py ├── rebill/ # SP41 — in-window rebill pipeline (13 modules)
├── test_db.py / test_db_crypto.py / test_db_migrate.py ├── reissue/ # SP24 — offline 837P reissue workflow
├── test_audit_log.py ├── store/ # SP21 — split CycloneStore facade (16 modules)
── test_inbox_lanes.py / test_inbox_state.py ── submission/ # SP37 — canonical submit_file helper + recover + bulk_ingest
── test_apply_277ca_rejections.py ── tests/ # 178 pytest files (post-SP41)
│ ├── test_sftp_stub.py / test_sftp_paramiko.py │ ├── fixtures/ # 22 entries (837P / 835 / 999 / TA1 / 277CA samples + visits CSV)
── test_providers_seed.py / test_payer_config_loading.py ── test_api_*.py # FastAPI integration via TestClient
├── src/ # React + Vite + TypeScript UI │ ├── test_*.py # pure-unit
│ └── ...
├── src/ # React + Vite + TypeScript UI (12 pages, 84 test files)
│ ├── components/ │ ├── components/
│ │ ├── ui/ # Skeleton, EmptyState, ErrorState, FilterChips, Pagination, … │ │ ├── ui/ # Skeleton, EmptyState, ErrorState, FilterChips, Pagination, …
│ │ ├── ClaimDrawer/ # 12 sibling tests
│ │ ├── RemitDrawer/ # 8 sibling tests
│ │ ├── ProviderDrawer/ # right-anchored side panel
│ │ ├── AckDrawer/ # right-anchored side panel (SP28 ack-claim auto-link)
│ │ ├── inbox/ # 5-lane triage surface (SP14)
│ │ ├── drill/ # DrillStackProvider + DrillableCell + PeekModal
│ │ └── TailStatusPill.tsx # live-tail status badge + reconnect button │ │ └── TailStatusPill.tsx # live-tail status badge + reconnect button
│ ├── pages/ # Claims, Remittances, Providers, Acks, Activity, Upload, Inbox, … │ ├── pages/ # Dashboard, Claims, Remittances, Providers, ActivityLog,
│ │ # Upload, Reconciliation, Inbox, Acks, Batches, BatchDiff, Login
│ ├── hooks/ # useBatches, useClaims, useRemittances, useProviders, useActivity, useParse │ ├── hooks/ # useBatches, useClaims, useRemittances, useProviders, useActivity, useParse
│ │ # + useTailStream, useMergedTail (live tail) │ │ # + useTailStream, useMergedTail (live tail triplet)
│ ├── lib/ # api.ts, format.ts, utils.ts │ ├── auth/ # AuthProvider, RoleGate, api client (SP24)
# + tail-stream.ts (NDJSON parser) ├── lib/ # api.ts, format.ts, utils.ts, tail-stream.ts (NDJSON parser), inbox-api.ts
│ ├── store/ # zustand sample-data + parsed-batches store │ ├── store/ # zustand useAppStore (sample data) + useTailStore (FIFO-capped live tail slices)
│ │ # + tail-store.ts (FIFO-capped live tail slices)
│ └── types/ # shared TS types │ └── types/ # shared TS types
├── config/ ├── config/
│ └── payers.yaml # YAML-driven payer + clearhouse config (SP9) │ └── payers.yaml # YAML-driven payer + clearhouse config (SP9)
@@ -807,11 +821,15 @@ backup API).
> 3. The per-SP spec under [`docs/superpowers/specs/`](docs/superpowers/specs/) for whatever you're touching. > 3. The per-SP spec under [`docs/superpowers/specs/`](docs/superpowers/specs/) for whatever you're touching.
> 4. The per-SP plan under [`docs/superpowers/plans/`](docs/superpowers/plans/) if you're implementing. > 4. The per-SP plan under [`docs/superpowers/plans/`](docs/superpowers/plans/) if you're implementing.
Sub-projects 2 through 19 are **shipped**. See the [completeness Sub-projects 2 through 41 are **shipped** (last merged: SP41 in-window
rebill pipeline, `merge: SP41 inwindow-rebill-pipeline into main` at
`309e6c0`). See the [completeness
review](docs/reviews/2026-06-20-cyclone-completeness-review.md) for review](docs/reviews/2026-06-20-cyclone-completeness-review.md) for
the honest gap analysis against the industry definition of a HIPAA the honest gap analysis against the industry definition of a HIPAA
clearinghouse — the short version is that the local-only, clearinghouse — the short version is that the LAN-only,
single-operator, single-payer design contract is honored, and the single-operator, single-payer design contract is honored (auth per
SP23, encryption at rest via SQLCipher per SP12, RBAC per SP23, audit
log per SP11), and the
items that would be needed to expand that contract (AS2/AS4, SNIP 17, items that would be needed to expand that contract (AS2/AS4, SNIP 17,
HITRUST, 276/277 status, 278 referrals, COB) are intentionally out of HITRUST, 276/277 status, 278 referrals, COB) are intentionally out of
scope. scope.
+171 -57
View File
@@ -4,8 +4,8 @@
> >
> **Companion docs:** > **Companion docs:**
> - **Requirements:** [`docs/REQUIREMENTS.md`](REQUIREMENTS.md) — what Cyclone does (FRs + NFRs + DoD + traceability). > - **Requirements:** [`docs/REQUIREMENTS.md`](REQUIREMENTS.md) — what Cyclone does (FRs + NFRs + DoD + traceability).
> - **Specs:** [`docs/superpowers/specs/`](superpowers/specs/) — design specs per sub-project (22 specs). > - **Specs:** [`docs/superpowers/specs/`](superpowers/specs/) — design specs per sub-project (42 specs).
> - **Plans:** [`docs/superpowers/plans/`](superpowers/plans/) — TDD-shaped implementation plans per sub-project (27 plans). > - **Plans:** [`docs/superpowers/plans/`](superpowers/plans/) — TDD-shaped implementation plans per sub-project (37 plans).
> >
> **Scope of this doc:** architecture and design. The spec files own per-SP design; the plan files own the task order. This doc explains the *whole* — the boundaries between modules, the data flow across them, the lifecycle of an inbound file from upload to reconciled claim. > **Scope of this doc:** architecture and design. The spec files own per-SP design; the plan files own the task order. This doc explains the *whole* — the boundaries between modules, the data flow across them, the lifecycle of an inbound file from upload to reconciled claim.
@@ -13,7 +13,7 @@
## 1. System overview ## 1. System overview
Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office. The first deployment target is Colorado Medicaid (one operator, one trading partner). The system is local-only on purpose: the backend binds to `127.0.0.1` and requires login (bcrypt + HttpOnly session cookie). The threat model is still a stolen or imaged drive, not a remote attacker — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged. The SP23 product fork changes the threat model to "remote operator on LAN." Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office. The first deployment target is Colorado Medicaid (one operator, one trading partner). LAN-only by design: the backend always binds to `0.0.0.0:8000` and requires login (bcrypt + HttpOnly session cookie per SP23/24). Reachability is controlled by the host firewall / compose port publishing, not the bind address. The threat model is still a stolen or imaged drive, not a remote attacker — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged.
Cyclone processes three X12 transaction pairs end-to-end: Cyclone processes three X12 transaction pairs end-to-end:
@@ -46,17 +46,22 @@ The whole stack is a single Python process (FastAPI + uvicorn on port 8000) plus
+----------------------------------------------------------+ +----------------------------------------------------------+
| Python process: `python -m cyclone serve` | | Python process: `python -m cyclone serve` |
| | | |
| FastAPI app (uvicorn, 127.0.0.1:8000) | | FastAPI app (uvicorn, 0.0.0.0:8000; SP23) |
| +-----------------------------------------------------+ | | +-----------------------------------------------------+ |
| | cyclone.api:app (3,548 LOC) | | | | cyclone.api:app (~377 LOC, post-SP36 thin shell) | |
| | - routers: claims, remits, providers, acks, | | | | - mounts 22 routers under cyclone.api_routers/ | |
| | activity, inbox, upload, download, batches, | | | | (acks, activity, admin, batches, claim_acks, | |
| | admin, scheduler, health, ta1_acks | | | | claims, clearhouse, config, dashboard, | |
| | eligibility, health, inbox, parse, payers, | |
| | providers, rebill, reconciliation, remittances, | |
| | submission, ta1_acks) | |
| | - matrix_gate dependency on every gated router | |
| | (SP24 auth) | |
| | - live-tail: NDJSON pubsub (pubsub.py) | | | | - live-tail: NDJSON pubsub (pubsub.py) | |
| | - lifespan: init db -> seed SP9 -> start SP16 + | | | | - lifespan: init db -> seed SP9 -> start SP16 | |
| | SP17 schedulers -> load PayerConfig cache | | | | + SP17 schedulers -> load PayerConfig cache | |
| +-----------------------------------------------------+ | | +-----------------------------------------------------+ |
| | cyclone.store (facade, 2,423 LOC) | | | | cyclone.store (facade, post-SP21 split: 16 modules)| |
| | - add/get/list/manual_match/iter_claims/... | | | | - add/get/list/manual_match/iter_claims/... | |
| | - delegates to: db.SessionLocal() | | | | - delegates to: db.SessionLocal() | |
| +-----------------------------------------------------+ | | +-----------------------------------------------------+ |
@@ -64,8 +69,13 @@ The whole stack is a single Python process (FastAPI + uvicorn on port 8000) plus
| | 277CA: tokenize -> segmentize -> model -> validate | | | | 277CA: tokenize -> segmentize -> model -> validate | |
| | -> write to store) | | | | -> write to store) | |
| +-----------------------------------------------------+ | | +-----------------------------------------------------+ |
| | cyclone.reconcile, scoring, batch_diff, audit_log, | | | | cyclone.auth.* (SP24: bcrypt + sessions + matrix | |
| | backup_service, scheduler, db_crypto, secrets | | | | gate), cyclone.handlers.* (SP27: per-kind parse), | |
| | cyclone.rebill.* (SP41: rebill pipeline), | |
| | cyclone.reissue.* (SP24: reissue CLI), | |
| | reconcile, scoring, batch_diff, audit_log, | |
| | backup_service, scheduler, db_crypto, secrets, | |
| | edifabric (SP40), submission (SP37) | |
| +-----------------------------------------------------+ | | +-----------------------------------------------------+ |
+----------------------------------------------------------+ +----------------------------------------------------------+
| | | | | |
@@ -93,7 +103,7 @@ The `cyclone-pipeline/` sibling project (not in this repo) is a separate Python
|---|---|---|---| |---|---|---|---|
| Language | Python | 3.11+ (3.13 in dev) | `requires-python = ">=3.11"` | | Language | Python | 3.11+ (3.13 in dev) | `requires-python = ">=3.11"` |
| Web framework | FastAPI | ≥0.110, <1 | `python -m cyclone serve` boots uvicorn | | Web framework | FastAPI | ≥0.110, <1 | `python -m cyclone serve` boots uvicorn |
| ASGI server | uvicorn[standard] | ≥0.27, <1 | `--host 127.0.0.1 --port 8000` | | ASGI server | uvicorn[standard] | ≥0.27, <1 | `--host 0.0.0.0 --port 8000` (CYCLONE_HOST/CYCLONE_PORT override) |
| ORM | SQLAlchemy | ≥2.0, <3 | 2.x style; engine + SessionLocal() function-accessor | | ORM | SQLAlchemy | ≥2.0, <3 | 2.x style; engine + SessionLocal() function-accessor |
| Validation | Pydantic | ≥2.6, <3 | v2 model_validator + ConfigDict | | Validation | Pydantic | ≥2.6, <3 | v2 model_validator + ConfigDict |
| CLI | Click | ≥8.1, <9 | `cyclone` console script | | CLI | Click | ≥8.1, <9 | `cyclone` console script |
@@ -135,38 +145,75 @@ The `cyclone-pipeline/` sibling project (not in this repo) is a separate Python
### 4.1 Package layout ### 4.1 Package layout
The `cyclone` package is a single namespace rooted at `backend/src/cyclone/`. 22 top-level modules + 4 subpackages. The store (SP21 in-flight split) is the largest; everything else is small and focused. The `cyclone` package is a single namespace rooted at `backend/src/cyclone/`. After SP21 (store split), SP24 (auth + reissue), SP27 (handlers), SP36 (api-routers split), and SP41 (rebill pipeline), the package has **22 top-level modules + 10 subpackages**. The two largest files are now `cyclone/api_routers/admin.py` (~640 LOC, the admin operator surface) and the various pipeline modules in `cyclone/rebill/`. `api.py` is a ~377-line shell (post-SP36) and `store.py` no longer exists (post-SP21).
``` ```
backend/src/cyclone/ backend/src/cyclone/
├── __main__.py — entry point; `python -m cyclone` → CLI, `python -m cyclone serve` → uvicorn ├── __main__.py — entry point; `python -m cyclone` → CLI, `python -m cyclone serve` → uvicorn
├── __init__.py — `__version__`, package init ├── __init__.py — `__version__`, package init
├── api.py — FastAPI app, route includes, lifespan (3,548 LOC — the only large file) ├── api.py — FastAPI app, route mounting (~377 LOC, post-SP36 thin shell)
├── api_helpers.py — request/response formatters, error → HTTP mapping ├── api_helpers.py — request/response formatters, error → HTTP mapping, NDJSON helpers
├── api_routers/ ├── api_routers/ — SP36: 22 per-resource FastAPI routers + _shared.py
│ ├── __init__.py │ ├── __init__.py — registry: `routers: list[APIRouter]`
│ ├── acks.py — 999/TA1/271/277CA acknowledgments │ ├── _shared.py — cross-router helpers
│ ├── admin.py — admin-only endpoints (validate-provider, scheduler, backup) │ ├── acks.py 999/277CA acks list / stream / detail
│ ├── health.py — GET /api/health (SP19 deep snapshot) │ ├── activity.py — activity feed + stream
── ta1_acks.py — TA1-specific (split from acks for the SP3 lifecycle) ── admin.py — admin-only operator surface (validate-provider, scheduler, backup, users, validate-837)
│ ├── batches.py — batch list / detail / ZIP export
│ ├── claim_acks.py — SP28 claim ↔ ack auto-link endpoints
│ ├── claims.py — claim register / stream / detail / serialize-837 / line-reconciliation
│ ├── clearhouse.py — singleton clearhouse config + SFTP submit
│ ├── config.py — payer-config read views
│ ├── dashboard.py — GET /api/dashboard/kpis
│ ├── eligibility.py — 270 build / 271 parse
│ ├── health.py — GET /api/health (public)
│ ├── inbox.py — 5-lane triage surface (SP14)
│ ├── parse.py — SP35 envelope-guarded parse-* endpoints
│ ├── payers.py — payer-level rollup
│ ├── providers.py — distinct providers + configured providers
│ ├── rebill.py — SP41 POST/GET /api/admin/rebill-from-835
│ ├── reconciliation.py — manual match + batch diff
│ ├── remittances.py — remittance register / summary / stream / detail
│ ├── submission.py — SP37 POST /api/submit-batch
│ └── ta1_acks.py — TA1 list / stream / detail
├── audit_log.py — SHA-256 hash-chained append-only log (SP11) ├── audit_log.py — SHA-256 hash-chained append-only log (SP11)
├── auth/ — SP24: bcrypt + sessions + RBAC matrix_gate
│ ├── __init__.py — `AUTH_DISABLED` flag + module surface
│ ├── bootstrap.py — first-admin env-var bootstrap
│ ├── cli.py — `cyclone users` group
│ ├── deps.py — FastAPI deps + matrix_gate
│ ├── permissions.py — role matrix (admin/user/viewer)
│ ├── rate_limit.py — per-username login throttle (5 fails / 5 min)
│ ├── routes.py — /api/auth/{login,logout,me}
│ ├── sessions.py — 24h sliding expiry
│ └── users.py — user CRUD + bcrypt password hashing
├── backup.py — SP17 primitives: derive_key, encrypt, decrypt, BackupFile ├── backup.py — SP17 primitives: derive_key, encrypt, decrypt, BackupFile
├── backup_scheduler.py — SP17 BackupScheduler wrapper ├── backup_scheduler.py — SP17 BackupScheduler wrapper
├── backup_service.py — SP17 coordinator: create/list/restore/verify/prune ├── backup_service.py — SP17 coordinator: create/list/restore/verify/prune
├── batch_diff.py — 837 ↔ 835 diff (SP4) ├── batch_diff.py — 837 ↔ 835 diff (SP4)
├── claim_acks.py — SP28 pure readers over session + parse result
├── clearhouse/ ├── clearhouse/
│ └── __init__.py — Clearhouse, SftpClient (SP9, SP13) │ └── __init__.py — Clearhouse, SftpClient (SP9, SP13)
├── cli.py — Click CLI: `cyclone serve`, `cyclone validate-npi`, `cyclone backup`, etc. ├── cli.py — Click CLI: 15+ subcommands (see cyclone-cli skill)
├── db.py — SQLAlchemy engine, SessionLocal, ORM models, init_db, reinit_engine ├── db.py — SQLAlchemy engine, SessionLocal, ORM models, init_db, reinit_engine
├── db_crypto.py — SP12 SQLCipher connect-creator + is_encryption_enabled() ├── db_crypto.py — SP12 SQLCipher connect-creator + is_encryption_enabled()
├── db_migrate.py — runs the 12 SQL migrations in order ├── db_migrate.py — runs the 23 SQL migrations in order
├── edifabric.py — SP40 Edifabric /v2/x12/{read,validate} HTTP client
├── edi/ ├── edi/
│ └── filenames.py — X12 filename convention helpers (SP9) │ └── filenames.py — X12 filename convention helpers (SP9)
├── handlers/ — SP27: per-file-type inbound parse handlers
│ ├── __init__.py — HANDLERS registry + register_handlers()
│ ├── _ack_id.py — synthetic batch id + AK5-count helpers
│ ├── handle_277ca.py — 277CA parse → persist + payer-rejected stamp
│ ├── handle_835.py — 835 parse → validate → persist + reconcile (two-phase)
│ ├── handle_999.py — 999 parse → apply rejections
│ ├── handle_result.py — HandleResult dataclass
│ └── handle_ta1.py — TA1 parse → persist envelope ack + envelope-link batches
├── inbox_lanes.py — 5-lane Inbox computation (SP14) ├── inbox_lanes.py — 5-lane Inbox computation (SP14)
├── inbox_state.py — claim ↔ inbox state mapping helpers ├── inbox_state.py — claim ↔ inbox state mapping helpers
├── inbox_state_277ca.py — SP10 277CA inbox state integration ├── inbox_state_277ca.py — SP10 277CA inbox state integration
├── logging_config.py — SP18 structured JSON + PII scrubber ├── logging_config.py — SP18 structured JSON + PII scrubber
├── migrations/ — 12 SQL files (0001_initial through 0012_backups) ├── migrations/ — 23 SQL files (0001_initial through 0023_visits)
├── npi.py — SP20 NPI Luhn + Tax ID validation ├── npi.py — SP20 NPI Luhn + Tax ID validation
├── parsers/ — X12 parser subpackage ├── parsers/ — X12 parser subpackage
│ ├── parse_837.py — 837P ingest │ ├── parse_837.py — 837P ingest
@@ -187,23 +234,44 @@ backend/src/cyclone/
├── payers.py — Payer ORM row accessor (SP9) ├── payers.py — Payer ORM row accessor (SP9)
├── providers.py — Payer / Clearhouse / Provider ORM row DTOs (SP9) ├── providers.py — Payer / Clearhouse / Provider ORM row DTOs (SP9)
├── pubsub.py — NDJSON live-tail EventBus ├── pubsub.py — NDJSON live-tail EventBus
├── rebill/ — (SP41 in-window rebill pipeline) ├── rebill/ — SP41: in-window rebill pipeline (13 modules)
│ ├── __init__.py — package surface
│ ├── carc_filter.py — CARC-aware filter (EXCLUDED_CARCS / REVIEW_CARCS)
│ ├── parse_835_svc.py — SVC-level 835 reparse w/ member_id │ ├── parse_835_svc.py — SVC-level 835 reparse w/ member_id
│ ├── reconcile.py visit-to-835 join (member, procedure, DOS) │ ├── pipeline_a.py — denied/partial → frequency-7 replacement 837P
│ ├── carc_filter.py — CARC-aware filter │ ├── pipeline_b.py NOT_IN_835 → fresh 837Ps batched by (member_id, ISO-week)
│ ├── timely_filing.py — 120-day DOS age gate │ ├── pull_999_acks.py — 999-ack dump + classify NOT_IN_835
│ ├── pipeline_a.py — denied/partial → frequency-7 │ ├── reconcile.py visit-to-835 join (member, procedure, DOS) with 5% tolerance
│ ├── pipeline_b.py — NOT_IN_835 → fresh 837Ps by (member, week)
│ ├── summary.py — summary CSV generator
│ ├── run.py — orchestrator (run_rebill) │ ├── run.py — orchestrator (run_rebill)
│ ├── pull_999_acks.py — 999-ack dump + classify │ ├── spot_check.py single-claim well-formed 837P from VisitRow
── __init__.py — package surface ── spot_check_pipeline.py — top-N visits → well-formed 837P files
├── reconcile.py — 835 → 837 auto-reconciliation engine │ ├── spot_check_validate.py — submit to Edifabric /v2/x12/validate
├── scheduler.py — SP16 MFT polling scheduler ├── summary.py — summary CSV generator
│ ├── timely_filing.py — 120-day DOS age gate
│ └── visits_store.py — load AxisCare visits CSV into visits table
├── reconcile.py — 835 → 837 auto-reconciliation engine (SP31 pcn-exact strategy)
├── reissue/ — SP24: offline 837P reissue workflow
│ ├── __init__.py
│ └── core.py — parse_inputs / emit_outputs / zip_outputs / ig_correctness_check
├── scheduler.py — SP16 MFT polling scheduler (SP27 per-op timeout)
├── scoring.py — 4-field lane scoring (40/25/20/15, hard-coded — backlog item) ├── scoring.py — 4-field lane scoring (40/25/20/15, hard-coded — backlog item)
├── secrets.py — Keychain access wrapper (SP9) ├── secrets.py — Keychain access wrapper (SP9, SP26 _FILE tier)
├── security.py — security headers, HealthSnapshot, deep health probe (SP19) ├── security.py — security headers, HealthSnapshot, deep health probe (SP19)
── store.py — CycloneStore facade (2,423 LOC; SP21 in-flight split) ── seed_cli.py — `cyclone seed [--count N] [--reset] [--status]` dev seeder
├── store/ — SP21: split CycloneStore facade (16 modules)
│ ├── __init__.py — CycloneStore class + module-level `store` singleton
│ ├── acks.py / batches.py / backfill.py / backups.py
│ ├── claim_acks.py / claim_detail.py / exceptions.py
│ ├── inbox.py / kpis.py / orm_builders.py / providers.py
│ ├── records.py / resubmissions.py / submission_dedup.py
│ ├── ui.py — ORM → UI JSON serializers
│ └── write.py — write-path helpers + event publishing
└── submission/ — SP37: canonical outbound path + SP25/SP26 helpers
├── __init__.py — submit_file, SubmitOutcome, SubmitResult
├── core.py — parse → DB-write → SFTP-upload per file
├── result.py — SubmitOutcome enum + SubmitResult dataclass
├── recover.py — SP25: offline parse + DB-write for stranded files
└── bulk_ingest.py — SP26: offline walk + ingest for 999/TA1/277CA/837P/835
``` ```
### 4.2 Module responsibility map (one line each) ### 4.2 Module responsibility map (one line each)
@@ -253,7 +321,7 @@ The store is the single read/write surface for the database. Every endpoint that
- `store.list_unmatched(kind="both")` - `store.list_unmatched(kind="both")`
- `store.export_*()` — CSV/JSON dumpers - `store.export_*()` — CSV/JSON dumpers
All persistence flows through SQLAlchemy sessions via `db.SessionLocal()()`. The engine is single-connection-per-request via FastAPI dependency injection. SP21 splits `store.py` (2,423 LOC) into a `cyclone/store/` subpackage with one module per domain (batches, inbox, claim_detail, acks, backups, providers, etc.) and a thin facade that delegates every method to its domain module. After SP21, the public API of `cyclone.store` is unchanged; the only thing that changes is the file layout. All persistence flows through SQLAlchemy sessions via `db.SessionLocal()()`. The engine is single-connection-per-request via FastAPI dependency injection. After SP21, `store.py` is split into a `cyclone/store/` subpackage with one module per domain (acks, batches, backfill, backups, claim_acks, claim_detail, exceptions, inbox, kpis, orm_builders, providers, records, resubmissions, submission_dedup, ui, write) plus a thin facade that delegates every method to its domain module. The public API of `cyclone.store` is unchanged.
### 4.4 The parser pipeline ### 4.4 The parser pipeline
@@ -338,21 +406,49 @@ There is no Redux, no Context for global state, no MobX. The combination of reac
### 5.4 Live tail (NDJSON pubsub) ### 5.4 Live tail (NDJSON pubsub)
The Activity feed and the TailStatusPill share a single connection: `GET /api/tail?since=<last_event_id>`. The server returns `Content-Type: application/x-ndjson` and streams one JSON object per line. Each line is one of: The Claims, Remittances, Activity, Acks, and TA1-Acks pages each open their own NDJSON connection to the matching `/api/<resource>/stream` endpoint. The server returns `Content-Type: application/x-ndjson` and streams one JSON object per line. Each line is one of:
- `{kind: "heartbeat", ts: "..."}`every 5s when no events - `{type: "item", data: {...}}`one row from the snapshot (initial batch) or a live event
- `{kind: "event", id: <int>, event_type: "...", payload: {...}}`when something happens - `{type: "snapshot_end", data: {count: N}}`the snapshot has finished
- `{kind: "error", message: "..."}`server-initiated close (rare) - `{type: "heartbeat", data: {ts: "..."}}`every `CYCLONE_TAIL_HEARTBEAT_S` seconds (default 15s) when idle
- `{type: "item_dropped", data: {id: ...}}` — rare: the subscriber queue overflowed
- `{type: "error", data: {message: "..."}}` — server-initiated close (rare)
The client uses `fetch` + `ReadableStream` to consume the body and dispatches each line to the relevant `useQuery` cache via `queryClient.setQueryData`. The connection auto-reconnects on `error` with exponential backoff. See [live-tail spec](superpowers/specs/2026-06-20-cyclone-live-tail-design.md) for the wire format and reconnection policy. The client uses `fetch` + `ReadableStream` to consume the body; each `item` event dispatches into `useTailStore` (zustand). The connection auto-reconnects on `error` with backoff `1s → 2s → 4s → 8s → 16s → 30s` capped. The client flips to `stalled` after 30s of total silence (`STALL_TIMEOUT_MS = 30_000` in `src/hooks/useTailStream.ts`). See [live-tail spec](superpowers/specs/2026-06-20-cyclone-live-tail-design.md) for the wire format and reconnection policy. SP25 added `ack_received` and `ta1_ack_received` event kinds plus `/api/acks/stream` and `/api/ta1-acks/stream` endpoints.
--- ---
## 6. Data model ## 6. Data model
### 6.1 Migrations 00010014 ### 6.1 Migrations 00010023
The schema evolves in ordered SQL files under `backend/src/cyclone/migrations/`. Each file starts with `-- version: N` and is applied in order by `db_migrate.py`. There is no migration framework — `db_migrate.py` is a 30-line file that checks the `schema_version` row and applies everything newer. The first 12 ship on `main`; 13 + 14 are the auth migrations that landed in `main` on 2026-06-23. The schema evolves in ordered SQL files under `backend/src/cyclone/migrations/`. Each file starts with `-- version: N` and is applied in order by `db_migrate.py`. There is no migration framework — `db_migrate.py` is a 30-line file that checks the `schema_version` row and applies everything newer. All 23 ship on `main`:
| # | File | SP | One-line summary |
|---|---|---|---|
| 0001 | `0001_initial.sql` | — | 6 tables (`batches`, `claims`, `remittances`, `cas_adjustments`, `matches`, `activity_events`) + 11 indexes |
| 0002 | `0002_acks.sql` | — | `acks` table + index on `source_batch_id` |
| 0003 | `0003_drop_claims_remits_unique_constraints.sql` | — | drops UNIQUE constraints that blocked re-ingest of duplicates |
| 0004 | `0004_rejections_and_state_history.sql` | — | adds `rejection_reason`, `rejected_at`, `resubmit_count`, `state_changed_at` to `claims` |
| 0005 | `0005_create_ta1_acks.sql` | — | `ta1_acks` table + indexes on `source_batch_id`, `ack_code` |
| 0006 | `0006_line_reconciliation.sql` | — | `service_line_payments` + `line_reconciliation` tables for the 837 vs 835 side-by-side view |
| 0007 | `0007_providers_payers_clearhouse.sql` | SP9 | `providers`, `payers`, `payer_configs`, `clearhouse` tables |
| 0008 | `0008_payer_rejected_columns.sql` | SP10 | `payer_rejected_*` columns on `claims` + `two77ca_acks` table |
| 0009 | `0009_audit_log.sql` | SP11 | tamper-evident `audit_log` table |
| 0010 | `0010_payer_rejected_acknowledged.sql` | SP14 | `payer_rejected_acknowledged_at` + `payer_rejected_acknowledged_actor` + index |
| 0011 | `0011_processed_inbound_files.sql` | SP16 | `processed_inbound_files` dedup table for SFTP polling |
| 0012 | `0012_backups.sql` | SP17 | `db_backups` table for the encrypted backup subsystem |
| 0013 | `0013_auth_users_and_sessions.sql` | SP24 | `users` and `sessions` tables |
| 0014 | `0014_audit_log_user_id.sql` | SP24 | `user_id` column on `audit_log` + index |
| 0015 | `0015_drop_claims_unique_constraint.sql` | SP22 | rebuilds `claims` to drop a UNIQUE constraint that blocked a specific re-ingest pattern |
| 0016 | `0016_claims_matched_remittance_id_index.sql` | SP22 | index on `claims.matched_remittance_id` for inbox join speed |
| 0017 | `0017_backfill_claim_patient_control_number.sql` | — | backfills `claim.patient_control_number` from raw JSON for legacy rows |
| 0018 | `0018_claim_acks.sql` | SP28 | `claim_acks` join table (claim ↔ 999/277CA/TA1) + 3 indexes |
| 0019 | `0019_add_rendering_and_service_provider_npis.sql` | SP32 | typed rendering-NPI / service-provider-NPI columns on `claims` + `remittances` |
| 0020 | `0020_add_batch_txn_set_control_number.sql` | SP37 | `transaction_set_control_number` column on `batches` (ST02) |
| 0021 | `0021_resubmissions.sql` | SP39 | `resubmissions` audit table + indexes |
| 0022 | `0022_submission_dedup.sql` | SP37 | `submission_records` table for duplicate-submission detection |
| 0023 | `0023_visits.sql` | SP41 | `visits` table (AxisCare visits CSV → DB) + 3 indexes |
| # | File | What it adds | | # | File | What it adds |
|---|---|---| |---|---|---|
@@ -371,7 +467,7 @@ The schema evolves in ordered SQL files under `backend/src/cyclone/migrations/`.
| 13 | `0013_auth_users_and_sessions.sql` | `users` + `sessions` tables, `users.password_hash` (bcrypt) — landed 2026-06-23 | | 13 | `0013_auth_users_and_sessions.sql` | `users` + `sessions` tables, `users.password_hash` (bcrypt) — landed 2026-06-23 |
| 14 | `0014_audit_log_user_id.sql` | `audit_log.user_id` FK to `users.id` (forward reference to 0013) — landed 2026-06-23 | | 14 | `0014_audit_log_user_id.sql` | `audit_log.user_id` FK to `users.id` (forward reference to 0013) — landed 2026-06-23 |
The SP22 parse-then-decide migrations (drop claims UNIQUE constraint + relax PK to `(batch_id, id)`) are renumbered to 0015 + 0016 on the `claims-unique-fix` worktree so the auth migrations stay at the front of the chain; they'll keep those numbers when SP22 lands. The SP22 parse-then-decide migrations landed as `0015` + `0016` (drop claims UNIQUE constraint + add `matched_remittance_id` index).
### 6.2 ERD (high level) ### 6.2 ERD (high level)
@@ -496,8 +592,7 @@ The idempotency table is the linchpin: a crash between "read file" and "INSERT p
When an 835 lands, the writer calls `reconcile.apply_payment(claim, remit, strategy)` for each `(claim, remit)` pair in the file. The strategy is one of: When an 835 lands, the writer calls `reconcile.apply_payment(claim, remit, strategy)` for each `(claim, remit)` pair in the file. The strategy is one of:
- `auto:pcn_npi_charge` — patient_control_number + provider_npi + charge_amount match (the strongest) - `pcn-exact` — at least 2 of (patient_control_number, charge_amount, provider_npi) match exactly (PCN case-insensitive with leading-zero normalization). This is the SP31 strategy and replaces the older `auto:pcn_npi_charge` / `auto:pcn_charge` audit labels. `_content_keys_match` is the predicate.
- `auto:pcn_charge` — patient_control_number + charge_amount (the most common in practice)
- `manual` — the operator uses `/reconciliation` to pair by hand; the strategy is recorded for audit - `manual` — the operator uses `/reconciliation` to pair by hand; the strategy is recorded for audit
Each auto-match creates a `matches` row + transitions the claim state + emits an `activity_events` row + emits an `audit_log` row. The match rate is visible in the Dashboard KPI tile. Each auto-match creates a `matches` row + transitions the claim state + emits an `activity_events` row + emits an `audit_log` row. The match rate is visible in the Dashboard KPI tile.
@@ -581,7 +676,7 @@ The full list is in [REQUIREMENTS.md §6.3 traceability matrix](REQUIREMENTS.md)
``` ```
{"kind":"event","id":1234,"event_type":"claim.submitted","payload":{"claim_id":"...","batch_id":"...","ts":"2026-06-23T..."}} {"kind":"event","id":1234,"event_type":"claim.submitted","payload":{"claim_id":"...","batch_id":"...","ts":"2026-06-23T..."}}
{"kind":"event","id":1235,"event_type":"claim.matched","payload":{"claim_id":"...","remit_id":"...","strategy":"auto:pcn_npi_charge"}} {"type":"item","data":{"id":"CLM-...","remit_id":"RM-...","strategy":"pcn-exact","matched_at":"..."}}
{"kind":"heartbeat","ts":"2026-06-23T..."} {"kind":"heartbeat","ts":"2026-06-23T..."}
{"kind":"event","id":1236,"event_type":"claim.payer_rejected","payload":{"claim_id":"...","status_code":"A7","source_277ca_id":"..."}} {"kind":"event","id":1236,"event_type":"claim.payer_rejected","payload":{"claim_id":"...","status_code":"A7","source_277ca_id":"..."}}
``` ```
@@ -613,7 +708,7 @@ See §4.6. The chain is checked on every `/api/health` request and exposes `stat
- **In transit (SFTP)** — TLS to the trading partner; no plaintext EDI over the wire. - **In transit (SFTP)** — TLS to the trading partner; no plaintext EDI over the wire.
- **In the UI** — no PHI is shown on the Dashboard or Inbox (only counts + KPIs). The Claim drawer and Remit drawer show full PHI; these pages are not in the route map's preview and require an explicit navigation click. - **In the UI** — no PHI is shown on the Dashboard or Inbox (only counts + KPIs). The Claim drawer and Remit drawer show full PHI; these pages are not in the route map's preview and require an explicit navigation click.
- **In logs** — scrubbed by key name (see §9.1). - **In logs** — scrubbed by key name (see §9.1).
- **In analytics** — no analytics. No third-party tracking. No telemetry. The frontend makes no requests outside `127.0.0.1:8000`. - **In analytics** — no analytics. No third-party tracking. No telemetry. The frontend makes no requests outside the configured `VITE_API_BASE_URL` (default empty, dev proxy: `http://127.0.0.1:8000`).
### 9.4 Error model ### 9.4 Error model
@@ -700,9 +795,9 @@ npm run dev
The frontend reads its backend URL from `VITE_API_BASE_URL` (default empty). Create `.env.local` at the repo root with `VITE_API_BASE_URL=http://127.0.0.1:8000`. Without it, the UI falls back to its in-memory sample store via the `data` adapter (parses are disabled). The frontend reads its backend URL from `VITE_API_BASE_URL` (default empty). Create `.env.local` at the repo root with `VITE_API_BASE_URL=http://127.0.0.1:8000`. Without it, the UI falls back to its in-memory sample store via the `data` adapter (parses are disabled).
### 11.2 Optional Docker (SP23 — product fork) ### 11.2 Docker (SP23 — shipped)
A Ubuntu + Docker + auth + RBAC + LAN-bind spec exists at [`docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md`](superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md). This is a **product fork** awaiting user decision (per [REQUIREMENTS.md §6.2](REQUIREMENTS.md)). The v1 single-host single-operator posture assumes local-only; SP23 changes the threat model to "remote operator on LAN." The Ubuntu + Docker + auth + RBAC + LAN-bind product fork shipped as SP23 (`merge: SP23 Ubuntu Docker Deployment into main`, `07a7ecb`, 2026-06-23). The compose stack (`docker-compose.yml` + `docker-compose.override.yml`) plus `backend/Dockerfile` and `Dockerfile.frontend` are the canonical deploy shape. First admin is bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD` env vars; compose refuses to start without both set. See [`docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md`](superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md) for the design rationale.
### 11.3 Single-host "production" ### 11.3 Single-host "production"
@@ -714,7 +809,7 @@ For a single operator on a single machine, the dev deployment IS the production
The following are explicitly NOT built in v1, by design: The following are explicitly NOT built in v1, by design:
- **Multi-user / multi-host** — login is required (single-user model today; SP23 adds RBAC). LAN-bind, Docker, reverse proxy still out of scope. (SP23 covers the LAN-bind / Docker / RBAC product fork.) - **Multi-user / multi-host** — login is required (single-user model today; RBAC is in place per SP23 for admin / user / viewer). LAN-bind + Docker + reverse proxy + remote multi-user is the v2 model (SP23 covers LAN-bind / Docker / RBAC).
- **Other trading partners** — only Colorado Medicaid via Gainwell is configured. The `PayerConfig` mechanism (SP9) supports adding more; the spec/plan/CLI work for that is on the backlog. - **Other trading partners** — only Colorado Medicaid via Gainwell is configured. The `PayerConfig` mechanism (SP9) supports adding more; the spec/plan/CLI work for that is on the backlog.
- **837I / 837D / 834 / 820 / 275 / 278 / 276-277** — only 837P and 835 are first-class. Other transaction types are listed in [REQUIREMENTS.md §6.2](REQUIREMENTS.md) as backlog items. - **837I / 837D / 834 / 820 / 275 / 278 / 276-277** — only 837P and 835 are first-class. Other transaction types are listed in [REQUIREMENTS.md §6.2](REQUIREMENTS.md) as backlog items.
- **NPPES real-time lookup** — only local NPI checksum validation (SP20). The operator who wants real registry lookup has to wire it in later. - **NPPES real-time lookup** — only local NPI checksum validation (SP20). The operator who wants real registry lookup has to wire it in later.
@@ -758,11 +853,30 @@ All in `docs/superpowers/specs/`:
- SP20 NPI validation: `2026-06-21-cyclone-npi-validation-design.md` - SP20 NPI validation: `2026-06-21-cyclone-npi-validation-design.md`
- SP21 store split: `2026-06-21-cyclone-store-split-design.md`; universal drilldown: `2026-06-21-cyclone-universal-drilldown-design.md` - SP21 store split: `2026-06-21-cyclone-store-split-design.md`; universal drilldown: `2026-06-21-cyclone-universal-drilldown-design.md`
- SP22 parse-decide: `2026-06-21-cyclone-parse-decide-workflow-design.md`; pipeline agent: `2026-06-21-cyclone-pipeline-agent-design.md` - SP22 parse-decide: `2026-06-21-cyclone-parse-decide-workflow-design.md`; pipeline agent: `2026-06-21-cyclone-pipeline-agent-design.md`
- SP23 (fork) Ubuntu + Docker: `2026-06-22-cyclone-ubuntu-docker-deployment-design.md` - SP23 Ubuntu + Docker + auth + RBAC + LAN-bind (shipped): `2026-06-22-cyclone-ubuntu-docker-deployment-design.md`
- SP24 reissue-claims: `2026-07-08-cyclone-reissue-claims-design.md`
- SP25 SFTP polling enablement: `2026-06-24-cyclone-sftp-polling-enablement-design.md`
- SP25 ack live-tail: `2026-07-02-cyclone-ack-live-tail-design.md`
- SP25 orphan data recovery: `2026-07-07-cyclone-orphan-data-recovery-design.md`
- SP26 SFTP password file: `2026-06-24-cyclone-sftp-password-file-companion-design.md`
- SP27 remittances architecture refactor: `2026-06-29-cyclone-remittances-architecture-refactor-design.md`
- SP28 ack-claim auto-link: `2026-07-02-cyclone-ack-claim-auto-link-design.md`
- SP29 Inbox 999-rejected drill: `2026-07-02-cyclone-999-rejected-drill-design.md`
- SP30 Dashboard Recent batches widget: `2026-07-02-cyclone-dashboard-recent-batches-design.md`
- SP31 835 strict content match: `2026-07-02-cyclone-835-strict-content-match-design.md`
- SP32 rendering + service-provider NPI extraction: `2026-07-02-cyclone-rendering-npi-extraction-design.md`
- SP33 CO TXIX payer fix: `2026-07-02-cyclone-co-txix-payer-fix-design.md`
- SP35 parse input guards: `2026-07-06-cyclone-parse-input-guards-design.md`
- SP36 api routers split: `2026-07-06-cyclone-api-routers-split-design.md`
- SP37 submit-batch canonical flow: `2026-07-07-cyclone-submit-batch-canonical-flow-design.md`
- SP38 orphan-ack housekeeping: `2026-07-07-cyclone-orphan-ack-housekeeping-design.md`
- SP39 2010BB NM109 fix: `2026-07-07-cyclone-2010bb-nm109-fix-design.md`
- SP40 Edifabric validation gate: `2026-07-07-cyclone-edifabric-validation-gate-design.md`
- SP41 in-window rebill pipeline: `2026-07-07-cyclone-inwindow-rebill-pipeline-design.md`
### 13.3 Plans ### 13.3 Plans
All in `docs/superpowers/plans/`. 27 files — one per spec (plus the round-2 backfill for SP9SP20 in the `2026-06-23-cyclone-sp*` naming). All in `docs/superpowers/plans/`. 37 files — one per shipped spec. SP34 (`999-acceptance-restore`) was de-scoped; SP31's `pcn-exact` content-match predicate supersedes the underlying race.
### 13.4 Reference docs ### 13.4 Reference docs
+76 -51
View File
@@ -25,7 +25,7 @@
Every requirement (§3, §4) has an ID; every ID is traceable to a sub-project (§6) which in turn points at a design spec and an implementation plan. The matrix at the end of §6 is the single index that ties everything together. Every requirement (§3, §4) has an ID; every ID is traceable to a sub-project (§6) which in turn points at a design spec and an implementation plan. The matrix at the end of §6 is the single index that ties everything together.
**Round history.** Round 1 (2026-06-23) produced this draft. Two independent reviewers (Reviewer A — components/data-model/deps/DoD; Reviewer B — traceability/consistency audit) read it against the actual codebase and surfaced 12 factual errors and 6 structural improvements. Round 2 (this version) applies every factual correction. Round 3 (pending) will re-verify reviewer agreement on the corrected doc. **Round history.** Round 1 (2026-06-23) produced this draft. Two independent reviewers (Reviewer A — components/data-model/deps/DoD; Reviewer B — traceability/consistency audit) read it against the actual codebase and surfaced 12 factual errors and 6 structural improvements. Round 2 (this version, dated 2026-07-08) applies every factual correction through SP41, including the post-SP23 doc alignment (LAN-bind + auth posture) and the SP21 store split + SP36 api split + SP41 rebill additions. Round 3 (pending) will re-verify reviewer agreement on the corrected doc.
--- ---
@@ -33,18 +33,18 @@ Every requirement (§3, §4) has an ID; every ID is traceable to a sub-project (
A self-hosted EDI claims-management suite for a single billing office. Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 and 005010X221A1), with a local-only FastAPI backend and a React UI for browsing, filtering, and inspecting the parsed data. A self-hosted EDI claims-management suite for a single billing office. Parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 and 005010X221A1), with a local-only FastAPI backend and a React UI for browsing, filtering, and inspecting the parsed data.
- **Local-only on purpose:** binds `127.0.0.1`, requires login (auth boundary is HTTP; bcrypt + HttpOnly session cookie), no internet exposure, single operator, single machine, one trading partner (Colorado Medicaid, currently). - **LAN-only by design:** binds `0.0.0.0:8000`, requires login (auth boundary is HTTP; bcrypt + HttpOnly session cookie; first admin bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD` env vars), reachability controlled by the host firewall / compose port publishing, single operator, one trading partner (Colorado Medicaid, currently). The dev/test escape hatch is `CYCLONE_AUTH_DISABLED=1`, which logs a WARNING at boot.
- **Single-process:** the backend is one Python process; the frontend is one Vite-served React app; there is no message broker, no separate worker, no queue. - **Single-process:** the backend is one Python process; the frontend is one Vite-served React app; there is no message broker, no separate worker, no queue.
- **Single source of truth:** SQLite at `~/.local/share/cyclone/cyclone.db` (overridable via `CYCLONE_DB_URL`); optionally encrypted at rest via SQLCipher (SP12). - **Single source of truth:** SQLite at `~/.local/share/cyclone/cyclone.db` (overridable via `CYCLONE_DB_URL`); optionally encrypted at rest via SQLCipher (SP12).
- **Threat model:** a process on the same host, plus a deliberate HTTP-layer login (no remote unauthenticated read or write). File-system threats (stolen/imaged drive) are still the primary concern; SQLCipher at rest + the macOS Keychain handle those. The SP23 product fork changes the threat model to "remote operator on LAN." - **Threat model:** a process on the same host, plus a deliberate HTTP-layer login (no remote unauthenticated read or write). File-system threats (stolen/imaged drive) are still the primary concern; SQLCipher at rest + the macOS Keychain handle those. SP23 expanded the threat model to "remote operator on LAN" by adding login + RBAC + Docker + Ubuntu packaging; the LAN-bind posture remains.
The Cyclone codebase has honored this contract end-to-end across 22 shipped sub-projects. Every design spec re-asserts the local-only / single-operator / single-payer / no-auth posture and lists (often at length) what is intentionally out of scope. The Cyclone codebase has honored this contract end-to-end across 41 shipped sub-projects. Every design spec re-asserts the LAN-only / single-operator / single-payer posture and lists (often at length) what is intentionally out of scope.
--- ---
## 2. Scope boundaries ## 2. Scope boundaries
### 2.1 In scope (today, after SP1SP22) ### 2.1 In scope (today, after SP1SP41)
| Capability | SP | | Capability | SP |
|---|---| |---|---|
@@ -69,9 +69,16 @@ The Cyclone codebase has honored this contract end-to-end across 22 shipped sub-
| Structured JSON logging + PII scrubber | SP18 | | Structured JSON logging + PII scrubber | SP18 |
| Security hardening (body size limit, rate limit, security headers) | SP19 | | Security hardening (body size limit, rate limit, security headers) | SP19 |
| NPI Luhn checksum + Tax ID format validation | SP20 | | NPI Luhn checksum + Tax ID format validation | SP20 |
| CycloneStore split (refactor; **in-flight on `refactor/store-split`**) | SP21 | | CycloneStore split (16-module subpackage; public API preserved via `cyclone.store` facade) | SP21 |
| Parse-then-decide upload dedup (idempotency) | SP22 | | Parse-then-decide upload dedup (idempotency) | SP22 |
| Pipeline automation agent (sibling project at `cyclone-pipeline/`) | SP22 | | Pipeline automation agent (sibling project at `cyclone-pipeline/`) | SP22 |
| HTTP-layer auth (bcrypt + HttpOnly session cookie) + RBAC roles + `CYCLONE_AUTH_DISABLED` escape hatch | SP23 |
| Ubuntu + Docker + nginx + tini packaging | SP23 |
| Audit-log `user_id` (every audit event records the acting user) | SP-auth |
| `submission_dedup` table + claim-id dedup at SFTP pre-flight | SP22 + SP41 |
| Resubmissions table + bulk ingest helpers | SP21 (resubmissions) |
| In-window rebill pipeline (visit↔835 reconciliation + Pipeline A/B + summary CSV) | SP41 |
| Live-tail NDJSON streams for `/api/acks/stream` and `/api/ta1-acks/stream` | SP-acks-tail |
### 2.2 Out of scope (today, by design) ### 2.2 Out of scope (today, by design)
@@ -140,8 +147,8 @@ Each requirement is `FR-NN`, traceable to one or more sub-projects. Verification
| FR-32 | Reject oversize request bodies (413), rate-limit requests (429), and add default security headers via pure-ASGI middlewares; emit a tamper-evident `api.request_rejected` audit event on 413/429. | SP19 | | FR-32 | Reject oversize request bodies (413), rate-limit requests (429), and add default security headers via pure-ASGI middlewares; emit a tamper-evident `api.request_rejected` audit event on 413/429. | SP19 |
| FR-33 | Surface a rich `GET /api/health` response — DB connectivity, MFT scheduler state, backup scheduler state, live pubsub subscriber counts, last batch id + timestamp. | SP19 (`api_routers/health.py:28-40`) | | FR-33 | Surface a rich `GET /api/health` response — DB connectivity, MFT scheduler state, backup scheduler state, live pubsub subscriber counts, last batch id + timestamp. | SP19 (`api_routers/health.py:28-40`) |
| FR-34 | Validate NPI with the CMS-published Luhn over `80840 + body` (warning-only — placeholder NPIs in test fixtures shouldn't block ingest). Reject Tax ID (EIN) with reserved prefixes (`00`, `07`, `80``89`). | SP20 | | FR-34 | Validate NPI with the CMS-published Luhn over `80840 + body` (warning-only — placeholder NPIs in test fixtures shouldn't block ingest). Reject Tax ID (EIN) with reserved prefixes (`00`, `07`, `80``89`). | SP20 |
| FR-35 | Split `CycloneStore` into per-concern modules (`store_persistence`, `store_reconcile`, `store_queries`, `store_mappers`) without changing the public store API. | SP21 (**in-flight** on `refactor/store-split` branch) | | FR-35 | Split `CycloneStore` into per-concern modules under `cyclone/store/` (16 modules, ~5,400 LOC total) without changing the public store API; the `cyclone.store` import surface still resolves to the same names. | SP21 (**Met**; merged on `main`; module list in §5.1) |
| FR-36 | De-duplicate 837 / 835 uploads via a parse-then-decide workflow (`find_existing_batch_for_claim` / `find_existing_batch_for_remit`); relax the claims/remits PK to `(batch_id, id)` via migration 0014 to support re-parses into different batches. | SP22 (migration 0014 in worktree; not yet on `main`) | | FR-36 | De-duplicate 837 / 835 uploads via a parse-then-decide workflow (`find_existing_batch_for_claim` / `find_existing_batch_for_remit`); relax the claims unique constraint via migration 0015 (a defensive drop of an inline UNIQUE that was a no-op against the current schema) to support re-parses into different batches. | SP22 (**Met**; migration 0015 on `main`) |
| FR-37 | (Sibling project, not in this repo) Drive the full 7-phase round-trip — preflight → browser upload → parse verify → SFTP submit → TA1 wait → 999 wait → scan + report — with crash-safe resume, structured JSON logging, idempotency dedup, per-run folder. | SP22 (`cyclone-pipeline/`) | | FR-37 | (Sibling project, not in this repo) Drive the full 7-phase round-trip — preflight → browser upload → parse verify → SFTP submit → TA1 wait → 999 wait → scan + report — with crash-safe resume, structured JSON logging, idempotency dedup, per-run folder. | SP22 (`cyclone-pipeline/`) |
| FR-38 | Provide a `DrillStackProvider` + `DrillDrawerHeader` shell so every drillable cell across the app opens a consistent drawer or peek modal. | SP21 | | FR-38 | Provide a `DrillStackProvider` + `DrillDrawerHeader` shell so every drillable cell across the app opens a consistent drawer or peek modal. | SP21 |
@@ -164,7 +171,7 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| ID | NFR | Source / SP | | ID | NFR | Source / SP |
|---|---|---| |---|---|---|
| NFR-1 | **Always bind 0.0.0.0 + auth.** Backend binds `0.0.0.0:8000` (overridable via `CYCLONE_HOST` / `CYCLONE_PORT`); CORS allowlist is exact (`http://localhost:5173`); login required (bcrypt + HttpOnly session cookie; first admin bootstrapped from env vars `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`); dev/test escape hatch via `CYCLONE_AUTH_DISABLED=1` (logs WARNING at boot). Reachability is controlled by the host firewall / compose port publishing, not the bind address. | SP1 + auth (2026-06-23 merge) + SP24 (doc reconciliation) | | NFR-1 | **Always bind 0.0.0.0 + auth.** Backend binds `0.0.0.0:8000` (overridable via `CYCLONE_HOST` / `CYCLONE_PORT`); CORS allowlist is exact (`http://localhost:5173`); login required (bcrypt + HttpOnly session cookie; first admin bootstrapped from env vars `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`); every API router declares `Depends(matrix_gate)` (`cyclone/auth/deps.py:matrix_gate`) so requests without a valid session are rejected; dev/test escape hatch via `CYCLONE_AUTH_DISABLED=1` (logs WARNING at boot). Reachability is controlled by the host firewall / compose port publishing, not the bind address. | SP1 + SP23 (auth + RBAC) |
| NFR-2 | **Determinism.** Parser / serializer round-trip is guaranteed on 113 real prodfiles (`docs/prodfiles/claims/*.x12`); canonical fields, not byte-identity (called out in the SP8 spec). | SP8 | | NFR-2 | **Determinism.** Parser / serializer round-trip is guaranteed on 113 real prodfiles (`docs/prodfiles/claims/*.x12`); canonical fields, not byte-identity (called out in the SP8 spec). | SP8 |
| NFR-3 | **Audit completeness.** Every reconciliation anomaly, every state transition, every 999/277CA reject writes an `ActivityEvent` (in `activity_events` table, un-chained; powers the `/activity` feed and inbox state transitions). | SP2 + SP10 | | NFR-3 | **Audit completeness.** Every reconciliation anomaly, every state transition, every 999/277CA reject writes an `ActivityEvent` (in `activity_events` table, un-chained; powers the `/activity` feed and inbox state transitions). | SP2 + SP10 |
| NFR-4 | **Tamper-evidence.** A separate `audit_log` table (SP11) carries SHA-256 hash-chained rows for security-sensitive events (login attempts, rejections, key rotations, backup lifecycle, rejected requests). `GET /api/admin/audit-log/verify` detects any break. **The `activity_events` (NFR-3) and `audit_log` (NFR-4) tables are distinct** — different semantics, different audiences. New code that needs an audit row must decide which one to write to. | SP11 | | NFR-4 | **Tamper-evidence.** A separate `audit_log` table (SP11) carries SHA-256 hash-chained rows for security-sensitive events (login attempts, rejections, key rotations, backup lifecycle, rejected requests). `GET /api/admin/audit-log/verify` detects any break. **The `activity_events` (NFR-3) and `audit_log` (NFR-4) tables are distinct** — different semantics, different audiences. New code that needs an audit row must decide which one to write to. | SP11 |
@@ -178,7 +185,7 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| NFR-12 | **Idempotency.** Inbound MFT files are de-duplicated via `processed_inbound_files`; 837/835 uploads are de-duplicated via the parse-then-decide workflow with `(batch_id, claim_id)` / `(batch_id, remit_id)` relaxed PKs. | SP16 + SP22 | | NFR-12 | **Idempotency.** Inbound MFT files are de-duplicated via `processed_inbound_files`; 837/835 uploads are de-duplicated via the parse-then-decide workflow with `(batch_id, claim_id)` / `(batch_id, remit_id)` relaxed PKs. | SP16 + SP22 |
| NFR-13 | **Crash-safety.** Incoming files are processed inside per-file try/except so a bad file doesn't stop the MFT loop; the pipeline agent has crash-safe resume keyed by run id. | SP16 + SP22 | | NFR-13 | **Crash-safety.** Incoming files are processed inside per-file try/except so a bad file doesn't stop the MFT loop; the pipeline agent has crash-safe resume keyed by run id. | SP16 + SP22 |
| NFR-14 | **Single-process.** No message broker, no separate worker, no queue; everything runs in one Python process. | (project-wide constraint) | | NFR-14 | **Single-process.** No message broker, no separate worker, no queue; everything runs in one Python process. | (project-wide constraint) |
| NFR-15 | **Test density.** Backend tests: **964 collected** (`pytest --collect-only`, 2026-06-23); frontend test files: **73**; prodfiles are exercised (113 + 19 + 1369), not just minimal fixtures. | (project-wide) | | NFR-15 | **Test density.** Backend tests: **1,624 collected** (`pytest --collect-only`, 2026-07-08); frontend test files: **85**; prodfiles are exercised (113 + 19 + 1369), not just minimal fixtures. | (project-wide) |
| NFR-16 | **Documentation discipline.** Every shipped sub-project has a design spec + implementation plan + smoke test; a new engineer can read the project top-to-bottom. (SPs 916 ship without on-disk plan files — see §6.1 — but their `feat(spN)` commits are on `main` and carry their own commit-message plan.) | (project-wide) | | NFR-16 | **Documentation discipline.** Every shipped sub-project has a design spec + implementation plan + smoke test; a new engineer can read the project top-to-bottom. (SPs 916 ship without on-disk plan files — see §6.1 — but their `feat(spN)` commits are on `main` and carry their own commit-message plan.) | (project-wide) |
| NFR-17 | **Fail-soft posture.** Reconciliation crashes don't lose the 835; the activity event records the failure. | SP2 | | NFR-17 | **Fail-soft posture.** Reconciliation crashes don't lose the 835; the activity event records the failure. | SP2 |
| NFR-18 | **Migration safety.** Migrations are forward-only via `PRAGMA user_version`; rollback procedures are documented per migration; the `user_version` runner is idempotent. **No checksum manifest exists today** (R-13 in §11). | SP2 + SP22 | | NFR-18 | **Migration safety.** Migrations are forward-only via `PRAGMA user_version`; rollback procedures are documented per migration; the `user_version` runner is idempotent. **No checksum manifest exists today** (R-13 in §11). | SP2 + SP22 |
@@ -193,14 +200,16 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| Module | Owns | Notes | | Module | Owns | Notes |
|---|---|---| |---|---|---|
| `cyclone.api` | HTTP routes, content negotiation, CORS allowlist, lifespan | 3,145 LOC (largest single file; partly split into `api_routers/` for 4 sub-routes) | | `cyclone.api` | HTTP routes, content negotiation, CORS allowlist, lifespan | 378 LOC; routes are mounted from `api_routers/` (22 sub-routers — see below) |
| `cyclone.api_helpers` | NDJSON / content-negotiation / live-tail helpers | | | `cyclone.api_helpers` | NDJSON / content-negotiation / live-tail helpers | |
| `cyclone.api_routers` | `acks`, `admin`, `health` (`/api/health`), `ta1_acks` sub-routers | Mounted at `api.py:270-273`; only 4 routes split out — bulk of routes still in `api.py` | | `cyclone.api_routers` | Per-resource sub-routers mounted under `/api/...` | 22 modules: `acks`, `activity`, `admin`, `batches`, `claim_acks`, `claims`, `clearhouse`, `config`, `dashboard`, `eligibility`, `health`, `inbox`, `parse`, `payers`, `providers`, `rebill`, `reconciliation`, `remittances`, `submission`, `ta1_acks` (+ `_shared.py`). Every router declares `Depends(matrix_gate)` for auth. |
| `cyclone.store` | Public store facade; persistence, reconciliation, queries, mappers | 2,172 LOC; SP21 split is in-flight on `refactor/store-split` | | `cyclone.auth` | Login, logout, session cookie, RBAC roles, `matrix_gate` dependency | New in SP23; password hashing, session table, user roles |
| `cyclone.handlers` | Shared FastAPI handlers (auth-aware JSON responses, audit-log injection, etc.) | Cross-cutting request glue |
| `cyclone.store` (subpackage) | Public store facade; the split modules behind it | 16 modules (`__init__` facade + `acks`, `backfill`, `backups`, `batches`, `claim_acks`, `claim_detail`, `exceptions`, `inbox`, `kpis`, `orm_builders`, `providers`, `records`, `resubmissions`, `submission_dedup`, `ui`, `write`); public API re-exported from `cyclone.store` so callers see no breaking change |
| `cyclone.db` | SQLAlchemy engine, session factory, ORM models (18 tables) | | | `cyclone.db` | SQLAlchemy engine, session factory, ORM models (18 tables) | |
| `cyclone.db_migrate` | `PRAGMA user_version` migration runner | | | `cyclone.db_migrate` | `PRAGMA user_version` migration runner | |
| `cyclone.db_crypto` | Optional SQLCipher encryption at rest; key rotation | `rotate_key()` is destructive (rewrites every page) | | `cyclone.db_crypto` | Optional SQLCipher encryption at rest; key rotation | `rotate_key()` is destructive (rewrites every page) |
| `cyclone.audit_log` | Hash-chained `audit_log` (SP11, SHA-256 chain) | Distinct from `cyclone.activity_events` written by other modules | | `cyclone.audit_log` | Hash-chained `audit_log` (SP11, SHA-256 chain) | Distinct from `cyclone.activity_events` written by other modules; post-SP23 records `user_id` of the actor |
| `cyclone.inbox_lanes` | The 5-lane inbox payload (Rejected / Payer-rejected / Candidates / Unmatched / Done today) | | | `cyclone.inbox_lanes` | The 5-lane inbox payload (Rejected / Payer-rejected / Candidates / Unmatched / Done today) | |
| `cyclone.inbox_state` | 999 envelope reject → claim state transitions | | | `cyclone.inbox_state` | 999 envelope reject → claim state transitions | |
| `cyclone.inbox_state_277ca` | 277CA STC A4/A6/A7 → payer_rejected stamp (monotonic) | | | `cyclone.inbox_state_277ca` | 277CA STC A4/A6/A7 → payer_rejected stamp (monotonic) | |
@@ -210,16 +219,19 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| `cyclone.reconcile` | Pure-function 835→claim match + line-level match | No DB session inside; testable with fabricated ORM objects | | `cyclone.reconcile` | Pure-function 835→claim match + line-level match | No DB session inside; testable with fabricated ORM objects |
| `cyclone.scoring` | 4-field weighted candidate scoring (40/25/20/15) | Weights hardcoded — see R-10 in §11 | | `cyclone.scoring` | 4-field weighted candidate scoring (40/25/20/15) | Weights hardcoded — see R-10 in §11 |
| `cyclone.pubsub` | In-process EventBus (drop-oldest, per-kind fan-out) | In-process only; not thread-safe across asyncio event loops | | `cyclone.pubsub` | In-process EventBus (drop-oldest, per-kind fan-out) | In-process only; not thread-safe across asyncio event loops |
| `cyclone.backup` | AES-256-GCM encrypted backups (orchestration + CLI) | 216 LOC | | `cyclone.backup` | AES-256-GCM encrypted backups (orchestration + CLI) | |
| `cyclone.backup_service` | Backup lifecycle (create / verify / restore) | 740 LOC | | `cyclone.backup_service` | Backup lifecycle (create / verify / restore) | |
| `cyclone.backup_scheduler` | 24h backup autostart loop | 315 LOC | | `cyclone.backup_scheduler` | 24h backup autostart loop | |
| `cyclone.scheduler` | asyncio MFT polling loop (SP16) | Uses `processed_inbound_files` for idempotency | | `cyclone.scheduler` | asyncio MFT polling loop (SP16) | Uses `processed_inbound_files` for idempotency |
| `cyclone.security` | Per-IP rate-limit, body-size limit, security headers (`SecurityHeadersMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`) | `EXEMPT_PATHS` list at `:201` | | `cyclone.security` | Per-IP rate-limit, body-size limit, security headers (`SecurityHeadersMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`) | `EXEMPT_PATHS` list at `:201` |
| `cyclone.logging_config` | JSON formatter, PII scrubber | | | `cyclone.logging_config` | JSON formatter, PII scrubber | |
| `cyclone.npi` | NPI Luhn + EIN format validators | | | `cyclone.npi` | NPI Luhn + EIN format validators | |
| `cyclone.batch_diff` | Batch Diff engine (powers `GET /api/batch-diff`) | 12 public functions/classes | | `cyclone.batch_diff` | Batch Diff engine (powers `GET /api/batch-diff`) | 12 public functions/classes |
| `cyclone.clearhouse` (`__init__.py`) | `SftpClient` (SP9 stub + SP13 paramiko), `InboundFile`, `make_client()` factory | 318 LOC; the SFTP home | | `cyclone.clearhouse` (`__init__.py`) | `SftpClient` (SP9 stub + SP13 paramiko), `InboundFile`, `make_client()` factory | The SFTP home |
| `cyclone.edi` (`filenames.py`) | HCPF X12 File Naming Standards helpers | Used by SP13/SP16 outbound filename construction | | `cyclone.edi` (`filenames.py`) | HCPF X12 File Naming Standards helpers | Used by SP13/SP16 outbound filename construction |
| `cyclone.rebill` | In-window rebill pipeline: visit↔835 reconciliation, CARC filter, timely-filing gate, Pipeline A/B, summary CSV | New in SP41 |
| `cyclone.reissue` | Reissue-claims pipeline (revised-claim regeneration + RUNBOOK entry) | SP24 |
| `cyclone.submission` | Canonical `submit_file` helper shared by `cyclone submit-batch` CLI and `POST /api/submit-batch`; parse → DB-write → SFTP-upload → audit per file | SP37 |
| `cyclone.cli` | Click CLI | | | `cyclone.cli` | Click CLI | |
| `cyclone.__main__` | `python -m cyclone serve` | | | `cyclone.__main__` | `python -m cyclone serve` | |
| `cyclone.parsers/` | X12 tokenizer, models, validator, writers, 277CA, 999, TA1, 270, 271, CAS, seg | 25 `.py` modules | | `cyclone.parsers/` | X12 tokenizer, models, validator, writers, 277CA, 999, TA1, 270, 271, CAS, seg | 25 `.py` modules |
@@ -230,7 +242,7 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| Directory | Owns | | Directory | Owns |
|---|---| |---|---|
| `src/pages/` | Acks, ActivityLog, BatchDiff, Batches, Claims, Dashboard, Inbox, Providers, Reconciliation, Remittances, Upload (11 pages) | | `src/pages/` | Acks, ActivityLog, BatchDiff, Batches, Claims, Dashboard, Inbox, Login, Providers, Reconciliation, Remittances, Upload (12 pages — Login added in SP23) |
| `src/hooks/` | TanStack Query hooks, URL-state hooks, live-tail hooks, keyboard hooks, batch-export, search, drawer keyboard, count-up, provider detail, payer summary, parse, ack detail (35+ files) | | `src/hooks/` | TanStack Query hooks, URL-state hooks, live-tail hooks, keyboard hooks, batch-export, search, drawer keyboard, count-up, provider detail, payer summary, parse, ack detail (35+ files) |
| `src/lib/` | `api.ts`, `format.ts`, `utils.ts`, `csv.ts`, `download.ts`, `event-routing.ts`, `tail-stream.ts`, `inbox-api.ts` (all with paired `.test.ts`) | | `src/lib/` | `api.ts`, `format.ts`, `utils.ts`, `csv.ts`, `download.ts`, `event-routing.ts`, `tail-stream.ts`, `inbox-api.ts` (all with paired `.test.ts`) |
| `src/components/` | 3 drawer families (`ClaimDrawer/` ~15 files, `RemitDrawer/` ~10 files, `ProviderDrawer/` ~6, `AckDrawer/` 3); `inbox/` (10 files), `drill/` (10 files, includes `DrillStackProvider` + `DrillDrawerHeader`), `charts/` (4 files), `ui/` (18 shadcn primitives — radix + cva + tailwind-merge), `KeyboardCheatsheet/` (3 files), plus top-level ActivityFeed, AnimatedNumber, BatchDetail, BatchDiffView, BatchesList, ClaimCard837, DominantKpiCard, EditorialNote, ExportBar, ExportCsvButton, KpiCard, Layout, NewClaimDialog, PageHeader, SearchBar, SearchResults, Sidebar, Sparkline, StatusBadge, StatusPill, TailStatusPill, TickerTape | | `src/components/` | 3 drawer families (`ClaimDrawer/` ~15 files, `RemitDrawer/` ~10 files, `ProviderDrawer/` ~6, `AckDrawer/` 3); `inbox/` (10 files), `drill/` (10 files, includes `DrillStackProvider` + `DrillDrawerHeader`), `charts/` (4 files), `ui/` (18 shadcn primitives — radix + cva + tailwind-merge), `KeyboardCheatsheet/` (3 files), plus top-level ActivityFeed, AnimatedNumber, BatchDetail, BatchDiffView, BatchesList, ClaimCard837, DominantKpiCard, EditorialNote, ExportBar, ExportCsvButton, KpiCard, Layout, NewClaimDialog, PageHeader, SearchBar, SearchResults, Sidebar, Sparkline, StatusBadge, StatusPill, TailStatusPill, TickerTape |
@@ -239,7 +251,7 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
### 5.2 Data model ### 5.2 Data model
**12 SQL migrations** in `backend/src/cyclone/migrations/` (verified on disk 2026-06-23): **23 SQL migrations** in `backend/src/cyclone/migrations/` (verified on disk 2026-07-08):
| # | File | Tables / columns added | | # | File | Tables / columns added |
|---|---|---| |---|---|---|
@@ -255,13 +267,17 @@ The `FR-RB-*` IDs are SP41-specific functional requirements for the in-window re
| 0010 | `0010_payer_rejected_acknowledged.sql` | `payer_rejected_acknowledged` column on `claims` | | 0010 | `0010_payer_rejected_acknowledged.sql` | `payer_rejected_acknowledged` column on `claims` |
| 0011 | `0011_processed_inbound_files.sql` | `processed_inbound_files` (SP16 idempotency) | | 0011 | `0011_processed_inbound_files.sql` | `processed_inbound_files` (SP16 idempotency) |
| 0012 | `0012_backups.sql` | `db_backups` (SP17) | | 0012 | `0012_backups.sql` | `db_backups` (SP17) |
| 0013 | `0013_auth_users_and_sessions.sql` | `users`, `sessions`, RBAC role columns (SP23 auth) |
**In-flight on `claims-unique-fix` worktree (not yet on `main`):** | 0014 | `0014_audit_log_user_id.sql` | `audit_log.user_id` column + index (records acting user on every audit event) |
| 0015 | `0015_drop_claims_unique_constraint.sql` | Defensive drop of the inline `UNIQUE(batch_id, patient_control_number)` on `claims` (SP22; table-recreate dance — no-op against current schema) |
| # | File | Tables / columns added | | 0016 | `0016_claims_matched_remittance_id_index.sql` | Index on `claims.matched_remittance_id` to speed inbox joins |
|---|---|---| | 0017 | `0017_backfill_claim_patient_control_number.sql` | Backfill `patient_control_number` from X12 CLM01 for legacy rows |
| 0013 | `0013_drop_claims_unique_constraint.sql` | Drop `claims` UNIQUE constraint | | 0018 | `0018_claim_acks.sql` | `claim_acks` table (claim-level ACK metadata) |
| 0014 | `0014_relax_claims_remits_pk.sql` | Relax PK to `(batch_id, id)` (SP22) | | 0019 | `0019_add_rendering_and_service_provider_npis.sql` | Rendering + service-provider NPI columns on `claims` |
| 0020 | `0020_add_batch_txn_set_control_number.sql` | `batches.transaction_set_control_number` (ST/SE control number persistence) |
| 0021 | `0021_resubmissions.sql` | `resubmissions` table (resubmission tracking + bulk-ingest helpers) |
| 0022 | `0022_submission_dedup.sql` | `submission_dedup` table (claim-id dedup window at SFTP pre-flight, SP41) |
| 0023 | `0023_visits.sql` | `visits` table (SP41 in-window rebill pipeline input) |
**ORM models** (declared in `backend/src/cyclone/db.py` — 18 classes total): **ORM models** (declared in `backend/src/cyclone/db.py` — 18 classes total):
@@ -417,18 +433,20 @@ Each row's "Spec" / "Plan" point to the design spec and implementation plan on d
| SP18 | Structured logging | [spec](superpowers/specs/2026-06-21-cyclone-structured-logging-design.md) | (no plan file on disk; feat(sp18) commits on `main`) | Met | | SP18 | Structured logging | [spec](superpowers/specs/2026-06-21-cyclone-structured-logging-design.md) | (no plan file on disk; feat(sp18) commits on `main`) | Met |
| SP19 | Security hardening + health probe | [spec](superpowers/specs/2026-06-21-cyclone-security-hardening-design.md) | (no plan file on disk; feat(sp19) commits on `main`) | Met | | SP19 | Security hardening + health probe | [spec](superpowers/specs/2026-06-21-cyclone-security-hardening-design.md) | (no plan file on disk; feat(sp19) commits on `main`) | Met |
| SP20 | NPI checksum + Tax ID format validation | [spec](superpowers/specs/2026-06-21-cyclone-npi-validation-design.md) | (no plan file on disk; feat(sp20) commits on `main`) | Met | | SP20 | NPI checksum + Tax ID format validation | [spec](superpowers/specs/2026-06-21-cyclone-npi-validation-design.md) | (no plan file on disk; feat(sp20) commits on `main`) | Met |
| SP21 | CycloneStore split (in-flight) + Universal drilldown (shipped) | [split spec](superpowers/specs/2026-06-21-cyclone-store-split-design.md), [drilldown spec](superpowers/specs/2026-06-21-cyclone-universal-drilldown-design.md) | [store-split plan](superpowers/plans/2026-06-21-cyclone-store-split.md), [drilldown plan](superpowers/plans/2026-06-21-cyclone-universal-drilldown.md) | Drilldown **Met**; split **In-flight** (`refactor/store-split` branch, not yet on `main`) | | SP21 | CycloneStore split (16-module subpackage) + Universal drilldown | [split spec](superpowers/specs/2026-06-21-cyclone-store-split-design.md), [drilldown spec](superpowers/specs/2026-06-21-cyclone-universal-drilldown-design.md) | [store-split plan](superpowers/plans/2026-06-21-cyclone-store-split.md), [drilldown plan](superpowers/plans/2026-06-21-cyclone-universal-drilldown.md) | Met (split landed on `main`; public API re-exported from `cyclone.store` facade; drilldown used by every drawer) |
| SP22 | Parse-decide workflow + Pipeline agent | [parse-decide spec](superpowers/specs/2026-06-21-cyclone-parse-decide-workflow-design.md), [pipeline spec](superpowers/specs/2026-06-21-cyclone-pipeline-agent-design.md) | [parse-decide plan](superpowers/plans/2026-06-21-cyclone-parse-decide-workflow.md), [pipeline plan](superpowers/plans/2026-06-21-cyclone-pipeline-agent.md) | Met (parse-decide on `main`; pipeline agent lives in `cyclone-pipeline/`) | | SP22 | Parse-decide workflow + Pipeline agent | [parse-decide spec](superpowers/specs/2026-06-21-cyclone-parse-decide-workflow-design.md), [pipeline spec](superpowers/specs/2026-06-21-cyclone-pipeline-agent-design.md) | [parse-decide plan](superpowers/plans/2026-06-21-cyclone-parse-decide-workflow.md), [pipeline plan](superpowers/plans/2026-06-21-cyclone-pipeline-agent.md) | Met (parse-decide on `main`; pipeline agent lives in `cyclone-pipeline/`) |
| SP24 | Auth posture alignment (docs-only reconciliation with the auth work in `main`) | [spec](superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md) | [plan](superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md) | Met (no code paths change; only docs + `__main__.py` WARNING + 3 skills addenda) | | SP23 | Ubuntu + Docker + nginx + tini + auth + RBAC (LAN-bind product fork) | [spec](superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md) | (plan in the same docs directory; auth is the dominant change) | Met (merges `cyclone.auth.*`, every API router gated by `matrix_gate`, Login page added to the React app) |
| SP24 | Reissue-claims (revised-claim regeneration + deprecation shim) | [spec](superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md) (docs alignment + reissue) | [plan](superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md) | Met (merge commit `c9588f7`; `cyclone.reissue` shipped) |
| SP25SP40 | Sub-projects landed between SP24 and SP41 (covering resubmissions, `claim_acks`, rendering/service NPI columns, transaction-set control numbers, additional live-tail streams, dashboards, rebill prerequisites) | (per-SP design specs + plans under `docs/superpowers/`) | (per-SP plans under `docs/superpowers/plans/`) | Met — see §6.1 acceptance table below |
| SP41 | In-window rebill pipeline (`cyclone.rebill.*`) | [spec](superpowers/specs/2026-07-07-cyclone-inwindow-rebill-pipeline-design.md) | [plan](superpowers/plans/2026-07-07-cyclone-inwindow-rebill-pipeline.md) | Met (merge commit `309e6c0`; visit↔835 reconciliation, Pipeline A/B, summary CSV, claim-id dedup at SFTP pre-flight) |
### 6.2 Backlog (specs awaiting plans, or vice versa) ### 6.2 Backlog (specs awaiting plans, or vice versa)
| Item | Status | Why | | Item | Status | Why |
|---|---|---| |---|---|---|
| SP23 (candidate) | Spec-only; **product fork awaiting user decision** | `2026-06-22-cyclone-ubuntu-docker-deployment-design.md` (auth + Docker + RBAC + LAN-bind); see §11 R-1, R-3 |
| Plan-file backfill for SP9, SP10, SP11, SP12, SP13, SP14, SP15, SP16, SP17, SP18, SP19, SP20 | Plan files missing on disk; `feat(spN)` commit messages substitute | Round-3 doc-backlog item | | Plan-file backfill for SP9, SP10, SP11, SP12, SP13, SP14, SP15, SP16, SP17, SP18, SP19, SP20 | Plan files missing on disk; `feat(spN)` commit messages substitute | Round-3 doc-backlog item |
| Spec-file backfill for SP6 (workflow/inbox), SP11 (audit log), SP12 (SQLCipher), SP13 (paramiko), SP15 (key rotation), SP16 (MFT scheduler) | Specs missing on disk | Round-3 doc-backlog item | | Spec-file backfill for SP6 (workflow/inbox), SP11 (audit log), SP12 (SQLCipher), SP13 (paramiko), SP15 (key rotation), SP16 (MFT scheduler) | Specs missing on disk | Round-3 doc-backlog item |
| Migration manifest (checksums for the 12 SQL files) | Open | Completeness review §3.1.18 | | Migration manifest (checksums for the 23 SQL files) | Open | Completeness review §3.1.18 |
| Tunable score weights (move 40/25/20/15 from code to config) | Open | Completeness review §3.1.15 | | Tunable score weights (move 40/25/20/15 from code to config) | Open | Completeness review §3.1.15 |
| License file (`LICENSE`) | Open | Completeness review §3.2.28 | | License file (`LICENSE`) | Open | Completeness review §3.2.28 |
| `pre-commit` / `Makefile` / `CONTRIBUTING.md` / `.editorconfig` / `ruff` config | Open | Completeness review §3.2.30 | | `pre-commit` / `Makefile` / `CONTRIBUTING.md` / `.editorconfig` / `ruff` config | Open | Completeness review §3.2.30 |
@@ -482,8 +500,8 @@ This matrix is the single index. Each row points to a requirement and where it's
| FR-32 | SP19 | security-hardening | (none on disk) | `test_security.py` | | FR-32 | SP19 | security-hardening | (none on disk) | `test_security.py` |
| FR-33 | SP19 | security-hardening | (none on disk) | `test_api.py:33 test_health_endpoint`, `test_security.py:204-221` (health snapshot) | | FR-33 | SP19 | security-hardening | (none on disk) | `test_api.py:33 test_health_endpoint`, `test_security.py:204-221` (health snapshot) |
| FR-34 | SP20 | npi-validation | (none on disk) | `test_npi.py`, `test_api_validate_provider.py`, `test_cli_validate.py` | | FR-34 | SP20 | npi-validation | (none on disk) | `test_npi.py`, `test_api_validate_provider.py`, `test_cli_validate.py` |
| FR-35 | SP21 | store-split | store-split | `test_store.py` (currently covers the un-split `store.py`; will cover the split modules once `refactor/store-split` lands) | | FR-35 | SP21 | store-split | store-split | `tests/store/test_*.py` (per-module coverage of the split subpackage); `test_store.py` continues to exercise the public `cyclone.store` facade |
| FR-36 | SP22 | parse-decide-workflow | parse-decide-workflow | `test_api_parse_persists.py`, `test_prodfiles_smoke.py`; migration 0014 in `claims-unique-fix` worktree | | FR-36 | SP22 | parse-decide-workflow | parse-decide-workflow | `test_api_parse_persists.py`, `test_prodfiles_smoke.py`, migration 0015 (defensive drop of inline UNIQUE on `claims(batch_id, patient_control_number)`) |
| FR-37 | SP22 | pipeline-agent | pipeline-agent | (lives in `cyclone-pipeline/`, not this repo) | | FR-37 | SP22 | pipeline-agent | pipeline-agent | (lives in `cyclone-pipeline/`, not this repo) |
| FR-38 | SP21 | universal-drilldown | universal-drilldown | `DrillStackProvider.test.tsx`, `useDrawerUrlState.test.ts`, plus per-drawer tests in `ClaimDrawer/`, `RemitDrawer/`, `ProviderDrawer/`, `AckDrawer/` | | FR-38 | SP21 | universal-drilldown | universal-drilldown | `DrillStackProvider.test.tsx`, `useDrawerUrlState.test.ts`, plus per-drawer tests in `ClaimDrawer/`, `RemitDrawer/`, `ProviderDrawer/`, `AckDrawer/` |
@@ -588,12 +606,12 @@ These are the existing per-SP acceptance checklists as captured in their design
```bash ```bash
# Backend # Backend
cd backend cd backend
.venv/bin/pytest # 964 tests collected (2026-06-23) .venv/bin/pytest # 1,624 tests collected (2026-07-08)
.venv/bin/python -m cyclone serve .venv/bin/python -m cyclone serve
# Frontend # Frontend
npm run typecheck npm run typecheck
npm test # 73 test files npm test # 85 test files
npm run build npm run build
# Smoke (planned in SP23) # Smoke (planned in SP23)
@@ -628,7 +646,7 @@ Recorded explicitly so a new engineer can challenge any of them.
| A8 | The X12 5010 transaction set Cyclone supports is sufficient for the operator's billing workflow. | Adding 837I / 837D / 278 / 276-277 / 277CA-round-trip / etc. is each a new SP. | | A8 | The X12 5010 transaction set Cyclone supports is sufficient for the operator's billing workflow. | Adding 837I / 837D / 278 / 276-277 / 277CA-round-trip / etc. is each a new SP. |
| A9 | Local-only bind is sufficient — no remote access needed. | Would require auth (SP23), TLS, reverse proxy, RBAC. | | A9 | Local-only bind is sufficient — no remote access needed. | Would require auth (SP23), TLS, reverse proxy, RBAC. |
| A10 | One Python process, no message broker, no separate worker. | A multi-worker deployment would require breaking thread-affinity (SP12/SP15 specifically). | | A10 | One Python process, no message broker, no separate worker. | A multi-worker deployment would require breaking thread-affinity (SP12/SP15 specifically). |
| A11 | 964 backend tests + 73 frontend test files is the right test density for this codebase (2026-06-23). If we add more transaction types or surface area, this needs to grow proportionally; coverage gates should be enforced (round-3 backlog). | New transaction types or surface area without test growth → coverage drifts down. | | A11 | 1,624 backend tests + 85 frontend test files is the right test density for this codebase (2026-07-08). If we add more transaction types or surface area, this needs to grow proportionally; coverage gates should be enforced (round-3 backlog). | New transaction types or surface area without test growth → coverage drifts down. |
| A12 | `cyclone-pipeline/` is a sibling project, not a sub-project of this repo. | The 7-phase round-trip agent is shipped in `cyclone-pipeline/`; if that project is abandoned, the manual operator workflow remains (upload via `/upload`, SFTP by hand, `check-835` not available). | | A12 | `cyclone-pipeline/` is a sibling project, not a sub-project of this repo. | The 7-phase round-trip agent is shipped in `cyclone-pipeline/`; if that project is abandoned, the manual operator workflow remains (upload via `/upload`, SFTP by hand, `check-835` not available). |
--- ---
@@ -639,9 +657,9 @@ Pulled forward from [`docs/reviews/2026-06-20-cyclone-completeness-review.md`](r
| ID | Risk / gap | Source | Status | | ID | Risk / gap | Source | Status |
|---|---|---|---| |---|---|---|---|
| R-1 | No app-layer auth (anyone on the host can read PHI). | Completeness review §3.1.4 + §3.2.25 | **Closed by SP24** — auth shipped via the merge of `origin/main` on 2026-06-23 (commits `a25504b`..`39ae988`, `cyclone.auth.*` + `matrix_gate` dependency on every router); SP24 reconciled the docs to match. SP23 still covers the LAN-bind / Docker / RBAC product fork. | | R-1 | No app-layer auth (anyone on the host can read PHI). | Completeness review §3.1.4 + §3.2.25 | **Closed by SP23** — auth shipped via the merge of `origin/main` on 2026-06-23 (commits `a25504b`..`39ae988`, `cyclone.auth.*` + `matrix_gate` dependency on every router); `users` / `sessions` tables live in migration 0013; every audit event records the acting `user_id` (migration 0014). |
| R-2 | SQLite plaintext if SQLCipher not enabled. | Completeness review §3.1.1 | **Partial** — SP12 + SP15 ship the encryption option, but it falls back to plain if the Keychain entry is missing. | | R-2 | SQLite plaintext if SQLCipher not enabled. | Completeness review §3.1.1 | **Partial** — SP12 + SP15 ship the encryption option, but it falls back to plain if the Keychain entry is missing. |
| R-3 | No Dockerfile / docker-compose.yml. | Completeness review §3.2.29 | **Open** — SP23 spec covers it but is awaiting user decision. | | R-3 | No Dockerfile / docker-compose.yml. | Completeness review §3.2.29 | **Closed by SP23** — Docker packaging is part of the LAN-bind product fork (Ubuntu base, `tini`, nginx reverse proxy, `docker-compose.yml`). |
| R-4 | PayerConfig still partly in code (`co_medicaid()` factory at `cyclone/parsers/payer.py:57-58`). | Completeness review §3.1.9 | **Open** — SP9 externalized config but the factory remains as a fallback for tests and default-payer selection. | | R-4 | PayerConfig still partly in code (`co_medicaid()` factory at `cyclone/parsers/payer.py:57-58`). | Completeness review §3.1.9 | **Open** — SP9 externalized config but the factory remains as a fallback for tests and default-payer selection. |
| R-5 | No vocabulary tables (ICD-10 / HCPCS / NDC). | Completeness review §3.1.11 | **Open** — explicitly out of scope per A6. | | R-5 | No vocabulary tables (ICD-10 / HCPCS / NDC). | Completeness review §3.1.11 | **Open** — explicitly out of scope per A6. |
| R-6 | No COB / secondary-claim generator. | Completeness review §3.1.12 | **Open** — explicitly out of scope per §2.2. | | R-6 | No COB / secondary-claim generator. | Completeness review §3.1.12 | **Open** — explicitly out of scope per §2.2. |
@@ -649,9 +667,9 @@ Pulled forward from [`docs/reviews/2026-06-20-cyclone-completeness-review.md`](r
| R-8 | No NPPES NPI lookup. | Completeness review §3.1.10 | **Open** — only Luhn checksum (SP20). | | R-8 | No NPPES NPI lookup. | Completeness review §3.1.10 | **Open** — only Luhn checksum (SP20). |
| R-9 | No 837I / 837D / 834 / 820 / 275 / 278 / 276-277 transaction types. | Completeness review §2.1 | **Open** — explicitly out of scope per §2.2. | | R-9 | No 837I / 837D / 834 / 820 / 275 / 278 / 276-277 transaction types. | Completeness review §2.1 | **Open** — explicitly out of scope per §2.2. |
| R-10 | Score weights hardcoded (`cyclone/scoring.py:42-46`). | Completeness review §3.1.15 | **Open** — not scheduled. | | R-10 | Score weights hardcoded (`cyclone/scoring.py:42-46`). | Completeness review §3.1.15 | **Open** — not scheduled. |
| R-11 | `cyclone/store.py` is a 2,172-line god-module. | Completeness review §3.1.17 | **In-flight** — SP21 spec + plan written; branch `refactor/store-split` exists; merge to `main` pending. | | R-11 | `cyclone/store.py` is a 2,172-line god-module. | Completeness review §3.1.17 | **Closed by SP21**`cyclone.store` is now a 16-module subpackage (`acks`, `backfill`, `backups`, `batches`, `claim_acks`, `claim_detail`, `exceptions`, `inbox`, `kpis`, `orm_builders`, `providers`, `records`, `resubmissions`, `submission_dedup`, `ui`, `write` + `__init__` facade); public API re-exported from `cyclone.store` so callers see no breaking change. |
| R-12 | `cyclone/api.py` is a 3,145-line god-module (larger than `store.py`). | Completeness review §3.1.19 | **Partial**`api_routers/` exists for 4 sub-routes (acks, admin, health, ta1_acks); bulk of routes still in `api.py`. | | R-12 | `cyclone/api.py` is a 3,145-line god-module (larger than `store.py`). | Completeness review §3.1.19 | **Closed by SP36**`api.py` is now 378 LOC; 22 sub-routers live under `cyclone/api_routers/` (every router gated by `Depends(matrix_gate)`). |
| R-13 | Migrations in flat dir without manifest. | Completeness review §3.1.18 | **Open** — no checksum list; 12 SQL files in `backend/src/cyclone/migrations/`. | | R-13 | Migrations in flat dir without manifest. | Completeness review §3.1.18 | **Open** — no checksum list; 23 SQL files in `backend/src/cyclone/migrations/`. |
| R-14 | No `pre-commit` / `Makefile` / `CONTRIBUTING.md` / `.editorconfig` / `ruff` config. | Completeness review §3.2.30 | **Open** — single-operator project. | | R-14 | No `pre-commit` / `Makefile` / `CONTRIBUTING.md` / `.editorconfig` / `ruff` config. | Completeness review §3.2.30 | **Open** — single-operator project. |
| R-15 | No license file (`LICENSE`). | Completeness review §3.2.28 | **Open** — not scheduled. | | R-15 | No license file (`LICENSE`). | Completeness review §3.2.28 | **Open** — not scheduled. |
| R-16 | PHI fixtures not flagged as PHI. | Completeness review §3.2.23 | **Open** — not scheduled. | | R-16 | PHI fixtures not flagged as PHI. | Completeness review §3.2.23 | **Open** — not scheduled. |
@@ -662,10 +680,11 @@ Pulled forward from [`docs/reviews/2026-06-20-cyclone-completeness-review.md`](r
| R-21 | No structured logging (before SP18). | Completeness review §3.1.5 | **Closed by SP18**. | | R-21 | No structured logging (before SP18). | Completeness review §3.1.5 | **Closed by SP18**. |
| R-22 | No backup automation (before SP17). | Completeness review §3.1.3 | **Closed by SP17**. | | R-22 | No backup automation (before SP17). | Completeness review §3.1.3 | **Closed by SP17**. |
| R-23 | `ActivityEvent` not tamper-evident (before SP11). | Completeness review §3.1.2 | **Closed by SP11** — but note: the tampered table is `audit_log`, distinct from `activity_events` (see NFR-3 / NFR-4). | | R-23 | `ActivityEvent` not tamper-evident (before SP11). | Completeness review §3.1.2 | **Closed by SP11** — but note: the tampered table is `audit_log`, distinct from `activity_events` (see NFR-3 / NFR-4). |
| R-24 | SP23 product fork (auth + Docker + RBAC + LAN-bind). | §6.2 | **Awaiting user decision** — see §2.2 and §11 R-1. | | R-24 | SP23 product fork (auth + Docker + RBAC + LAN-bind). | §6.1 | **Closed by SP23** — auth, RBAC, Docker, nginx, tini, LAN-bind all shipped on `main`. |
| R-25 | `vitest@^4.1.9` is unusually new (current stable line is 1.x/2.x). | §5.3 | **Open** — verify the version pin is intentional before the next `npm install`; fallback to `^2.x` if accidental. | | R-25 | `vitest@^4.1.9` is unusually new (current stable line is 1.x/2.x). | §5.3 | **Open** — verify the version pin is intentional before the next `npm install`; fallback to `^2.x` if accidental. |
| R-26 | No CI workflow (`.github/workflows/`). | §8.1 #3 | **Open** — release gating is manual today. | | R-26 | No CI workflow (`.github/workflows/`). | §8.1 #3 | **Open** — release gating is manual today. |
| R-27 | `backend/src/cyclone/workflow/__pycache__/` is dead bytecode. | §5.1 | **Open** — recommend deletion. | | R-27 | `backend/src/cyclone/workflow/__pycache__/` is dead bytecode. | §5.1 | **Open** — recommend deletion. |
| R-28 | Test density drifted from 964 → 1,624 backend tests and 73 → 85 frontend test files. | §4 NFR-15 | **Open** — coverage gates (`--cov-fail-under=90`) still not enforced; counts tracked in NFR-15. |
--- ---
@@ -694,7 +713,10 @@ All under [`docs/superpowers/specs/`](superpowers/specs/):
- [2026-06-21-cyclone-store-split-design.md](superpowers/specs/2026-06-21-cyclone-store-split-design.md) — SP21 (split) - [2026-06-21-cyclone-store-split-design.md](superpowers/specs/2026-06-21-cyclone-store-split-design.md) — SP21 (split)
- [2026-06-21-cyclone-structured-logging-design.md](superpowers/specs/2026-06-21-cyclone-structured-logging-design.md) — SP18 - [2026-06-21-cyclone-structured-logging-design.md](superpowers/specs/2026-06-21-cyclone-structured-logging-design.md) — SP18
- [2026-06-21-cyclone-universal-drilldown-design.md](superpowers/specs/2026-06-21-cyclone-universal-drilldown-design.md) — SP21 (drilldown) - [2026-06-21-cyclone-universal-drilldown-design.md](superpowers/specs/2026-06-21-cyclone-universal-drilldown-design.md) — SP21 (drilldown)
- [2026-06-22-cyclone-ubuntu-docker-deployment-design.md](superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md) — **SP23 candidate; awaiting plan + user product-fork decision** - [2026-06-22-cyclone-ubuntu-docker-deployment-design.md](superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md) — **SP23 (shipped)**: Ubuntu + Docker + nginx + tini + auth + RBAC LAN-bind product fork
- [2026-06-23-cyclone-auth-posture-alignment-design.md](superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md) — **SP24 (shipped)**: reissue-claims + auth docs alignment
- [2026-07-07-cyclone-inwindow-rebill-pipeline-design.md](superpowers/specs/2026-07-07-cyclone-inwindow-rebill-pipeline-design.md) — **SP41 (shipped)**: in-window rebill pipeline (`cyclone.rebill.*`)
- [2026-07-08-cyclone-doc-pass-design.md](superpowers/specs/2026-07-08-cyclone-doc-pass-design.md) — **SP42 (in flight)**: this doc-pass (post-SP23SP41 reconciliation)
### 12.2 Implementation plans ### 12.2 Implementation plans
@@ -713,8 +735,11 @@ All under [`docs/superpowers/plans/`](superpowers/plans/):
- [2026-06-21-cyclone-parse-decide-workflow.md](superpowers/plans/2026-06-21-cyclone-parse-decide-workflow.md) — SP22 - [2026-06-21-cyclone-parse-decide-workflow.md](superpowers/plans/2026-06-21-cyclone-parse-decide-workflow.md) — SP22
- [2026-06-21-cyclone-pipeline-agent.md](superpowers/plans/2026-06-21-cyclone-pipeline-agent.md) — SP22 - [2026-06-21-cyclone-pipeline-agent.md](superpowers/plans/2026-06-21-cyclone-pipeline-agent.md) — SP22
- [2026-06-21-cyclone-skill-catalog.md](superpowers/plans/2026-06-21-cyclone-skill-catalog.md) — documentation meta-project - [2026-06-21-cyclone-skill-catalog.md](superpowers/plans/2026-06-21-cyclone-skill-catalog.md) — documentation meta-project
- [2026-06-21-cyclone-store-split.md](superpowers/plans/2026-06-21-cyclone-store-split.md) — SP21 (split, in-flight) - [2026-06-21-cyclone-store-split.md](superpowers/plans/2026-06-21-cyclone-store-split.md) — SP21 (split, shipped)
- [2026-06-21-cyclone-universal-drilldown.md](superpowers/plans/2026-06-21-cyclone-universal-drilldown.md) — SP21 (drilldown) - [2026-06-21-cyclone-universal-drilldown.md](superpowers/plans/2026-06-21-cyclone-universal-drilldown.md) — SP21 (drilldown)
- [2026-06-23-cyclone-auth-posture-alignment.md](superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md) — SP24 (reissue-claims + auth docs alignment)
- [2026-07-07-cyclone-inwindow-rebill-pipeline.md](superpowers/plans/2026-07-07-cyclone-inwindow-rebill-pipeline.md) — SP41 (in-window rebill pipeline)
- [2026-07-08-cyclone-doc-pass.md](superpowers/plans/2026-07-08-cyclone-doc-pass.md) — SP42 (this doc-pass)
### 12.3 Reviews and reference ### 12.3 Reviews and reference
@@ -737,13 +762,13 @@ All under [`docs/superpowers/plans/`](superpowers/plans/):
For the two independent reviewers at the end of round 2 (re-verifying the corrections): For the two independent reviewers at the end of round 2 (re-verifying the corrections):
1. **Components** — Do §5.1 (backend modules) + §5.1 (frontend dirs) list every existing module that ships PHI or serves a state-changing route? Are the boundaries (DB / ORM / reconcile / store / api / pubsub / clearhouse / batch_diff / edi.filenames) drawn correctly? Can you, from §5.1 alone, draw the import graph? 1. **Components** — Do §5.1 (backend modules) + §5.1 (frontend dirs) list every existing module that ships PHI or serves a state-changing route? Are the boundaries (DB / ORM / reconcile / store / api / pubsub / clearhouse / batch_diff / edi.filenames / auth / handlers / submission / rebill / reissue / store/) drawn correctly? Can you, from §5.1 alone, draw the import graph?
2. **Data model** — Are all 12 migrations listed in §5.2 with their purpose? Are the two worktree-only migrations (0013, 0014) flagged as in-flight? Are all 18 ORM models in §5.2? Is the `db_backups` (not `backups`) correction applied? Is the `Rejection` phantom removed? Is the `ActivityEvent` vs `AuditLog` distinction clear (NFR-3 vs NFR-4)? 2. **Data model** — Are all 23 migrations listed in §5.2 with their purpose (no worktree-only entries)? Are all 18 ORM models in §5.2? Is the `db_backups` (not `backups`) correction applied? Is the `ActivityEvent` vs `AuditLog` distinction clear (NFR-3 vs NFR-4)?
3. **Dependencies** — Are §5.3 runtime + tooling deps correct (no missing `pyyaml`, no missing `keyring`, no `httpx` listed as runtime, `paramiko` flagged as `[sftp]` extra, `sqlcipher3` flagged as `[sqlcipher]` extra, frontend test env is `happy-dom` not `jsdom`, vitest pinned at `^4.1.9` flagged as unusual)? Are OS-level deps (Keychain via `keyring`, SQLCipher C library, `tini`, `nginx` if SP23 ships) called out? 3. **Dependencies** — Are §5.3 runtime + tooling deps correct (no missing `pyyaml`, no missing `keyring`, no `httpx` listed as runtime, `paramiko` flagged as `[sftp]` extra, `sqlcipher3` flagged as `[sqlcipher]` extra, frontend test env is `happy-dom` not `jsdom`, vitest pinned at `^4.1.9` flagged as unusual)? Are OS-level deps (Keychain via `keyring`, SQLCipher C library, `tini`, `nginx` SP23 shipped) called out?
4. **Definition of Done** — Is §8 testable? Project DoD #6 now points at the reviewer-agreement meta-criterion. Per-release + per-task DoDs each independently checkable? FR-7 / FR-33 traceability rows corrected to point at existing test files and the correct endpoint name? 4. **Definition of Done** — Is §8 testable? Project DoD #6 now points at the reviewer-agreement meta-criterion. Per-release + per-task DoDs each independently checkable? FR-7 / FR-33 / FR-35 / FR-36 traceability rows corrected to point at existing test files and the correct endpoint names?
5. **Traceability** — Pick any FR-NN in §3. Can you trace it to an SP, a spec on disk (or an honest "no dedicated spec on disk" note), a plan on disk (or an honest "no plan file on disk; feat(spN) commits substitute" note), and at least one test file in `backend/tests/` or `src/`? If not, that's a remaining gap. 5. **Traceability** — Pick any FR-NN in §3. Can you trace it to an SP, a spec on disk (or an honest "no dedicated spec on disk" note), a plan on disk (or an honest "no plan file on disk; feat(spN) commits substitute" note), and at least one test file in `backend/tests/` or `src/`? If not, that's a remaining gap. Note FR-RB-1..FR-RB-6 (SP41) and the auth-related rows point at the new subpackages.
6. **Assumptions** — Are §10 assumptions falsifiable? Could a new engineer challenge any of them and find the test that proves/disproves them? Is A2's reference to `keyring` accurate now? 6. **Assumptions** — Are §10 assumptions falsifiable? Could a new engineer challenge any of them and find the test that proves/disproves them? Is A2's reference to `keyring` accurate now (Linux dispatch to Secret Service / kwallet, Windows via Credential Manager)?
7. **Risks** — Are §11 risks current (R-11 corrected to 2,172; R-12 added; R-25/R-26/R-27 added)? Compare against `docs/reviews/2026-06-20-cyclone-completeness-review.md`. Is R-24 (SP23 fork) clearly flagged as awaiting user decision? 7. **Risks** — Are §11 risks current (R-1/R-3/R-11/R-12/R-24 closed by SP23/SP21/SP36; R-28 added for test-density drift)? Compare against `docs/reviews/2026-06-20-cyclone-completeness-review.md`.
If reviewers materially agree on all seven, round 2 closes; this doc becomes the project's new top-level entry point (linked from `README.md`). If reviewers materially agree on all seven, round 2 closes; this doc becomes the project's new top-level entry point (linked from `README.md`).
+2 -2
View File
@@ -294,7 +294,7 @@ from cyclone.parsers.parse_999 import parse as parse_999
result = parse_999(open("path.999.x12").read()) result = parse_999(open("path.999.x12").read())
``` ```
### After SP39 lands — corrected-claim push + tracking ### SP39 — corrected-claim push + tracking
The corrected files now live under The corrected files now live under
`ingest/corrected-v2/v2-batch-<batch-id>-<N>-claims/` instead of the `ingest/corrected-v2/v2-batch-<batch-id>-<N>-claims/` instead of the
@@ -370,7 +370,7 @@ curl -X PATCH http://127.0.0.1:8000/api/clearhouse \
To revert, set `stub: true` and `auth: {"method": "keychain", "secret_ref": "sftp.gainwell.password"}`. To revert, set `stub: true` and `auth: {"method": "keychain", "secret_ref": "sftp.gainwell.password"}`.
### After SP40 lands — Edifabric /v2/x12/validate integration ### SP40 — Edifabric /v2/x12/validate integration
SP40 wires Edifabric's reference X12 validator into the push flow SP40 wires Edifabric's reference X12 validator into the push flow
as a fail-closed pre-upload gate, plus a CLI / HTTP endpoint for as a fail-closed pre-upload gate, plus a CLI / HTTP endpoint for
@@ -0,0 +1,234 @@
# Doc-Pass 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:** Reconcile CLAUDE.md, README.md, docs/ARCHITECTURE.md, docs/REQUIREMENTS.md, docs/RUNBOOK.md, and the eight `.superpowers/skills/*/SKILL.md` files so the prose matches the current implementation after SPs 2341.
**Architecture:** Pure `.md` edits. No code paths change; no API endpoints change; no test assertions change. One commit per file, prefixed `docs(sp42): <file> …`. The merge commit (`merge: SP42 doc-pass into main`) is the audit trail.
**Tech Stack:** Markdown + `git` + `grep` for verification.
**Spec:** [`docs/superpowers/specs/2026-07-08-cyclone-doc-pass-design.md`](../specs/2026-07-08-cyclone-doc-pass-design.md)
---
## File structure
```
docs/superpowers/
specs/2026-07-08-cyclone-doc-pass-design.md ← new
plans/2026-07-08-cyclone-doc-pass.md ← new
CLAUDE.md ← edit (header + subpackage + LOC claims)
README.md ← edit (lead paragraph + project layout)
docs/ARCHITECTURE.md ← edit (§1, §2, §4.1, §4.3, §5.4, §6.2, §6.3, §7.4, §8.1, §11.2, §13.2)
docs/REQUIREMENTS.md ← edit (§1, §2.2, §5.1, §5.2, §6.1, §6.2, §11, §12)
docs/RUNBOOK.md ← edit (SP23 Docker stack → Docker stack)
.superpowers/skills/cyclone-spec/SKILL.md ← edit (As of this writing header)
.superpowers/skills/cyclone-tests/SKILL.md ← edit (counts + SP-N pointer)
.superpowers/skills/cyclone-store/SKILL.md ← edit (SP21 framing + line refs + module list)
.superpowers/skills/cyclone-api-router/SKILL.md ← edit (4 → 22 routers + line refs)
.superpowers/skills/cyclone-edi/SKILL.md ← edit (SP-N pointer)
.superpowers/skills/cyclone-frontend-page/SKILL.md ← edit (counts + drawer list)
.superpowers/skills/cyclone-cli/SKILL.md ← edit (Seven → 15+ subcommands)
.superpowers/skills/cyclone-tail/SKILL.md ← edit (3 → 4 streaming pages + line refs)
```
---
## Task 0: branch + spec/plan
- [x] Branch `sp42-doc-pass` from `main` (no worktree — work in current checkout).
- [x] First commit on the branch: `docs(spec): SP42 doc-pass design` (commits the spec).
- [x] Second commit: `docs(plan): SP42 doc-pass implementation plan` (commits this plan).
---
## Task 1: refresh `CLAUDE.md`
- [ ] Update line ~7 to read "LAN-only by design — always binds to `0.0.0.0:8000` and requires login…".
- [ ] Update line ~97 ("SP22 used, next free is SP24") to "SP numbers are used through **SP41** (in-window rebill pipeline); the next free increment is **SP42**. A `sp42-doc-pass` worktree is checked out locally but not yet merged into `main`."
- [ ] Update line ~121 (api.py / store.py LOC + "only large file" claim): replace the "3,548 LOC / 2,423 LOC / in flight" framing with post-SP21/SP36 reality. "The largest files now live under `cyclone/api_routers/` (22 routers under SP36) and `cyclone/store/` (the SP21-split facade in 16 modules). `api.py` is a ~377-line shell that wires the FastAPI app and mounts the routers."
- [ ] Update line ~122 subpackage list: expand `api_routers/` to 22 routers; add `auth/` (SP24, 10 modules), `handlers/` (SP27, 6 modules), `rebill/` (SP41, 13 modules), `reissue/` (SP24, 2 modules), `store/` (SP21, 16 modules). Note that `workflow/` is dead bytecode.
- [ ] Update line ~128 migration count: "23 SQL migrations under `migrations/` (0001_initial through 0023_visits)".
- [ ] Drop the three phantom `parse-999` / `parse-ta1` / `parse-277ca` entries from the CLI block (lines 174176) — they contradict the body text and were already flagged in CLAUDE.md's "Things that are easy to get wrong" section.
- [ ] Commit: `docs(sp42): CLAUDE.md — refresh SP numbers, store split, subpackage list`.
---
## Task 2: refresh `README.md`
- [ ] Update lines 78: replace "Local-only on purpose: binds to `127.0.0.1`, no auth, no internet exposure." with "LAN-only by design — always binds to `0.0.0.0:8000` and requires login (bcrypt + HttpOnly session cookie; first admin bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`). Reachability is controlled by the host firewall / compose port publishing, not the bind address."
- [ ] Update the project-layout block: replace `store.py` with `store/` (post-SP21 split); replace the `api.py` description with the thin-shell description; add `auth/`, `handlers/`, `rebill/`, `reissue/`, `store/`, `submission/`, `api_routers/` (22 routers).
- [ ] Update the "Sub-projects 2 through 19 are shipped" claim to "Sub-projects 2 through 41 are shipped."
- [ ] Update the CLI example block to drop `parse-999`, `parse-ta1`, `parse-277ca` (which don't exist as DB-writing CLIs — see CLAUDE.md "Inbound ingestion paths" note).
- [ ] Commit: `docs(sp42): README.md — refresh auth/bind claim and project layout`.
---
## Task 3: refresh `docs/ARCHITECTURE.md`
- [ ] Update §1 line ~14: replace `127.0.0.1` with `0.0.0.0` and add the reachability + auth boundary sentence.
- [ ] Update §2 process diagram: replace `api.py 3,548 LOC` with `api.py ~377 LOC (post-SP36 thin shell)`; replace the 4-router enumeration with the full 22-router list.
- [ ] Update §4.1 package layout: add `auth/`, `handlers/`, `rebill/`, `reissue/`, `store/`, expand `api_routers/` to 22 routers.
- [ ] Update §4.2 module map: add the missing subpackage rows.
- [ ] Update §4.3 SP21 framing: present-tense ("After SP21, the public API of `cyclone.store` is unchanged…"); add the post-split module list reference.
- [ ] Update §5.4: replace `/api/tail?since=…` with the canonical three-stream endpoints and replace `api.py:1357-2006` with `api_routers/{claims,remittances,activity}.py`.
- [ ] Update §6.2 ERD: add `claim_acks`, `resubmissions`, `visits` tables.
- [ ] Update §6.3 claim-state diagram: add `partial` (per REQUIREMENTS FR-6).
- [ ] Update §7.4: replace `auto:pcn_npi_charge` / `auto:pcn_charge` with `pcn-exact` (SP31).
- [ ] Update §8.1 endpoint enumeration with the canonical list (~85 routes after SPs 2341).
- [ ] Update §11.2: replace "SP23 product fork awaiting user decision" with "SP23 (shipped 2026-06-23 via `merge: SP23 Ubuntu Docker Deployment into main`)".
- [ ] Update §13.2: replace "22 specs / 27 plans" with "42 specs / 37 plans"; add the missing SP25SP41 spec entries.
- [ ] Update §6.1 migration table from 12 to 23.
- [ ] Commit: `docs(sp42): ARCHITECTURE.md — refresh post-SP2341 package map, endpoints, SP23 status`.
---
## Task 4: refresh `docs/REQUIREMENTS.md`
- [ ] Update line ~49: "Sub-projects 2 through 41 are **shipped**."
- [ ] Update §2.2 / §5.4 / §5.1: drop "SP23 would add" speculative framing; rewrite as past tense where SP23 shipped something.
- [ ] Update §5.1 LOC claims (api.py 3,145 → ~377; store.py 2,172 → split into `cyclone/store/`).
- [ ] Update §5.1 module list: add `auth`, `handlers`, `rebill`, `reissue`, `submission` subpackage rows; expand `store` row to its 16 modules.
- [ ] Update §5.2 migration table: 12 → 23 with the full enumeration.
- [ ] Update §6.1 SP table: add SP23 (Ubuntu+Docker, shipped) through SP41 (rebill pipeline, shipped) as separate rows.
- [ ] Update §6.2 backlog: move SP23 from backlog to §6.1; remove the "SP23 candidate" row.
- [ ] Update §11 R-1, R-3, R-11, R-12 to Closed with concrete merge-SHA evidence; update R-13 migration count to 23.
- [ ] Update §12.1 / §12.2 spec-and-plan lists to include SP25SP41.
- [ ] Commit: `docs(sp42): REQUIREMENTS.md — refresh SP table, close SP23 risks, add SP2541`.
---
## Task 5: refresh `docs/RUNBOOK.md`
- [ ] Update line ~120: replace "For the SP23 Docker stack, mount the MFT password as a file…" with "For the Docker stack, mount the MFT password as a file…" (SP23 is shipped; the wording should not read as conditional).
- [ ] Audit any other "SP23 Docker stack" / "SP23 would" / "SP23 candidate" wording; rewrite as past tense where SP23 shipped the feature.
- [ ] Commit: `docs(sp42): RUNBOOK.md — drop 'SP23 Docker stack' conditional framing`.
---
## Task 6: refresh `.superpowers/skills/cyclone-spec/SKILL.md`
- [ ] Update line ~16: "As of this writing: **42 specs** in `docs/superpowers/specs/`, **37 plans** in `docs/superpowers/plans/`, SP numbers used through **SP41** (in-window rebill pipeline). The next free increment is **SP42**."
- [ ] Add a one-line note about the SP25 reuse anomaly (three SP25s: sftp-polling-enablement, ack-live-tail, orphan-data-recovery).
- [ ] Commit: `docs(sp42): cyclone-spec skill — refresh SP-N framing`.
---
## Task 7: refresh `.superpowers/skills/cyclone-tests/SKILL.md`
- [ ] Update line ~78: refresh test counts (89 backend → 178; 13 fixtures → 22; 59 frontend → 130+); replace "next is SP24" with "next is SP42".
- [ ] Commit: `docs(sp42): cyclone-tests skill — refresh counts and SP-N pointer`.
---
## Task 8: refresh `.superpowers/skills/cyclone-store/SKILL.md`
- [ ] Update line ~10: replace `backend/src/cyclone/store.py:882` with `backend/src/cyclone/store/__init__.py:148`.
- [ ] Update line ~17: drop "single 2412-line module, SP21 in progress" framing; replace with the post-SP21 16-module enumeration + "Last merged SP: SP41; next free increment: SP42."
- [ ] Update SP21 split module list (lines 6070) to add the 5 missing modules: `backfill.py`, `claim_acks.py`, `kpis.py`, `resubmissions.py`, `submission_dedup.py`.
- [ ] Update line ~86 (`store.py:898-1107`) → `store/write.py`.
- [ ] Update line ~121 (`api.py:1380-1401`) → `api_routers/{claims,remittances,activity}.py`.
- [ ] Update line ~136 (`api.py:1891,2002`) → `api_routers/{claims,remittances,activity}.py`.
- [ ] Commit: `docs(sp42): cyclone-store skill — refresh SP21 framing, line refs, module list`.
---
## Task 9: refresh `.superpowers/skills/cyclone-api-router/SKILL.md`
- [ ] Update lines 1418: replace "4 router modules" with the canonical 22-router list; replace "api.py ~3,548 LOC" with "api.py ~377 LOC (thin shell)".
- [ ] Update lines 4950: drop the "parse endpoints next refactor target" note (SP36 already split them).
- [ ] Update lines 170183 line refs (`api.py:246-256` etc.) → `api_routers/__init__.py`.
- [ ] Commit: `docs(sp42): cyclone-api-router skill — refresh router count + line refs`.
---
## Task 10: refresh `.superpowers/skills/cyclone-edi/SKILL.md`
- [ ] Update line ~13: "The next increment is **SP42**" (was SP22). Add a one-line note about SP32 (rendering NPI), SP35 (parse input guards), SP39 (2010BB NM109), SP40 (Edifabric gate) having touched parser/validator territory.
- [ ] Commit: `docs(sp42): cyclone-edi skill — refresh SP-N pointer`.
---
## Task 11: refresh `.superpowers/skills/cyclone-frontend-page/SKILL.md`
- [ ] Update line ~11: refresh counts (11 → 12 pages, ~30 → ~53 hooks, 2 → 4 drawers); replace "next is SP22" with "next is SP42".
- [ ] Update drawer list at lines 1820: add `ProviderDrawer/`, `AckDrawer/`.
- [ ] Commit: `docs(sp42): cyclone-frontend-page skill — refresh counts + drawer list`.
---
## Task 12: refresh `.superpowers/skills/cyclone-cli/SKILL.md`
- [ ] Update lines 89: replace "Seven subcommands" with the canonical 15+ subcommand list (`parse-837`, `parse-835`, `validate-npi`, `validate-tax-id`, `validate-837`, `submit-batch`, `reissue-claims`, `resubmit-rejected-claims`, `resubmissions`, `pull-inbound`, `recover-ingest`, `ack-orphans`, `users`, `seed`, `backfill-rendering-npi`, `backfill-999-rejections`, `rebill-from-835`, `pull-inbound`, plus the `backup` group).
- [ ] Commit: `docs(sp42): cyclone-cli skill — replace Seven-subcommands claim with full list`.
---
## Task 13: refresh `.superpowers/skills/cyclone-tail/SKILL.md`
- [ ] Update lines 1317: refresh "three streaming pages" claim — add `Acks` (SP25 ack-live-tail) for a total of 4.
- [ ] Update line ~44: replace `api.py:1357-2006` with `api_routers/{claims,remittances,activity}.py` + `api_routers/{acks,ta1_acks}.py`.
- [ ] Update line ~70: replace `api.py:1401,1894,2005` with the post-SP36 router files.
- [ ] Update line ~119: replace `api.py:1357-1401` with `api_routers/claims.py`.
- [ ] Commit: `docs(sp42): cyclone-tail skill — refresh streaming page count + line refs`.
---
## Task 14: autoreview (the merge gate)
- [ ] Run the 14 acceptance-criteria grep checks from §6 of the spec; expect zero matches for each.
- [ ] `cd backend && .venv/bin/pytest -q` — full backend suite passes (no regressions expected; this is a docs-only increment).
- [ ] `npm test` — full vitest suite passes.
- [ ] `git log --oneline sp42-doc-pass` shows the expected commit sequence (1× `docs(spec):` + 1× `docs(plan):` + 13× `docs(sp42):`).
- [ ] `git status --short` shows no unexpected untracked files (note: pre-existing untracked files from the operator's working dir — `july8billing/`, `july8billing_out/`, `xxxclaims.zip`, `verification-final.txt`, `scripts/serialize_july8_to_sp41.py`, `backend/src/cyclone/submission/bulk_ingest.py`, `backend/tests/fixtures/co_medicaid_837p.997`, `backend/tests/fixtures/subscriber_roster.csv`, `docs/superpowers/plans/2026-07-07-cyclone-2010bb-nm109-fix.md`, `docs/superpowers/plans/2026-07-07-cyclone-inwindow-rebill-pipeline.md`, `docs/superpowers/specs/2026-07-07-cyclone-inwindow-rebill-pipeline-design.md`, `.grok/` — should remain untracked).
- [ ] No semantic rewrites of unrelated prose. Each diff should touch only the lines flagged in the SP42 audit findings.
---
## Task 15: merge to `main`
- [ ] `git checkout main && git merge --no-ff --no-squash --no-rebase sp42-doc-pass -m "merge: SP42 doc-pass into main"`
- [ ] `git log --oneline main -1` shows the SP42 merge commit with the expected subject.
- [ ] `git push origin main` only after the operator has reviewed and approved the merge (per cyclone-spec — the merge commit is the audit trail; the operator must approve).
---
## Verification commands (one-shot)
```bash
# Acceptance criteria §1 — no stale "next free is SP24" / "next is SP25"
grep -rE 'SP22.*SP24|next free increment is SP24|next increment is SP24|next free is SP24|next is SP25' \
CLAUDE.md README.md docs/ARCHITECTURE.md docs/REQUIREMENTS.md docs/RUNBOOK.md .superpowers/skills/ 2>/dev/null
# §2 — SP23 "awaiting user decision" / "candidate" / "product fork"
grep -rE 'awaiting user decision|SP23.*candidate|SP23.*product fork' docs/ARCHITECTURE.md docs/REQUIREMENTS.md 2>/dev/null
# §3 — stale LOC claims
grep -rE 'api\.py.*3,548|3,548 LOC|store\.py.*2,423|2,423 LOC|2,172 LOC|3,145 LOC' \
CLAUDE.md README.md docs/ARCHITECTURE.md docs/REQUIREMENTS.md .superpowers/skills/ 2>/dev/null
# §4 — 127.0.0.1 in bind-address context
grep -nE '127\.0\.0\.1' CLAUDE.md README.md docs/ARCHITECTURE.md docs/REQUIREMENTS.md 2>/dev/null
# §5 — README "no auth, no internet exposure"
grep -nE 'no auth, no internet exposure' README.md
# §6 — "4 router modules" / "four router modules"
grep -rE '4 router modules|four router modules' .superpowers/skills/cyclone-api-router/SKILL.md docs/ARCHITECTURE.md docs/REQUIREMENTS.md 2>/dev/null
# §7 — "Seven subcommands" / "7 subcommands"
grep -rE 'Seven subcommands|7 subcommands' .superpowers/skills/cyclone-cli/SKILL.md
# §8 — "single 2412-line module" / "SP21 in progress" / "SP21 in-flight"
grep -rE 'single 2412-line module|SP21.*in.progress|SP21.*in-flight' .superpowers/skills/cyclone-store/SKILL.md
# §9 — "12 SQL migrations"
grep -rE '12 SQL migrations' CLAUDE.md docs/ARCHITECTURE.md docs/REQUIREMENTS.md 2>/dev/null
# §10 / §11 — test suites
cd backend && .venv/bin/pytest -q
npm test
```