Files
cyclone/docs/REQUIREMENTS.md

74 KiB
Raw Permalink Blame History

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 via CYCLONE_DB_URL); optionally encrypted at rest via SQLCipher (SP12).
  • Threat model: a process on the same host, plus a deliberate HTTP-layer login (no remote unauthenticated read or write). File-system threats (stolen/imaged drive) are still the primary concern; SQLCipher at rest + the macOS Keychain handle those. The SP23 product fork changes the threat model to "remote operator on LAN."

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 SP1SP22)

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 + SP9SP17
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 pull keeps 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 (5074, 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 (initiateconfirm 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, 8089). 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

3.1 In-window rebill pipeline FRs (SP41)

The FR-RB-* IDs are SP41-specific functional requirements for the in-window rebill pipeline. They sit alongside FR-1..FR-38 in §3 but use the RB prefix to indicate they cover the rebill sub-domain. Each row links to its implementation module under cyclone.rebill.* (or, for FR-RB-5, the store-side cyclone.store.submission_dedup helper that the pipeline integrates with).

ID Requirement SP
FR-RB-1 Visit-to-835 reconciliation on (member_id, procedure, DOS). Match key plus a best-of-N across duplicate SVC rows (per-SVC charge tolerance within 5% to absorb legitimate AxisCare-vs-835 differences). Implemented in cyclone.rebill.reconcile.reconcile_visits_to_835. SP41
FR-RB-2 CARC-aware filter. Excluded reasons (CO-45 / CO-26 / CO-129) trip the EXCLUDED_CARC outcome; review reasons (PI-16, OA-18, etc.) produce REBILLED_A with needs_review=true. Implemented in cyclone.rebill.carc_filter.decide_carc. SP41
FR-RB-3 120-day timely-filing gate with per-batch override. Default window = 120 days from DOS as of run date; --override-filing/--no-override-filing CLI flag (and matching API flag) relaxes the gate for past-window visits. Implemented in cyclone.rebill.timely_filing.timely_filing_decision. SP41
FR-RB-4 Pipeline A (frequency-7 replacement) for denied/partial visits + Pipeline B (member-week batched fresh 837Ps) for NOT_IN_835 visits. Pipeline A emits a single 837P per visit with CLM05-3 = '7'; Pipeline B batches by (member_id, ISO-week). Implemented in cyclone.rebill.pipeline_a and cyclone.rebill.pipeline_b. SP41
FR-RB-5 Claim-id dedup at SFTP pre-flight (30-day window, configurable). Implemented in cyclone.store.submission_dedup.check_duplicate; integrated into cyclone.submission.submit_file which raises DuplicateClaimError before any upload bytes are sent. SP41
FR-RB-6 Summary CSV with REBILLED_A / REBILLED_B / EXCLUDED_CARC / EXCLUDED_TIMELY_FILING / EXCLUDED_PAYER / EXCLUDED_NO_EVV rows plus a per-disposition tally. Implemented in cyclone.rebill.summary.write_summary_csv and surfaced by cyclone.rebill.run.run_rebill. SP41

4. Non-functional requirements

ID NFR Source / SP
NFR-1 Always bind 0.0.0.0 + auth. Backend binds 0.0.0.0:8000 (overridable via CYCLONE_HOST / CYCLONE_PORT); CORS allowlist is exact (http://localhost:5173); login required (bcrypt + HttpOnly session cookie; first admin bootstrapped from env vars CYCLONE_ADMIN_USERNAME + CYCLONE_ADMIN_PASSWORD); dev/test escape hatch via CYCLONE_AUTH_DISABLED=1 (logs WARNING at boot). Reachability is controlled by the host firewall / compose port publishing, not the bind address. SP1 + auth (2026-06-23 merge) + SP24 (doc reconciliation)
NFR-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 916 ship without on-disk plan files — see §6.1 — but their feat(spN) commits are on main and carry their own commit-message plan.) (project-wide)
NFR-17 Fail-soft posture. Reconciliation crashes don't lose the 835; the activity event records the failure. SP2
NFR-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,<3
  • click>=8.1,<9
  • fastapi>=0.110,<1
  • uvicorn[standard]>=0.27,<1 (the [standard] extra pulls httptools, uvloop, websockets, watchfiles)
  • python-multipart>=0.0.9,<1
  • sqlalchemy>=2.0,<3
  • pyyaml>=6.0,<7loads config/payers.yaml (SP9)
  • keyring>=25.0,<26macOS Keychain backend used by cyclone/secrets.py (SP12/SP13/SP15); on Linux dispatches to Secret Service / kwallet
  • cryptography>=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,<1dev only, not runtime
  • ruff, mypynot 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.0
  • lucide-react@^0.453.0
  • sonner@^1.5.0
  • zustand@^4.5.5
  • react-router-dom@^6.27.0 (note: package name is react-router-dom, not react-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 ← powers src/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.9unusually new version (worth a sanity check — current stable is 1.x/2.x; verify it's intentional)
  • @testing-library/react@^16.3.2
  • happy-dom@^20.10.6not jsdom (vitest config line 13 explicitly says they don't need a DOM env: "Node 18+ exposes Response / fetch globals")
  • 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…)

  1. 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.
  2. 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.
  3. All shipped SPs in §6.1 have an Acceptance row marked "Met" and the corresponding tests pass. Tests are run via pytest (backend) and npm 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.
  4. All backlog items in §6.2 are either scheduled, explicitly deferred with rationale, or removed — testable as a checklist walk.
  5. 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.
  6. 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…)

  1. The new SP's design spec has been self-reviewed (every claim cited with line/file, every assumption recorded).
  2. The implementation plan has TDD-shaped tasks with per-task acceptance criteria.
  3. Every task's tests are green in pytest (backend) and npm test (frontend) and npm run build (frontend typecheck + build).
  4. The new SP's Acceptance row in §6.1 is updated to "Met".
  5. New audit events (if any) are listed in the audit-log test fixtures (backend/tests/test_audit_log.py).
  6. Migration files (if any) follow the NNNN_*.sql ordering, append to this doc's §5.2 table, and (post-round-3) update the migration manifest (R-13).
  7. 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…)

  1. The code change is in a single commit (or a small, reviewable series).
  2. The matching test exists and passes.
  3. No existing test regresses.
  4. The commit message names the SP and the FR/NFR it advances (e.g., feat(sp19): … (FR-32, NFR-8)).
  5. If the task introduces a migration, it's added to backend/src/cyclone/migrations/ with the next sequential NNNN and 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 by test_prodfiles_smoke.py for 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-cov is installed; the gate (--cov-fail-under=90) is not enforced today and is a round-3 backlog item.
  • Store facade (store, and the future store_persistence / store_reconcile / store_queries / store_mappers modules): 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/*.x12 must parse → serialize → parse to the same claim_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 Partialapi_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 SP19api_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 SP19SecurityHeadersMiddleware in cyclone/security.py.
R-19 No request body size / rate limit (before SP19). Completeness review §3.1.4 Closed by SP19BodySizeLimitMiddleware + 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/:

12.2 Implementation plans

All under docs/superpowers/plans/:

12.3 Reviews and reference

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):

  1. Components — Do §5.1 (backend modules) + §5.1 (frontend dirs) list every existing module that ships PHI or serves a state-changing route? Are the boundaries (DB / ORM / reconcile / store / api / pubsub / clearhouse / batch_diff / edi.filenames) drawn correctly? Can you, from §5.1 alone, draw the import graph?
  2. Data model — Are all 12 migrations listed in §5.2 with their purpose? Are the two worktree-only migrations (0013, 0014) flagged as in-flight? Are all 18 ORM models in §5.2? Is the db_backups (not backups) correction applied? Is the Rejection phantom removed? Is the ActivityEvent vs AuditLog distinction clear (NFR-3 vs NFR-4)?
  3. Dependencies — Are §5.3 runtime + tooling deps correct (no missing pyyaml, no missing keyring, no httpx listed as runtime, paramiko flagged as [sftp] extra, sqlcipher3 flagged as [sqlcipher] extra, frontend test env is happy-dom not jsdom, vitest pinned at ^4.1.9 flagged as unusual)? Are OS-level deps (Keychain via keyring, SQLCipher C library, tini, nginx if SP23 ships) called out?
  4. Definition of Done — Is §8 testable? Project DoD #6 now points at the reviewer-agreement meta-criterion. Per-release + per-task DoDs each independently checkable? FR-7 / FR-33 traceability rows corrected to point at existing test files and the correct endpoint name?
  5. Traceability — Pick any FR-NN in §3. Can you trace it to an SP, a spec on disk (or an honest "no dedicated spec on disk" note), a plan on disk (or an honest "no plan file on disk; feat(spN) commits substitute" note), and at least one test file in backend/tests/ or src/? If not, that's a remaining gap.
  6. Assumptions — Are §10 assumptions falsifiable? Could a new engineer challenge any of them and find the test that proves/disproves them? Is A2's reference to keyring accurate now?
  7. Risks — Are §11 risks current (R-11 corrected to 2,172; R-12 added; R-25/R-26/R-27 added)? Compare against docs/reviews/2026-06-20-cyclone-completeness-review.md. Is R-24 (SP23 fork) clearly flagged as awaiting user decision?

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.