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:
+171
-57
@@ -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 0001–0014
|
||||
### 6.1 Migrations 0001–0023
|
||||
|
||||
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 SP9–SP20 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user