Replaces every stale 'no auth' / 'no authentication' / 'no second party to authenticate' claim in the three top-level docs with the v1 posture: the auth boundary is HTTP (bcrypt + HttpOnly session cookie; first admin bootstrapped from CYCLONE_ADMIN_USERNAME + CYCLONE_ADMIN_PASSWORD env vars); the file-system posture (SQLCipher at rest, macOS Keychain) is unchanged; the threat model is still a stolen/imaged drive. Closes requirements §11 R-1 (was Open; now Closed by SP24 — auth shipped via the origin/main merge on 2026-06-23, SP24 reconciled the docs). Also: - backend/src/cyclone/__main__.py — emit WARNING when AUTH_DISABLED is True at boot so a misconfigured production deploy fails loudly - §6.1 migrations table in ARCHITECTURE.md — list 0013 (auth_users_and_ sessions) and 0014 (audit_log_user_id) with the renumbering note - §4.2 module map — add the cyclone.auth.* package - Three skills addenda: cyclone-tests (AUTH_DISABLED conftest bypass is mandatory context), cyclone-api-router (every router needs Depends(matrix_gate)), cyclone-spec (spec template threat-model is now auth-aware) This also tracks CLAUDE.md in git for the first time (was previously untracked; the SP24 doc updates are in scope for the increment). Spec: docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md Plan: docs/superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md
72 KiB
Cyclone — Requirements, Architecture, Tasks, and Test Strategy
Date: 2026-06-23
Status: Round 2 (post-review corrections — ready for verification)
Audience: New engineer or AI agent who needs to understand Cyclone's contract, the work that's already shipped, the work that's not, and what "done" means before they write a single line of code.
Scope: Local-only, single-operator, single-host EDI claims-management suite for one billing office, currently trading with Colorado Medicaid (CO XIX).
Sister docs: README.md is the operator-facing entry point; docs/ARCHITECTURE.md is the top-level technical design (process topology, package layout, data flow, lifecycle); docs/superpowers/specs/ holds per-SP design specs; docs/superpowers/plans/ holds per-SP implementation plans; docs/reviews/2026-06-20-cyclone-completeness-review.md is the industry-scope gap analysis; docs/reference/ holds condensed X12 / payer notes.
0. How to read this document
| If you want to know… | Go to |
|---|---|
| What Cyclone is and is not | §1, §2 |
| What it must do (functional) | §3 |
| What it must not fail at (non-functional) | §4 |
| How the system is built | §5 |
| Which sub-project (SP) owns which requirement | §3 + §6 traceability matrix |
| What "shipped" means | §7 |
| What "done" means for the project as a whole | §8 |
| How we test | §9 |
| What we're assuming to be true | §10 |
| What's still missing or risky | §11 |
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.
1. The contract — what Cyclone is
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). - 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 viaCYCLONE_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."
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.
2. Scope boundaries
2.1 In scope (today, after SP1–SP22)
| Capability | SP |
|---|---|
| 837P (005010X222A1) parse + serializer with round-trip guarantee | SP1 + SP8 |
| 835 (005010X221A1) parse + CAS deep-parsing | SP1 + SP3 |
| 999, TA1, 270, 271, 277CA parse / generate | SP3 + SP10 |
| Structural validation rules (R021, R034, R035, R200, R210, …) | SP3 + SP20 |
| SQLite persistence with forward-compatible migrations | SP2 + SP9–SP17 |
| Automatic 837P ↔ 835 reconciliation; manual reconciliation UI | SP2 + SP7 |
| 7-state claim lifecycle with reversal handling | SP2 |
| Tamper-evident hash-chained audit log | SP11 |
| NDJSON pubsub on store writes + live-tail UI with reconnect | SP5 |
| 5-lane inbox (Rejected / Payer-rejected / Candidates / Unmatched / Done today) | SP6 + SP10 + SP14 |
| Per-line 837 SV1 ↔ 835 SVC adjustment audit | SP7 |
| Per-claim and per-remit detail drawers + Cmd-K search + batch diff + CSV export | SP4 + SP21 |
| Universal drilldown (drawer + peek modal everywhere) | SP21 |
| Multi-payer / multi-NPI config + clearhouse stub | SP9 |
paramiko-backed SFTP wire to Gainwell MFT |
SP13 |
| 24h MFT inbound scheduler with idempotent file processing | SP16 |
| SQLCipher encryption at rest + key rotation | SP12 + SP15 |
| Automated AES-256-GCM encrypted DB backups | SP17 |
| Structured JSON logging + PII scrubber | SP18 |
| Security hardening (body size limit, rate limit, security headers) | SP19 |
| NPI Luhn checksum + Tax ID format validation | SP20 |
CycloneStore split (refactor; in-flight on refactor/store-split) |
SP21 |
| Parse-then-decide upload dedup (idempotency) | SP22 |
Pipeline automation agent (sibling project at cyclone-pipeline/) |
SP22 |
2.2 Out of scope (today, by design)
| Item | Why |
|---|---|
| AS2 / AS4 (EDIINT, signed, encrypted, MDN-back) connectivity | Single-host local tool; the operator hands the 837 ZIP to the Gainwell MFT UI manually |
| Real-time 270/271 round-trip over CAQH CORE Phase II/III SOAP envelopes | Out of single-host scope; builder + parser are X12-only |
| 837I (institutional), 837D (dental), 834, 820, 275, 278, 276/277 | Single-payer scope; not currently demanded by the CO XIX workflow |
| NPPES NPI registry lookup (offline validation only) | Single-host, no internet exposure |
| ICD-10 / HCPCS / NDC vocabulary tables | Structural validation only; the operator has the source vocab |
| COB / secondary-claim generator | Workflow scope |
| HITRUST / SOC 2 / BAA template | Single-operator; threat model doesn't require them |
| HA / DR / load balancing / multi-host replication | Single-host scope |
| Prometheus / Grafana / alerting | curl health | mail cron is sufficient |
| 2FA / SSO / OAuth | Single-operator |
| LUKS full-disk encryption | Operator / host concern |
| Component / E2E browser tests (Playwright) | Local-only, manual smoke scripts are sufficient |
2.3 Deferred with rationale
- Public TLS / public domain / Caddy reverse proxy — LAN-only bind is enough for the v1 single-Ubuntu-server posture; VPN handles outside access. (Re-evaluated if the Ubuntu Docker plan ships.)
- Off-box automated backup shipping — host cron / rsync is documented in the operator runbook, not automated in code.
- Self-service password reset — admin can reset via CLI; self-service is v2 (would only be relevant if auth ships).
- Per-file encryption of prodfiles /
sftp_staging— relies on volume-level filesystem permissions + the SQLCipher-encrypted DB + the host being physically secure. - Watchtower / automatic updates — manual
docker compose pullkeeps the operator in the loop on schema migrations.
3. Functional requirements
Each requirement is FR-NN, traceable to one or more sub-projects. Verification is "shipped + tested" (see §6) or "planned" (see §6.2).
| ID | Requirement | SP |
|---|---|---|
| FR-1 | Parse an uploaded 837P file into structured Claim records. |
SP1 |
| FR-2 | Parse an uploaded 835 file into structured Remittance records with per-CAS adjustment rows. |
SP1 + SP3 |
| FR-3 | Persist every successful parse to SQLite; survive backend restart. | SP2 |
| FR-4 | Run a forward-compatible migration runner (PRAGMA user_version) so the schema can evolve without Alembic. |
SP2 |
| FR-5 | Auto-match each 835 CLP back to a prior 837 Claim within ±7 days via (claim_id, service_date); apply lifecycle transitions; write Match and ActivityEvent rows. |
SP2 |
| FR-6 | Maintain a 7-state claim lifecycle (`submitted → received → paid | partial |
| FR-7 | Provide a /reconciliation page that lists unmatched claims and unmatched remittances and supports manual pair/unpair. |
SP2 (tests live in test_reconcile.py, test_reconcile_line_level.py, test_inbox_endpoints.py, test_api_gets.py) |
| FR-8 | Run structural 837P validation rules R021 (NPI checksum, warning-only), R034 (REF*G1 on frequency 7/8), R035 (BHT06 transaction-type allowlist), R200/R210. |
SP3 + SP20 |
| FR-9 | Generate a 999 ACK transaction set on 837 ingest when ?ack=true is passed to POST /api/parse-837; persist it to the acks table. |
SP3 |
| FR-10 | Parse inbound 999 ACKs and surface them on a /acks page with regenerable raw text. |
SP3 |
| FR-11 | Parse + persist TA1 interchange ACKs (separate from 999 transaction-set ACKs). | SP3 |
| FR-12 | Build a 270 from a JSON payload (POST /api/eligibility/request) and parse an inbound 271 (POST /api/eligibility/parse-271) into coverage_benefits. |
SP3 |
| FR-13 | Parse inbound 277CA files and stamp payer_rejected on claims whose STC category is A4/A6/A7 — monotonic (a looser later 277CA cannot downgrade). Also persist 277CA ACK rows to two77ca_acks. |
SP10 |
| FR-14 | Provide a /inbox page with five lanes ordered by urgency: Rejected (999 R/E), Payer-rejected (277CA A4/A6/A7), Candidates (CLP didn't auto-match), Unmatched (waiting), Done today (last 24h terminal transitions). |
SP6 + SP10 + SP14 |
| FR-15 | Score each Candidate on 4 weighted fields (patient control number 40, service date 25, charge amount 20, provider NPI 15); tier strong (≥75, Match enabled), weak (50–74, dimmed), hidden (<50). | SP6 |
| FR-16 | Provide per-claim and per-remit detail drawers with full raw X12 segments, state-history timeline, validation panel, line-level reconciliation tab, and party grid. URL is synced (?claim=…, ?remit=…). |
SP4 + SP7 + SP21 |
| FR-17 | Provide a side-by-side Batch Diff view (added / removed / changed claims). | SP4 (logic lives in cyclone/batch_diff.py) |
| FR-18 | Provide a global Cmd-K search across claims, remittances, activity, and acks. | SP4 |
| FR-19 | Provide per-page CSV export for claims, remittances, and inbox lanes. | SP4 |
| FR-20 | Provide a live-tail NDJSON stream on /api/{claims,remittances,activity}/stream with snapshot-first, snapshot_end, heartbeat, and reconnect. |
SP5 |
| FR-21 | Surface a TailStatusPill with states live / connecting / reconnecting / stalled / error / closed, with backoff ladder 1s → 2s → 4s → 8s → 16s → 30s capped. |
SP5 |
| FR-22 | Tie every 835 CAS adjustment back to the specific 837 SV1 service line it adjudicates; surface unmatched-line warnings. | SP7 (line-reconciliation rows in line_reconciliations) |
| FR-23 | Serialize a single edited claim back to a valid X12 837P file with round-trip guarantee (113 prodfiles parse → serialize → parse to the same claim_id). |
SP8 |
| FR-24 | Export a ZIP bundle of selected rejected claims as a batch ready to hand to the MFT UI. | SP8 |
| FR-25 | Load payer / payer_config / clearhouse config from config/payers.yaml (parsed via pyyaml) and 3 new DB tables (providers, payers, payer_configs); replace the in-code PAYER_FACTORIES dict. |
SP9 |
| FR-26 | Push a cyclone clearhouse submit to the Gainwell MFT inbound path via paramiko (SftpClient lives in cyclone/clearhouse/__init__.py). Credentials read from the macOS Keychain via the keyring library at call time. |
SP13 |
| FR-27 | Poll the Gainwell MFT inbound path every tick (configurable); route downloaded files to the right parser (999 / 835 / 277CA / TA1); idempotent via processed_inbound_files; crash-safe via per-file try/except. |
SP16 |
| FR-28 | Optionally encrypt the SQLite file via SQLCipher (AES-256); key fetched from the macOS Keychain via keyring. Falls back to plain SQLite if the Keychain entry is missing. |
SP12 |
| FR-29 | Rotate the SQLCipher key in place via PRAGMA rekey, serialized through a threading.Lock and SQLAlchemy NullPool; write db.key_rotated audit event with old + new key fingerprints and post-rotation table_count. |
SP15 |
| FR-30 | Produce AES-256-GCM encrypted DB backups (PBKDF2-HMAC-SHA256, 200k iters) via the existing BackupService; two-step restore (initiate → confirm with one-shot 64-char hex token); retention pruning with a 30-day default. Backups table is db_backups. |
SP17 |
| FR-31 | Emit all API / CLI / scheduler / backup logs as newline-delimited JSON with ISO-8601 ms timestamps; redact obvious PHI (NPIs, SSNs, DOBs, patient names) via a PiiScrubber filter on message + extras. |
SP18 (uses stdlib logging + a JSON formatter, not structlog) |
| 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-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-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-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 |
4. Non-functional requirements
| ID | NFR | Source / SP |
|---|---|---|
| NFR-1 | Local-only bind + auth. Backend binds 127.0.0.1:8000 (overridable via 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). |
SP1 + auth (2026-06-23 merge) + SP24 (doc reconciliation) |
| 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-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-5 | Encrypted at rest (optional). SQLite is encrypted via SQLCipher AES-256 when the macOS Keychain entry exists and sqlcipher3 is installed; plain SQLite otherwise. |
SP12 |
| NFR-6 | Encrypted backups. Backups are AES-256-GCM encrypted at rest (PBKDF2-HMAC-SHA256, 200k iters); passphrase lives in the macOS Keychain (cyclone backup init-passphrase). |
SP17 |
| NFR-7 | Logging discipline. API / CLI / scheduler / backup logs flow through a JsonFormatter (NDJSON, ISO-8601 ms) by default; PiiScrubber redacts obvious PHI; configurable via CYCLONE_LOG_LEVEL / CYCLONE_LOG_FILE / CYCLONE_LOG_JSON / CYCLONE_LOG_NO_PII_SCRUB. |
SP18 |
| NFR-8 | Operational hygiene. Request body size limit, rate limit, and default security headers are middleware-enforced (SP19). EXEMPT_PATHS = ("/api/health", "/healthz", "/readyz") is the documented bypass list (cyclone/security.py:201). |
SP19 |
| NFR-9 | Thread-affinity. SQLCipher operations are serialized through a threading.Lock + SQLAlchemy NullPool to keep SQLCipher thread-affine under FastAPI's per-request threadpool. |
SP12 + SP15 |
| NFR-10 | Live-tail resilience. Heartbeat every 15s default (CYCLONE_TAIL_HEARTBEAT_S); client flips to stalled after 30s of total silence (heartbeat included); reconnect backoff capped at 30s. |
SP5 |
| NFR-11 | Payer config externalized. Payer / payer_config / clearhouse config lives in config/payers.yaml + DB tables, not in code; a new payer is a YAML + DB row, not a code change. Note: co_medicaid() factory in cyclone/parsers/payer.py:57-58 still exists as a fallback for tests and the default-payer selection (R-4 in §11). |
SP9 |
| 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-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-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 9–16 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-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 |
5. Architecture overview
5.1 Components
Backend (one Python process, FastAPI on uvicorn) — modules under backend/src/cyclone/:
| 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_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.store |
Public store facade; persistence, reconciliation, queries, mappers | 2,172 LOC; SP21 split is in-flight on refactor/store-split |
cyclone.db |
SQLAlchemy engine, session factory, ORM models (18 tables) | |
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.audit_log |
Hash-chained audit_log (SP11, SHA-256 chain) |
Distinct from cyclone.activity_events written by other modules |
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_277ca |
277CA STC A4/A6/A7 → payer_rejected stamp (monotonic) | |
cyclone.providers |
Multi-NPI provider lookups | |
cyclone.payers |
Payer / payer_config lookups | Loads config/payers.yaml via pyyaml |
cyclone.secrets |
macOS Keychain-backed secret fetcher (wraps keyring) |
|
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.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_service |
Backup lifecycle (create / verify / restore) | 740 LOC |
cyclone.backup_scheduler |
24h backup autostart loop | 315 LOC |
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.logging_config |
JSON formatter, PII scrubber | |
cyclone.npi |
NPI Luhn + EIN format validators | |
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.edi (filenames.py) |
HCPF X12 File Naming Standards helpers | Used by SP13/SP16 outbound filename construction |
cyclone.cli |
Click CLI | |
cyclone.__main__ |
python -m cyclone serve |
|
cyclone.parsers/ |
X12 tokenizer, models, validator, writers, 277CA, 999, TA1, 270, 271, CAS, seg | 25 .py modules |
Stale artifact (do not import): backend/src/cyclone/workflow/ contains only a __pycache__/ subdir with stale .pyc files (no __init__.py, no source). The actual sources live at the top level. Recommend deleting the directory.
Frontend (one Vite-served React app) — directories under src/:
| Directory | Owns |
|---|---|
src/pages/ |
Acks, ActivityLog, BatchDiff, Batches, Claims, Dashboard, Inbox, Providers, Reconciliation, Remittances, Upload (11 pages) |
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/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/store/ |
index.ts (zustand sample data), tail-store.ts (FIFO-capped live tail slices) |
src/types/ |
index.ts (flat barrel of shared TS types) |
5.2 Data model
12 SQL migrations in backend/src/cyclone/migrations/ (verified on disk 2026-06-23):
| # | File | Tables / columns added |
|---|---|---|
| 0001 | 0001_initial.sql |
batches, claims, remittances, cas_adjustments, matches, activity_events (6 tables) |
| 0002 | 0002_acks.sql |
acks |
| 0003 | 0003_drop_claims_remits_unique_constraints.sql |
Drops claims and remittances UNIQUE constraints |
| 0004 | 0004_rejections_and_state_history.sql |
Adds rejection_reason, rejected_at, resubmit_count, state_changed_at to claims (no new table) |
| 0005 | 0005_create_ta1_acks.sql |
ta1_acks |
| 0006 | 0006_line_reconciliation.sql |
service_line_payments, line_reconciliations; FK service_line_payment_id on cas_adjustments; claim_level_adjustment_amount on remittances |
| 0007 | 0007_providers_payers_clearhouse.sql |
providers, payers, payer_configs, clearhouse |
| 0008 | 0008_payer_rejected_columns.sql |
two77ca_acks (table) + payer_rejected columns on claims |
| 0009 | 0009_audit_log.sql |
audit_log (SHA-256 hash-chained) |
| 0010 | 0010_payer_rejected_acknowledged.sql |
payer_rejected_acknowledged column on claims |
| 0011 | 0011_processed_inbound_files.sql |
processed_inbound_files (SP16 idempotency) |
| 0012 | 0012_backups.sql |
db_backups (SP17) |
In-flight on claims-unique-fix worktree (not yet on main):
| # | File | Tables / columns added |
|---|---|---|
| 0013 | 0013_drop_claims_unique_constraint.sql |
Drop claims UNIQUE constraint |
| 0014 | 0014_relax_claims_remits_pk.sql |
Relax PK to (batch_id, id) (SP22) |
ORM models (declared in backend/src/cyclone/db.py — 18 classes total):
| Python class | Table | Notes |
|---|---|---|
Batch |
batches |
|
Claim |
claims |
|
Remittance |
remittances |
|
CasAdjustment |
cas_adjustments |
|
ServiceLinePayment |
service_line_payments |
Created by 0006; SP1+ |
LineReconciliation |
line_reconciliations |
Created by 0006; SP7 backbone |
Match |
matches |
Reversals add a second row (NOT UNIQUE on (claim_id, remittance_id)) |
Ack |
acks |
|
Ta1Ack |
ta1_acks |
|
ActivityEvent |
activity_events |
Created by 0001; SP2 — distinct from AuditLog |
Two77caAck |
two77ca_acks |
Created by 0008; SP10 |
Provider |
providers |
|
Payer |
payers |
|
PayerConfigORM |
payer_configs |
|
ClearhouseORM |
clearhouse |
Singleton row (id = 1) |
AuditLog |
audit_log |
SHA-256 hash-chained |
ProcessedInboundFile |
processed_inbound_files |
|
DbBackup |
db_backups |
Note: table is db_backups, NOT backups |
Boundary contracts:
| Module | Public API | Pure? | Notes |
|---|---|---|---|
cyclone.db |
init_db(), SessionLocal(), model classes |
No (engine + schema) | |
cyclone.db_migrate |
run(engine) |
Yes (modulo engine) | |
cyclone.reconcile |
match(), apply_payment(), apply_reversal(), split_unmatched(), line-level match |
Yes (no DB session inside) | Unit-testable with fabricated ORM objects |
cyclone.scoring |
score_pair(...) |
Yes | Weights hardcoded — see R-10 |
cyclone.audit_log |
append_event(...), verify_chain() |
No (DB-owning) but append is deterministic | SHA-256 chain |
cyclone.activity_events (via store) |
append(...) |
No (DB-owning) | Un-chained; powers /activity feed |
cyclone.store |
add(), get_batch(), iter_*(), list_unmatched(), manual_match(), manual_unmatch() |
No (owns sessions, calls reconcile) | |
cyclone.api |
HTTP routes | No | |
cyclone.pubsub |
subscribe_raw(kind, queue_size=256), publish(kind, event) |
No (in-process) | Drop-oldest on full queue |
cyclone.backup + backup_service + backup_scheduler |
initiate_backup, confirm_restore, tick() |
No (DB-owning) | |
cyclone.db_crypto |
is_encryption_enabled(), rotate_key() |
No | rotate_key() is destructive |
cyclone.clearhouse |
SftpClient, make_client(), InboundFile |
No | Real paramiko in SP13 |
cyclone.batch_diff |
diff_batches(), diff_batches_to_wire() |
Yes | Pure function over claim/remit summaries |
cyclone.cas_codes |
CARC_REASON_CODES dict |
Yes | Annual refresh is manual (R-22 in completeness review) |
cyclone.edi.filenames |
filename regexes + helpers | Yes | |
cyclone.logging_config |
JsonFormatter, PiiScrubber, CycloneDevFormatter |
No (installs handlers) | |
cyclone.npi |
validate_npi(), validate_tax_id() |
Yes | |
cyclone.parsers.* |
Pure-function parse + serialize | Yes |
5.3 Dependencies
Backend runtime (verified against backend/pyproject.toml:10-24):
pydantic>=2.6,<3click>=8.1,<9fastapi>=0.110,<1uvicorn[standard]>=0.27,<1(the[standard]extra pullshttptools,uvloop,websockets,watchfiles)python-multipart>=0.0.9,<1sqlalchemy>=2.0,<3pyyaml>=6.0,<7← loadsconfig/payers.yaml(SP9)keyring>=25.0,<26← macOS Keychain backend used bycyclone/secrets.py(SP12/SP13/SP15); on Linux dispatches to Secret Service / kwalletcryptography>=49.0,<50
Backend optional extras:
[sqlcipher]→sqlcipher3>=0.6,<1(SP12; falls back to plain SQLite if missing)[sftp]→paramiko>=3.4,<6(SP13)
Backend dev/test (backend/pyproject.toml:27-33):
pytest>=8.0,pytest-cov>=4.1,pytest-asyncio>=0.23,<1(asyncio_mode = "auto"),pytest-randomly>=4.1,httpx>=0.27,<1← dev only, not runtimeruff,mypy— not installed (per completeness review §3.2.30 deferred)
Frontend runtime (verified against package.json):
react@^18.3.1,react-dom@^18.3.1@tanstack/react-query@^5.101.0lucide-react@^0.453.0sonner@^1.5.0zustand@^4.5.5react-router-dom@^6.27.0(note: package name isreact-router-dom, notreact-router)@radix-ui/react-dialog@^1.1.2,@radix-ui/react-label@^2.1.0,@radix-ui/react-select@^2.1.2,@radix-ui/react-slot@^1.1.0,@radix-ui/react-tabs@^1.1.15← powerssrc/components/ui/(shadcn primitives)class-variance-authority@^0.7.0,clsx@^2.1.1,tailwind-merge@^2.5.4,ansi-styles@^6.2.3← shadcn helper stack
Frontend dev/test:
vitest@^4.1.9← unusually new version (worth a sanity check — current stable is 1.x/2.x; verify it's intentional)@testing-library/react@^16.3.2happy-dom@^20.10.6← not jsdom (vitest config line 13 explicitly says they don't need a DOM env: "Node 18+ exposesResponse/fetchglobals")typescript@^5.6.3,vite@^5.4.10,@vitejs/plugin-react@^4.3.3@types/node,@types/react,@types/react-dom,autoprefixer,postcss,tailwindcss@^3.4.14,tailwindcss-animate
External services: None. (The MFT path is one-way outbound to Gainwell MFT, polled via SFTP — not a service Cyclone depends on, it's a sink it writes to.)
OS-level: macOS Keychain (backed by keyring); SQLite / SQLCipher shared library; ssh host keys / known_hosts for SP13. SP23 would add tini + curl + nginx in container scenarios.
5.4 One-page data flow
┌──────────────────────────────┐
Operator drops file │ FastAPI backend │
on /upload ───────▶│ /api/parse-837|835|999| │
(or SFTP inbound) │ 277ca|ta1|270 │
│ │
│ parse → validate ──┐ │
│ ▼ │
│ reconcile ──▶ store ──▶ activity_events (un-chained)
│ │ │
│ │ └─▶ audit_log (SP11, chained)
│ ▼ (login / rejected / key events)
│ SQLite (encrypted optional)
│ │
│ ▼
│ pubsub.publish(<kind>)
└──────────┬───────────────────┘
│ NDJSON stream
▼
┌──────────────────────────────┐
│ React UI (Vite-served) │
│ /claims /remits /inbox │
│ /reconciliation /providers │
│ /acks /activity /batches │
│ /upload /dashboard /health │
│ drill stack everywhere │
└──────────────────────────────┘
6. Sub-project inventory
6.1 Shipped SPs (in main)
Each row's "Spec" / "Plan" point to the design spec and implementation plan on disk. SPs without a plan file on disk shipped via feat(spN) commits on main whose commit messages carry the implementation order; those commit logs substitute for a plan file. Closing the plan-file gap is on the round-3 backlog.
| SP | Title | Spec | Plan | Acceptance |
|---|---|---|---|---|
| SP1 | Production-readiness: in-memory store, GET endpoints, react-query, reference notes | spec | plan | Met |
| SP2 | DB + reconciliation: SQLite, 7-state lifecycle, auto-match on 835 ingest, manual reconciliation UI | spec | plan | Met |
| SP3 | EDI features: 837P rules (R034/R035), CAS reason codes, 999 ACK, TA1 ACK, 270/271 | spec | plan | Met |
| SP4 | Claim drawer + Remit drawer + Cmd-K search + batch diff + CSV export | spec | plan | Met |
| SP5 | Live tail: NDJSON pubsub on Claims/Remittances/Activity, heartbeat, reconnect | spec | plan | Met |
| SP6 | Inbox workflow: 4 lanes (Rejected/Candidates/Unmatched/Done today), 4-field scoring, bulk actions | No dedicated spec on disk — SP6 ships via the SP4 spec's coverage and the workflow-automation plan | plan | Met |
| SP7 | Per-line adjustment audit: 837 SV1 ↔ 835 SVC strict match, line reconciliation tab | spec | plan | Met |
| SP8 | Outbound 837P serializer: single-claim download + bundle resubmit ZIP | spec (the spec's own title mistakenly says "Sub-project 7"; the file maps to SP8) | plan | Met |
| SP9 | Multi-payer, multi-NPI, SFTP stub | spec | (no plan file on disk; feat(sp9) commits on main) |
Met |
| SP10 | 277CA + Payer-Rejected lane | (no dedicated spec on disk; covered inline in parse-decide spec and cyclone/parsers/parse_277ca.py) |
(no plan file on disk; feat(sp10) commits on main) |
Met |
| SP11 | Tamper-evident audit log | (no dedicated spec on disk; cyclone/audit_log.py:116 append_event and :190 verify_chain are the implementation) |
(no plan file on disk; feat(sp11) commits on main) |
Met |
| SP12 | Encryption at rest (SQLCipher) | (no dedicated spec on disk; cyclone/db_crypto.py is the implementation) |
(no plan file on disk; feat(sp12) commits on main) |
Met |
| SP13 | SFTP wire-up (paramiko) | (no dedicated spec on disk; extends SP9; cyclone/clearhouse/__init__.py holds SftpClient) |
(no plan file on disk; feat(sp13) commits on main) |
Met |
| SP14 | 5-lane Inbox UI | (no dedicated spec on disk; extends SP6) | (no plan file on disk; feat(sp14) commits on main) |
Met |
| SP15 | SQLCipher key rotation | (no dedicated spec on disk; extends SP12) | (no plan file on disk; feat(sp15) commits on main) |
Met |
| SP16 | Live MFT polling scheduler | (no dedicated spec on disk; cyclone/scheduler.py is the implementation; idempotency via processed_inbound_files from migration 0011) |
(no plan file on disk; feat(sp16) commits on main) |
Met |
| SP17 | Encrypted DB backups | spec | (no plan file on disk; feat(sp17) commits on main) |
Met |
| SP18 | Structured logging | spec | (no plan file on disk; feat(sp18) commits on main) |
Met |
| SP19 | Security hardening + health probe | spec | (no plan file on disk; feat(sp19) commits on main) |
Met |
| SP20 | NPI checksum + Tax ID format validation | spec | (no plan file on disk; feat(sp20) commits on main) |
Met |
| SP21 | CycloneStore split (in-flight) + Universal drilldown (shipped) | split spec, drilldown spec | store-split plan, drilldown plan | Drilldown Met; split In-flight (refactor/store-split branch, not yet on main) |
| SP22 | Parse-decide workflow + Pipeline agent | parse-decide spec, pipeline spec | parse-decide plan, pipeline plan | 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 | plan | Met (no code paths change; only docs + __main__.py WARNING + 3 skills addenda) |
6.2 Backlog (specs awaiting plans, or vice versa)
| 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 |
| 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 |
| 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 |
pre-commit / Makefile / CONTRIBUTING.md / .editorconfig / ruff config |
Open | Completeness review §3.2.30 |
CI workflow file (.github/workflows/) |
Open | §8.1 #3 references CI; no CI file exists today |
PHI fixtures flagged as PHI (docs/prodfiles/PHI.md or equivalent) |
Open | Completeness review §3.2.23 |
Delete stale backend/src/cyclone/workflow/__pycache__/ |
Open | Empty dead-code directory (§5.1) |
EHR/PM integration (webhook in/out, callable POST /api/parse-837) |
Open | Completeness review §5.1 |
| COB / secondary-claim generator | Open | Completeness review §3.1.12 + §5.1 |
| Real-time eligibility round-trip (SFTP pickup + SOAP + retry + 271 parse) | Open | Completeness review §3.1.8 + §5.1 |
| 837I / 837D / 834 / 820 / 275 / 278 / 276/277 transaction types | Open | Completeness review §2.1 |
| NPPES NPI registry lookup | Open | Completeness review §3.1.10 |
| ICD-10 / HCPCS / NDC vocabulary tables | Open | Completeness review §3.1.11 |
6.3 Traceability matrix (compressed)
This matrix is the single index. Each row points to a requirement and where it's realized. A change to a requirement must update its row; a change to an SP must update the matching FRs/NFRs.
| ID | SP | Spec | Plan | Key tests |
|---|---|---|---|---|
| FR-1 | SP1 | production-readiness | production-readiness | test_parse_837.py, test_prodfiles_smoke.py |
| FR-2 | SP1, SP3 | production-readiness + edi-features | production-readiness + edi-features | test_parse_835.py, test_service_line_payments.py |
| FR-3 | SP2 | db-reconciliation | db-reconciliation | test_db.py, test_api_parse_persists.py |
| FR-4 | SP2 | db-reconciliation | db-reconciliation | test_db_migrate.py |
| FR-5 | SP2 | db-reconciliation | db-reconciliation | test_reconcile.py, test_store_reconcile.py |
| FR-6 | SP2 | db-reconciliation | db-reconciliation | test_models.py, test_reconcile.py |
| FR-7 | SP2 | db-reconciliation | db-reconciliation | test_reconcile.py, test_reconcile_line_level.py, test_inbox_endpoints.py, test_api_gets.py |
| FR-8 | SP3, SP20 | edi-features + npi-validation | edi-features | test_validator.py, test_validation_r200_r210.py |
| FR-9 | SP3 | edi-features | edi-features | test_acks.py, test_api_parse_persists_ack.py |
| FR-10 | SP3 | edi-features | edi-features | test_api_999.py, test_models_999.py, test_parse_999.py |
| FR-11 | SP3 | edi-features | edi-features | test_api_ta1.py, test_parse_ta1.py |
| FR-12 | SP3 | edi-features | edi-features | test_parse_270.py, test_parse_271.py, test_api_eligibility.py |
| FR-13 | SP10 | parse-decide-workflow (277CA section) | parse-decide-workflow | test_parse_277ca.py, test_apply_277ca_rejections.py |
| FR-14 | SP6, SP10, SP14 | workflow-automation + parse-decide-workflow | workflow-automation | test_inbox_lanes.py, test_inbox_endpoints.py, test_lane_filter_acknowledged.py |
| FR-15 | SP6 | workflow-automation | workflow-automation | test_scoring.py |
| FR-16 | SP4, SP7, SP21 | claim-drawer + line-reconciliation + universal-drilldown | claim-drawer + line-reconciliation + universal-drilldown | test_api_claim_detail.py, test_store_claim_detail.py |
| FR-17 | SP4 | claim-drawer | claim-drawer | test_batch_diff.py |
| FR-18 | SP4 | claim-drawer | claim-drawer | useSearch.test.ts, SearchResults.test.tsx |
| FR-19 | SP4 | claim-drawer | claim-drawer | ExportCsvButton.test.tsx, csv.test.ts |
| FR-20 | SP5 | live-tail | live-tail | test_api_stream_live.py, useTailStream.test.ts, tail-stream.test.ts |
| FR-21 | SP5 | live-tail | live-tail | TailStatusPill.test.tsx, useMergedTail.test.ts |
| FR-22 | SP7 | line-reconciliation | line-reconciliation | test_reconcile_line_level.py, test_api_line_reconciliation.py |
| FR-23 | SP8 | serialize-837 | serialize-837 | test_serialize_837.py, test_api_serialize_837.py |
| FR-24 | SP8 | serialize-837 | serialize-837 | test_api_batch_export_837.py |
| FR-25 | SP9 | multi-payer-npi-sftp | (none on disk) | test_providers_seed.py, test_payer_config_loading.py |
| FR-26 | SP13 | multi-payer-npi-sftp (extends) | (none on disk) | test_sftp_paramiko.py, test_sftp_stub.py |
| FR-27 | SP16 | (none on disk) | (none on disk) | test_scheduler.py, test_api_scheduler.py |
| FR-28 | SP12 | (none on disk) | (none on disk) | test_db_crypto.py, test_security.py |
| FR-29 | SP15 | (none on disk) | (none on disk) | test_api_rotate_key.py |
| FR-30 | SP17 | encrypted-backup | (none on disk) | test_backup_crypto.py, test_backup_service.py, test_backup_scheduler.py, test_api_backup.py, test_cli_backup.py |
| FR-31 | SP18 | structured-logging | (none on disk) | test_logging_formatter.py, test_logging_scrubber.py, test_logging_setup.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-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-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-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/ |
7. Acceptance criteria — per-SP (compressed)
These are the existing per-SP acceptance checklists as captured in their design specs, condensed into one table. A new engineer can use this to know whether a given SP is "really done" without reading 22 specs end-to-end.
| SP | Acceptance criterion | Verified by |
|---|---|---|
| SP1 | pytest passes; /api/claims, /api/remittances, /api/providers, /api/activity, /api/batches, /api/batches/{id} return JSON; CORS allowlist stays http://localhost:5173; bind stays 127.0.0.1 |
test_api_gets.py + manual smoke |
| SP2 | DB at ~/.local/share/cyclone/cyclone.db; migrations apply on first run; 7-state Claim lifecycle; reconciliation runs on 835 ingest; /reconciliation page works |
test_db*.py, test_reconcile.py, test_reconcile_line_level.py, test_inbox_endpoints.py |
| SP3 | R034/R035/R200/R210 enforced; CAS reason codes surfaced; 999 in + out; TA1 in; 270/271 builder + parser | test_validator*.py, test_acks.py, test_api_999.py, test_api_ta1.py, test_parse_270.py, test_parse_271.py |
| SP4 | Drawer opens with URL sync; Cmd-K search returns results across resources; batch diff renders added/removed/changed; CSV exports stream | ClaimDrawer*.test.tsx, useSearch.test.ts, BatchDiff.test.tsx, ExportCsvButton.test.tsx |
| SP5 | /api/{claims,remittances,activity}/stream returns NDJSON with snapshot + snapshot_end + heartbeat; client flips to stalled after 30s silence; reconnect ladder capped at 30s |
test_api_stream_live.py, TailStatusPill.test.tsx |
| SP6 | /inbox shows 5 lanes (4 in SP6; 5th added in SP10); scoring tiers visible; bulk actions call the right endpoint |
test_inbox_lanes.py, BulkBar.test.tsx |
| SP7 | Line reconciliation tab in ClaimDrawer matches 837 SV1 ↔ 835 SVC strictly; unmatched-line warnings surfaced | test_reconcile_line_level.py, test_api_line_reconciliation.py |
| SP8 | 113 prodfiles parse → serialize → parse back to the same claim_id; ZIP bundle export works |
test_prodfiles_smoke.py, test_api_batch_export_837.py |
| SP9 | config/payers.yaml loads (via pyyaml); new providers/payers/payer_configs tables populate; clearhouse.submit writes to staging_dir (stub) |
test_providers_seed.py, test_payer_config_loading.py |
| SP10 | 277CA parse + persist; payer_rejected stamp applied monotonically; lane 2 in /inbox renders |
test_parse_277ca.py, test_apply_277ca_rejections.py |
| SP11 | audit_log hash chain verifies via GET /api/admin/audit-log/verify; any break is detected |
test_audit_log.py, test_api_audit_log.py |
| SP12 | db_crypto.is_encryption_enabled() returns True when Keychain entry exists and sqlcipher3 installed; otherwise falls back with WARNING |
test_db_crypto.py |
| SP13 | SftpClient actually pushes to mft.gainwelltechnologies.com:…; credentials read from Keychain via keyring at call time |
test_sftp_paramiko.py (unit); integration via cyclone-pipeline |
| SP14 | All 5 lanes render in /inbox; bulk acknowledge action drops payer-rejected claims without erasing the original rejection |
test_inbox_endpoints_sp7.py, test_lane_filter_acknowledged.py |
| SP15 | POST /api/admin/db/rotate-key runs PRAGMA rekey; emits db.key_rotated with old + new fingerprints; concurrent calls serialized |
test_api_rotate_key.py |
| SP16 | Scheduler ticks every interval; per-file try/except keeps the loop alive; processed_inbound_files de-duplicates |
test_scheduler.py, test_api_scheduler.py |
| SP17 | Backup scheduler ticks every 24h; encrypted .bin + .meta.json written; restore via 2-step token works; retention prunes at 30 days |
test_backup_*.py |
| SP18 | All logs emit as NDJSON by default; PiiScrubber redacts NPIs/SSNs/DOBs/names; CYCLONE_LOG_JSON=0 falls back to CycloneDevFormatter |
test_logging_*.py |
| SP19 | 413 on oversize body, 429 on rate-limit, security headers present; api.request_rejected audit event on 413/429; GET /api/health returns rich payload |
test_security.py, test_api.py:33 |
| SP20 | NPI Luhn catches 99% of typos; EIN rejects reserved prefixes; warning-only (placeholder NPIs in fixtures don't block ingest); CLI + API surface | test_npi.py, test_api_validate_provider.py, test_cli_validate.py |
| SP21 | CycloneStore is split into store_persistence / store_reconcile / store_queries / store_mappers; public store API unchanged; DrillStackProvider + DrillDrawerHeader shell used by every drillable cell |
test_store.py (split modules), DrillStackProvider.test.tsx, drawer-key tests |
| SP22 | Re-parsing an 837 into a fresh batch does not 409 on claim_id collision; parse-then-decide workflow checks find_existing_batch_for_claim; pipeline agent runs 7 phases end-to-end |
test_api_parse_persists.py, test_prodfiles_smoke.py, pipeline integration |
8. Definition of Done
8.1 Project-level DoD (the project as a whole is "done" when…)
- All FRs in §3 are implemented and tested — verifiable by walking the §6.3 traceability matrix and confirming each row links to either a passing test or an §2.2 out-of-scope entry.
- All NFRs in §4 are met or have a documented exception — NFR-1 through NFR-18 are each either met (with a citation) or carry an explicit exception in this section.
- All shipped SPs in §6.1 have an Acceptance row marked "Met" and the corresponding tests pass. Tests are run via
pytest(backend) andnpm test(frontend) per §9.4; the project has no CI workflow today (round-3 backlog item — see §6.2). A release-time gate should run the suites and fail if any test is red. - All backlog items in §6.2 are either scheduled, explicitly deferred with rationale, or removed — testable as a checklist walk.
- All assumptions in §10 are still valid — re-checked at release time; an invalid assumption is either promoted to a backlog item or rolled into the next release.
- All four reviewer questions (components / data model / dependencies / DoD) have independent agreement from at least two reviewers — the meta-DoD: a doc with this scope is "done" when two reviewers can independently describe the system from it and agree.
8.2 Per-release DoD (the next release is "done" when…)
- The new SP's design spec has been self-reviewed (every claim cited with line/file, every assumption recorded).
- The implementation plan has TDD-shaped tasks with per-task acceptance criteria.
- Every task's tests are green in
pytest(backend) andnpm test(frontend) andnpm run build(frontend typecheck + build). - The new SP's Acceptance row in §6.1 is updated to "Met".
- New audit events (if any) are listed in the audit-log test fixtures (
backend/tests/test_audit_log.py). - Migration files (if any) follow the
NNNN_*.sqlordering, append to this doc's §5.2 table, and (post-round-3) update the migration manifest (R-13). - The README's project-layout section is updated if any module is added/renamed/removed.
8.3 Per-task DoD (the smallest unit of work is "done" when…)
- The code change is in a single commit (or a small, reviewable series).
- The matching test exists and passes.
- No existing test regresses.
- The commit message names the SP and the FR/NFR it advances (e.g.,
feat(sp19): … (FR-32, NFR-8)). - If the task introduces a migration, it's added to
backend/src/cyclone/migrations/with the next sequentialNNNNand the new schema is reflected in §5.2.
9. Test strategy
9.1 Taxonomy
| Layer | Tool | Where | What it covers |
|---|---|---|---|
| Backend unit | pytest |
backend/tests/ |
Pure-function logic: cyclone.reconcile.*, cyclone.scoring.*, cyclone.npi.*, cyclone.cas_codes, cyclone.audit_log.append_event, cyclone.edi.filenames |
| Backend integration | pytest (TestClient) |
backend/tests/test_api_*.py |
FastAPI route round-trips, content negotiation, NDJSON streaming, auth/rate-limit/SP19, 999/TA1/277CA parse endpoints, scheduler endpoints |
| Backend fixture-driven | pytest (tests/fixtures/*.txt) |
backend/tests/test_prodfiles_smoke.py |
113 + 19 + 1369 production files; round-trip property |
| Backend SQL | pytest (in-memory SQLite) |
backend/tests/test_db*.py |
Schema, migrations, ORM, encryption round-trip |
| Frontend unit | vitest (*.test.ts(x)) |
src/lib/, src/hooks/, src/components/ui/ |
Pure functions, hooks, primitives |
| Frontend component | vitest + Testing Library |
src/components/, src/pages/ |
Drawers, lane rendering, drill stack, export buttons |
| Smoke | manual / shell | scripts/smoke.sh (planned in SP23) |
End-to-end parse → reconcile → export flow |
9.2 Fixture sources
backend/tests/fixtures/minimal_*.txt— minimal valid 270 / 271 / 277CA / 835 / 837P / 999 / TA1 / co_medicaid / unbalanced_835 — written by hand, used in unit tests.docs/prodfiles/claims/*.x12(113 files) — real-looking production files used bytest_prodfiles_smoke.pyfor the round-trip property.docs/prodfiles/FromHPE/(1369 files) — full inbound directory from the production SFTP path.docs/prodfiles/837p-from-axiscare/(19 files) — production 837Ps from AxisCare.docs/prodfiles/835fromco/(5 files) — production 835s from CO Medicaid.
⚠ PHI warning: prodfiles contain real-looking patient data. Even in a single-operator tool, fixtures should be flagged as PHI (completeness review §3.2.23 — flagged but not yet actioned; for v1 they're kept under docs/prodfiles/ with no additional access control).
9.3 Coverage goals
- Pure-function modules (
reconcile,scoring,npi,cas_codes,parsers,edi.filenames,batch_diff): ≥ 90% line coverage.pytest-covis installed; the gate (--cov-fail-under=90) is not enforced today and is a round-3 backlog item. - Store facade (
store, and the futurestore_persistence / store_reconcile / store_queries / store_mappersmodules): every public method has at least one happy-path and one failure-path test. - API routes: every route has a happy-path test; error paths (404, 409, 413, 415, 422, 423, 429, 500) covered by at least one test each.
- Round-trip property (SP8): 100% of
docs/prodfiles/claims/*.x12must parse → serialize → parse to the sameclaim_id. - Live tail (SP5): one happy snapshot → live item → disconnect cleanup test per endpoint.
9.4 How to run
# Backend
cd backend
.venv/bin/pytest # 964 tests collected (2026-06-23)
.venv/bin/python -m cyclone serve
# Frontend
npm run typecheck
npm test # 73 test files
npm run build
# Smoke (planned in SP23)
bash scripts/smoke.sh
9.5 What is NOT covered (explicit)
- Component / E2E browser tests (Playwright). Deferred; the local-only threat model doesn't justify them. (Completeness review §3.2 + SP23 §14.)
- Property-based tests (Hypothesis) for the parser. Could add; not scheduled.
- Mutation tests (mutmut / cosmic-ray). Not scheduled.
- Performance / load tests. Single-operator scale; not scheduled.
- NPPES round-trip validation (only Luhn checksum is covered; SP20).
- External-integration tests against the real Gainwell MFT. The pipeline agent covers this in
cyclone-pipeline/; out of scope for this repo. - CI workflow. Not present in the repo; release gating is manual today.
10. Assumptions
Recorded explicitly so a new engineer can challenge any of them.
| # | Assumption | If wrong, what changes |
|---|---|---|
| A1 | One operator, one host, one trading partner (CO Medicaid). | Multi-tenant / multi-payer scope would require SP23 + multi-payer NPI expansion + COB + AS2/AS4. |
| A2 | The host runs macOS (Keychain via keyring) OR Ubuntu (Docker secrets available, SP23). |
Linux-without-Docker / Windows would require a different secrets backend (keyring already dispatches via Secret Service / kwallet on Linux; Windows would use Credential Manager). |
| A3 | The host is physically secure; LUKS / FileVault is the operator's concern, not Cyclone's. | If not, the SQLCipher-encrypted-DB posture is insufficient; per-file encryption of prodfiles/ and sftp_staging/ becomes mandatory. |
| A4 | The operator can hand the 837 ZIP to the Gainwell MFT UI manually. | Removes the need for a built-in SFTP submit (SP13); the SFTP wire stays stub. |
| A5 | The 835 lands the following Monday (CO Medicaid payment cycle) and is verified by a separate check-835 subcommand (in cyclone-pipeline/), not inline. |
Would require a different round-trip contract; affects SP22 pipeline agent. |
| A6 | ICD-10 / HCPCS / NDC vocabulary is not Cyclone's responsibility. | Would require vendoring vocab CSVs and adding vocabulary checks (FR + tests). |
| A7 | NPPES NPI registry lookup is not needed (offline Luhn is enough). | Would require scheduled NPPES download + online check + new FR. |
| 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. |
| 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. |
| 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). |
11. Risks and known gaps
Pulled forward from docs/reviews/2026-06-20-cyclone-completeness-review.md. Status as of 2026-06-23.
| 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-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-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-6 | No COB / secondary-claim generator. | Completeness review §3.1.12 | Open — explicitly out of scope per §2.2. |
| R-7 | No real-time eligibility round-trip. | Completeness review §3.1.8 | Open — 270/271 are file-exchange only per SP3. |
| 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-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-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-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-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-16 | PHI fixtures not flagged as PHI. | Completeness review §3.2.23 | Open — not scheduled. |
| R-17 | /api/health not rich (before SP19). |
Completeness review §3.2.24 | Closed by SP19 — api_routers/health.py:28-40 returns the rich snapshot via get_health_snapshot(). |
| R-18 | No CSP / no security headers (before SP19). | Completeness review §3.2.25 | Closed by SP19 — SecurityHeadersMiddleware in cyclone/security.py. |
| R-19 | No request body size / rate limit (before SP19). | Completeness review §3.1.4 | Closed by SP19 — BodySizeLimitMiddleware + RateLimitMiddleware in cyclone/security.py. |
| R-20 | No 277CA support (before SP10). | Completeness review §3.1.7 | Closed by SP10. |
| 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-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-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-27 | backend/src/cyclone/workflow/__pycache__/ is dead bytecode. |
§5.1 | Open — recommend deletion. |
12. References
12.1 Design specs
All under docs/superpowers/specs/:
- 2026-06-19-cyclone-837p-parser-design.md — pre-SP1, superseded by
production-readiness - 2026-06-19-cyclone-db-reconciliation-design.md — SP2
- 2026-06-19-cyclone-production-readiness-design.md — SP1
- 2026-06-20-cyclone-claim-drawer-design.md — SP4 (no inbox section; SP6 ships from the workflow-automation plan)
- 2026-06-20-cyclone-edi-features-design.md — SP3
- 2026-06-20-cyclone-line-reconciliation-design.md — SP7
- 2026-06-20-cyclone-live-tail-design.md — SP5
- 2026-06-20-cyclone-multi-payer-npi-sftp-design.md — SP9 (extended by SP13)
- 2026-06-20-cyclone-serialize-837-design.md — SP8 (spec's own title mistakenly says "Sub-project 7")
- 2026-06-21-cyclone-claims-unique-constraint-and-409-ux-design.md — pre-SP22, superseded by
parse-decide-workflow - 2026-06-21-cyclone-encrypted-backup-design.md — SP17
- 2026-06-21-cyclone-npi-validation-design.md — SP20
- 2026-06-21-cyclone-parse-decide-workflow-design.md — SP22 (parse-decide) + covers 277CA (SP10)
- 2026-06-21-cyclone-pipeline-agent-design.md — SP22 (pipeline agent)
- 2026-06-21-cyclone-security-hardening-design.md — SP19
- 2026-06-21-cyclone-skill-catalog-design.md — documentation meta-project (not mapped to a behavioral SP)
- 2026-06-21-cyclone-store-split-design.md — SP21 (split)
- 2026-06-21-cyclone-structured-logging-design.md — SP18
- 2026-06-21-cyclone-universal-drilldown-design.md — SP21 (drilldown)
- 2026-06-22-cyclone-ubuntu-docker-deployment-design.md — SP23 candidate; awaiting plan + user product-fork decision
12.2 Implementation plans
All under docs/superpowers/plans/:
- 2026-06-19-cyclone-837p-parser.md — pre-SP1, superseded
- 2026-06-19-cyclone-db-reconciliation.md — SP2
- 2026-06-19-cyclone-production-readiness.md — SP1
- 2026-06-20-cyclone-claim-drawer.md — SP4
- 2026-06-20-cyclone-edi-features.md — SP3
- 2026-06-20-cyclone-line-reconciliation.md — SP7
- 2026-06-20-cyclone-live-tail.md — SP5
- 2026-06-20-cyclone-serialize-837.md — SP8
- 2026-06-20-cyclone-workflow-automation.md — SP6
- 2026-06-21-cyclone-claims-unique-constraint-and-409-ux.md — pre-SP22, superseded
- 2026-06-21-cyclone-parse-decide-workflow.md — SP22
- 2026-06-21-cyclone-pipeline-agent.md — SP22
- 2026-06-21-cyclone-skill-catalog.md — documentation meta-project
- 2026-06-21-cyclone-store-split.md — SP21 (split, in-flight)
- 2026-06-21-cyclone-universal-drilldown.md — SP21 (drilldown)
12.3 Reviews and reference
- 2026-06-20-cyclone-completeness-review.md — industry-scope gap analysis.
docs/reference/835.md,docs/reference/837p.md,docs/reference/co-medicaid.md,docs/reference/x12naming.md— condensed X12 + CO Medicaid notes.
12.4 External
- HIPAA Security Rule §164.312 — audit controls, encryption, access control.
- X12 005010X222A1 (837P), 005010X221A1 (835), 005010X231A1 (999), 005010X279A1 (270/271), 005010X214 (277CA), 005010X230 (TA1).
- CAQH CORE Phase II/III (real-time eligibility — out of scope per §2.2).
- CMS NPI Luhn specification.
- NPPES API (NPI registry — out of scope per §2.2 + A7).
- OWASP password-storage cheat sheet (argon2id parameters — SP23).
- paramiko, FastAPI, SQLAlchemy 2.0, Pydantic v2, structlog, TanStack Query v5 docs.
13. Reviewer checklist (for round 2 → round 3 closure)
For the two independent reviewers at the end of round 2 (re-verifying the corrections):
- 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?
- 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(notbackups) correction applied? Is theRejectionphantom removed? Is theActivityEventvsAuditLogdistinction clear (NFR-3 vs NFR-4)? - Dependencies — Are §5.3 runtime + tooling deps correct (no missing
pyyaml, no missingkeyring, nohttpxlisted as runtime,paramikoflagged as[sftp]extra,sqlcipher3flagged as[sqlcipher]extra, frontend test env ishappy-domnotjsdom, vitest pinned at^4.1.9flagged as unusual)? Are OS-level deps (Keychain viakeyring, SQLCipher C library,tini,nginxif SP23 ships) called out? - 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?
- 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/orsrc/? If not, that's a remaining gap. - 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
keyringaccurate now? - 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?
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).
End of round 2. Next: re-verify with reviewers (round 3) or escalate any remaining disagreement to the user.