docs(sp42): ARCHITECTURE.md — refresh package map, router count, migration table, SP23 status, live-tail wire format, pcn-exact strategy

This commit is contained in:
Nora
2026-07-08 23:08:00 -06:00
parent b0956d039c
commit d8d03d631f
+171 -57
View File
@@ -4,8 +4,8 @@
>
> **Companion docs:**
> - **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).
> - **Plans:** [`docs/superpowers/plans/`](superpowers/plans/) — TDD-shaped implementation plans per sub-project (27 plans).
> - **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 (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.
@@ -13,7 +13,7 @@
## 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:
@@ -46,17 +46,22 @@ The whole stack is a single Python process (FastAPI + uvicorn on port 8000) plus
+----------------------------------------------------------+
| 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) | |
| | - routers: claims, remits, providers, acks, | |
| | activity, inbox, upload, download, batches, | |
| | admin, scheduler, health, ta1_acks | |
| | cyclone.api:app (~377 LOC, post-SP36 thin shell) | |
| | - mounts 22 routers under cyclone.api_routers/ | |
| | (acks, activity, admin, batches, claim_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) | |
| | - lifespan: init db -> seed SP9 -> start SP16 + | |
| | SP17 schedulers -> load PayerConfig cache | |
| | - lifespan: init db -> seed SP9 -> start SP16 | |
| | + 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/... | |
| | - 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 | |
| | -> write to store) | |
| +-----------------------------------------------------+ |
| | cyclone.reconcile, scoring, batch_diff, audit_log, | |
| | backup_service, scheduler, db_crypto, secrets | |
| | cyclone.auth.* (SP24: bcrypt + sessions + matrix | |
| | 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"` |
| 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 |
| Validation | Pydantic | ≥2.6, <3 | v2 model_validator + ConfigDict |
| 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
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/
├── __main__.py — entry point; `python -m cyclone` → CLI, `python -m cyclone serve` → uvicorn
├── __init__.py — `__version__`, package init
├── api.py — FastAPI app, route includes, lifespan (3,548 LOC — the only large file)
├── api_helpers.py — request/response formatters, error → HTTP mapping
├── api_routers/
│ ├── __init__.py
│ ├── acks.py — 999/TA1/271/277CA acknowledgments
│ ├── admin.py — admin-only endpoints (validate-provider, scheduler, backup)
│ ├── health.py — GET /api/health (SP19 deep snapshot)
── ta1_acks.py — TA1-specific (split from acks for the SP3 lifecycle)
├── api.py — FastAPI app, route mounting (~377 LOC, post-SP36 thin shell)
├── api_helpers.py — request/response formatters, error → HTTP mapping, NDJSON helpers
├── api_routers/ — SP36: 22 per-resource FastAPI routers + _shared.py
│ ├── __init__.py — registry: `routers: list[APIRouter]`
│ ├── _shared.py — cross-router helpers
│ ├── acks.py 999/277CA acks list / stream / detail
│ ├── activity.py — activity feed + stream
── 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)
├── 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_scheduler.py — SP17 BackupScheduler wrapper
├── backup_service.py — SP17 coordinator: create/list/restore/verify/prune
├── batch_diff.py — 837 ↔ 835 diff (SP4)
├── claim_acks.py — SP28 pure readers over session + parse result
├── clearhouse/
│ └── __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_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/
│ └── 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_state.py — claim ↔ inbox state mapping helpers
├── inbox_state_277ca.py — SP10 277CA inbox state integration
├── 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
├── parsers/ — X12 parser subpackage
│ ├── parse_837.py — 837P ingest
@@ -187,23 +234,44 @@ backend/src/cyclone/
├── payers.py — Payer ORM row accessor (SP9)
├── providers.py — Payer / Clearhouse / Provider ORM row DTOs (SP9)
├── 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
│ ├── reconcile.py visit-to-835 join (member, procedure, DOS)
│ ├── carc_filter.py — CARC-aware filter
│ ├── timely_filing.py — 120-day DOS age gate
│ ├── pipeline_a.py — denied/partial → frequency-7
│ ├── pipeline_b.py — NOT_IN_835 → fresh 837Ps by (member, week)
│ ├── summary.py — summary CSV generator
│ ├── pipeline_a.py — denied/partial → frequency-7 replacement 837P
│ ├── pipeline_b.py NOT_IN_835 → fresh 837Ps batched by (member_id, ISO-week)
│ ├── pull_999_acks.py — 999-ack dump + classify NOT_IN_835
│ ├── reconcile.py visit-to-835 join (member, procedure, DOS) with 5% tolerance
│ ├── run.py — orchestrator (run_rebill)
│ ├── pull_999_acks.py — 999-ack dump + classify
── __init__.py — package surface
├── reconcile.py — 835 → 837 auto-reconciliation engine
├── scheduler.py — SP16 MFT polling scheduler
│ ├── spot_check.py single-claim well-formed 837P from VisitRow
── spot_check_pipeline.py — top-N visits → well-formed 837P files
│ ├── spot_check_validate.py — submit to Edifabric /v2/x12/validate
├── 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)
├── secrets.py — Keychain access wrapper (SP9)
├── secrets.py — Keychain access wrapper (SP9, SP26 _FILE tier)
├── 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)
@@ -253,7 +321,7 @@ The store is the single read/write surface for the database. Every endpoint that
- `store.list_unmatched(kind="both")`
- `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
@@ -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)
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
- `{kind: "event", id: <int>, event_type: "...", payload: {...}}`when something happens
- `{kind: "error", message: "..."}`server-initiated close (rare)
- `{type: "item", data: {...}}`one row from the snapshot (initial batch) or a live event
- `{type: "snapshot_end", data: {count: N}}`the snapshot has finished
- `{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.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 |
|---|---|---|
@@ -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 |
| 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)
@@ -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:
- `auto:pcn_npi_charge` — patient_control_number + provider_npi + charge_amount match (the strongest)
- `auto:pcn_charge` — patient_control_number + charge_amount (the most common in practice)
- `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.
- `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.
@@ -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":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":"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 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 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
@@ -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).
### 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"
@@ -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:
- **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.
- **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.
@@ -758,11 +853,30 @@ All in `docs/superpowers/specs/`:
- 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`
- 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
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