diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index dab11f7..2cd05ca 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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=`. 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//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: , 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