diff --git a/.superpowers/skills/cyclone-api-router/SKILL.md b/.superpowers/skills/cyclone-api-router/SKILL.md index 6447533..4f3aa97 100644 --- a/.superpowers/skills/cyclone-api-router/SKILL.md +++ b/.superpowers/skills/cyclone-api-router/SKILL.md @@ -19,6 +19,10 @@ content negotiation + `tail_events`), and ~30 routes still inlined in `backend/src/cyclone/api.py`. The next refactor target is the parse endpoints. +## Auth gate (SP24) + +Every router declared in `backend/src/cyclone/api_routers/` **must** carry `dependencies=[Depends(matrix_gate)]` at the `APIRouter(...)` declaration — not on each individual endpoint. The gate lives at `backend/src/cyclone/auth/deps.py:107` and the role matrix is at `backend/src/cyclone/auth/permissions.py`. The roles are `admin / user / viewer`; `matrix_gate` returns 401 when there's no session and 403 when the role is below the endpoint's required role. When `AUTH_DISABLED` is True (conftest autouse fixture flips it; `CYCLONE_AUTH_DISABLED=1` in prod-by-mistake), the gate short-circuits to a synthetic admin — see the SP24 spec for the threat-model implications. New routers get the gate by default; the auth-aware convention is `router = APIRouter(dependencies=[Depends(matrix_gate)])`. + ## When to use - **Adding an endpoint.** You're adding a new GET / POST handler — diff --git a/.superpowers/skills/cyclone-spec/SKILL.md b/.superpowers/skills/cyclone-spec/SKILL.md index c7411ed..9f2ec93 100644 --- a/.superpowers/skills/cyclone-spec/SKILL.md +++ b/.superpowers/skills/cyclone-spec/SKILL.md @@ -10,9 +10,15 @@ a spec, a plan, an implementation branch, and a single atomic merge commit into `main`. This skill encodes the conventions so every increment follows the same shape and the commit history stays auditable. -As of this writing: **16 specs** in `docs/superpowers/specs/`, **12 plans** -in `docs/superpowers/plans/`, and SP numbers used through **SP21** (the -universal-drilldown design in progress). The next increment is **SP22**. +As of this writing: **17 specs** in `docs/superpowers/specs/`, **13 plans** +in `docs/superpowers/plans/`, and SP numbers used through **SP22**. **SP23** +is the Ubuntu + Docker + RBAC product fork (awaiting user decision); +**SP24** is the auth-posture alignment (docs-only). The next free increment +is **SP25** after SP24 lands. + +## Auth-aware spec template (SP24) + +The threat-model section in the canonical SP-N spec template (`## 1. Scope`, second-to-last bullet) used to read "no second party to authenticate; no second host to harden against." **That phrasing is stale as of 2026-06-23** — the auth work landed in `main` and every backend endpoint requires login. New specs should instead state the auth boundary explicitly: "the auth boundary is HTTP (login required, bcrypt + HttpOnly session cookie); file-system threats remain the local-only threat model (SQLCipher at rest, macOS Keychain). SP23 changes the threat model to LAN-bound remote operator." Reference: [`docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md`](../../../docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md). ## When to use diff --git a/.superpowers/skills/cyclone-tests/SKILL.md b/.superpowers/skills/cyclone-tests/SKILL.md index f1195f0..4286acc 100644 --- a/.superpowers/skills/cyclone-tests/SKILL.md +++ b/.superpowers/skills/cyclone-tests/SKILL.md @@ -7,7 +7,11 @@ description: "Cyclone pytest + vitest fixture patterns, prodfiles layout, backen The Cyclone test suite is split two ways: **backend pytest** (89 test files under `backend/tests/`, 13 flat fixtures in `backend/tests/fixtures/`, one autouse `conftest.py` that resets the DB per-test) and **frontend vitest** (59 `*.test.ts(x)` siblings across `src/`, two rendering styles — `@testing-library/react` and a custom `createRoot`+`Probe` shim). This skill codifies the conventions so additions stay consistent with what's already there. -As of this writing: **89 backend test files**, **13 flat backend fixtures**, **59 frontend `*.test.ts(x)` siblings**, and **23 prodfiles samples** across `docs/prodfiles/{837p-from-axiscare,835fromco,FromHPE,claims}/`. The next increment is **SP22**. +As of this writing: **89 backend test files**, **13 flat backend fixtures**, **59 frontend `*.test.ts(x)` siblings**, and **23 prodfiles samples** across `docs/prodfiles/{837p-from-axiscare,835fromco,FromHPE,claims}/`. The next increment is **SP24** (SP23 is the Ubuntu+Docker fork, awaiting user decision). + +## Auth flag (SP24) + +The autouse `conftest.py` fixture at `backend/tests/conftest.py` flips `cyclone.auth.deps.AUTH_DISABLED = True` for the entire test session, so every test runs without a login round-trip. **Any new test that reads `cyclone.auth.deps.AUTH_DISABLED` directly will see `True`** — that's the test-suite reality, not a production reality. If you need a test that exercises the real gate, import `from cyclone.auth.deps import matrix_gate` and call it directly with a `Request` whose `state` carries a real session, or flip the flag back inside the test and reset it on teardown. The startup WARNING (`backend/src/cyclone/__main__.py`) is silent in tests by default because the conftest sets the flag before `bootstrap.run()` is called via `import`. ## When to use diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4ecbf0c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,184 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office (Colorado Medicaid currently). It parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 / 005010X221A1) and also handles 999, TA1, 270, 271, and 277CA. Local-only by design: binds to `127.0.0.1`, requires login (auth boundary is HTTP; bcrypt + HttpOnly session cookie; first admin bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD` env vars; see SP24 spec for the full posture), no internet exposure. + +Stack: one Python process (FastAPI + uvicorn, port 8000) + one Node process in dev (Vite, port 5173). The authoritative state is a single SQLite file at `~/.local/share/cyclone/cyclone.db` (or SQLCipher at the same path when the macOS Keychain entry + `sqlcipher3` are both present). + +For the day-1 architecture read, see `docs/ARCHITECTURE.md` (process topology, module map, store facade, parser pipeline, pubsub). For the what-it-does read, see `docs/REQUIREMENTS.md` (FRs + NFRs + DoD). + +## Install + +```bash +# Backend (Python 3.11+) +cd backend +python -m venv .venv +.venv/bin/pip install -e '.[dev]' + +# Frontend (Node 20+) +cd .. +npm install +``` + +Optional backend extras: `pip install -e '.[sqlcipher]'` (encryption at rest, SP12) and `pip install -e '.[sftp]'` (real SFTP, SP13). + +## Dev (two terminals) + +```bash +# Terminal 1 — backend +cd backend +.venv/bin/python -m cyclone serve # default 127.0.0.1:8000 +# CYCLONE_PORT=... overrides port; CYCLONE_RELOAD=1 enables uvicorn --reload +# Or: .venv/bin/uvicorn cyclone.api:app --reload --port 8000 + +# Terminal 2 — frontend +npm run dev # Vite on http://localhost:5173 +``` + +Vite proxies `/api/*` to the backend at `http://127.0.0.1:${CYCLONE_PORT:-8000}` so relative-URL fetchers (the live-tail NDJSON streams in particular) resolve through the same origin. Override the backend port with `CYCLONE_PORT` in the frontend terminal too. + +Create `.env.local` at the repo root with `VITE_API_BASE_URL=http://127.0.0.1:8000`. Without it, the UI runs against the in-memory zustand store and real EDI parsing is disabled. + +## Test + +```bash +# Backend — full suite +cd backend && .venv/bin/pytest + +# Backend — one file +cd backend && .venv/bin/pytest tests/test_api_999.py -v + +# Backend — one test by node id +cd backend && .venv/bin/pytest tests/test_api_999.py::test_parse_999_endpoint_happy_path -v + +# Frontend — full suite +npm test # alias for `vitest run` + +# Frontend — one file +npx vitest run src/hooks/useFoo.test.ts + +# Frontend — typecheck +npm run typecheck + +# Frontend — build (tsc -b + vite build) +npm run build + +# Frontend — lint +npm run lint +``` + +**Conventions** (full detail in `.superpowers/skills/cyclone-tests/SKILL.md`): + +- Backend tests live under `backend/tests/test_*.py`. Two flavors: `test_api__.py` (FastAPI integration via `fastapi.testclient.TestClient`) and `test__.py` (pure-unit). Autouse `conftest.py` points `CYCLONE_DB_URL` at `tmp_path/test.db`, calls `db._reset_for_tests()` + `db.init_db()`, and wires a fresh `EventBus` onto `app.state`. +- Prodfiles (real EDI samples under `docs/prodfiles//`) are never read directly from a test — copy to `backend/tests/fixtures/.txt` first and reference as a module-level `Path` constant. The `fixtures/` dir is flat (no per-test subdirs). +- Frontend tests are siblings: `useFoo.ts` → `useFoo.test.ts`, `ClaimDrawer.tsx` → `ClaimDrawer.test.tsx`. Setup is `// @vitest-environment happy-dom` plus `(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;`. Mock the API at the module boundary with `vi.mock("@/lib/api", ...)`; stub fetch with `vi.stubGlobal("fetch", vi.fn().mockResolvedValue(...))`. `vitest.config.ts` sets `VITE_API_BASE_URL=http://test.local` so the `api` module doesn't throw `notConfiguredError` before the mock fires. +- Time-sensitive tests: frontend uses `vi.useFakeTimers()` + `vi.setSystemTime(...)` + `vi.advanceTimersByTime(ms)`. Backend passes explicit `datetime(...)` values. Don't add `await new Promise((r) => setTimeout(r, N))` — it's the legacy flaky pattern. + +## Project-scoped skills (`.superpowers/skills/`) + +Cyclone ships 8 skills under `.superpowers/skills/`. They auto-load by description match — no slash command needed. **Read the relevant skill before touching the matching subsystem.** + +| Skill | Owns | +|---|---| +| `cyclone-spec` | The SP-N spec → plan → implement → merge flow (branch shape, file paths, commit prefixes, PR title, merge shape). | +| `cyclone-tests` | pytest + vitest fixture patterns, prodfiles drop-in rule, determinism rules. | +| `cyclone-edi` | EDI parser/validator conventions (837P/835/999/270/271/277CA/TA1, R-codes, CAS mapping). | +| `cyclone-tail` | Live-tail streaming wire format and the `useTailStream` + `useMergedTail` + `TailStatusPill` hook triplet. | +| `cyclone-store` | `CycloneStore` facade, write-paths, pubsub event contract, SP21 split map. | +| `cyclone-api-router` | FastAPI router conventions (`api_routers/`, `api_helpers.py`), response/error-envelope shapes. | +| `cyclone-frontend-page` | React page conventions (TanStack Query `use` hook, drawer, URL state, sibling test). | +| `cyclone-cli` | CLI subcommand conventions (`cli.py`, exit codes, smoke tests). | + +## The SP-N increment flow + +Every feature ships as a numbered **SP-N increment**: spec → plan → implementation branch → single atomic merge into `main`. As of the last backfill, SP numbers are used through **SP22**; **SP23** is reserved for the Ubuntu + Docker + RBAC product fork (`docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md`, awaiting user decision); the next free increment is **SP24**. Read `cyclone-spec` before starting a new one. Non-negotiable shape: + +- **Branch:** `sp-` (e.g. `sp22-line-reconciliation`). +- **Spec path:** `docs/superpowers/specs/YYYY-MM-DD-cyclone--design.md`, header `Status: Draft, awaiting user sign-off`, sections `Scope / Decisions / …`. Specs contain zero code blocks. +- **Plan path:** `docs/superpowers/plans/YYYY-MM-DD-cyclone-.md`, header per `superpowers:writing-plans` with `Goal / Architecture / Tech Stack / Spec` metadata + numbered `- [ ] Step N:` tasks. +- **Commit prefixes:** `feat(sp): …`, `docs(spec): …`, `docs(plan): …`, `merge: SP into main`. +- **PR title:** `SP ` (matches the merge-commit subject). +- **Merge shape:** single atomic merge commit. **No squash** (collapses the audit trail) and **no rebase** (rewrites the SHAs the review was performed against). The SP-N merge commit *is* the record of the increment landing. + +The matching skill to load alongside `cyclone-spec` depends on the subsystem the SP-N touches (see the "Related skills" section at the bottom of each skill file). + +## Live-tail wire format + +The Claims, Remittances, and Activity pages stay current without manual refresh. The backend publishes an internal event on every store write, the page opens a streaming HTTP connection to the matching `/api//stream` endpoint, and new rows append to the table the moment they hit the database. + +Endpoints (all accept the same query params as their non-streaming counterparts; `Content-Type: application/x-ndjson`): + +| Method | Path | Subscribes to | Default sort | +|---|---|---|---| +| GET | `/api/claims/stream` | `claim_written` | `-submission_date` | +| GET | `/api/remittances/stream` | `remittance_written` | `-received_date` | +| GET | `/api/activity/stream` | `activity_recorded` | `-timestamp` (limit 50) | + +Wire format: one JSON object per line, `{"type": ..., "data": ...}`. The first batch is the **snapshot** of currently-known rows, then `snapshot_end` with the count, then the **live** events. Known types: `item`, `snapshot_end`, `heartbeat` (keeps the connection alive on idle — clients flip to `stalled` after 30s of total silence), `item_dropped` (rare), `error`. + +Status pill states (rendered by `` in `src/components/TailStatusPill.tsx`): `live` (success), `connecting` (warning), `reconnecting` (warning), `stalled` (destructive, ↻ Reconnect button), `error` (destructive, ↻ Reconnect button), `closed` (destructive). Backoff on error: `1s → 2s → 4s → 8s → 16s → 30s` capped. `STALL_TIMEOUT_MS = 30_000` in `src/hooks/useTailStream.ts:53`. Heartbeat interval is `CYCLONE_TAIL_HEARTBEAT_S` env var, default 15s. + +Frontend triplet for any live page: `use(params)` (initial fetch) + `useTailStream(resource)` (opens the NDJSON stream, drives backoff/stall) + `useMergedTail(resource, baseItems, filterFn?)` (merges snapshot + tail, dedup'd by id). The subscription lives on the page, not inside the data hook — see `cyclone-frontend-page` for why. + +## Backend at a glance + +`backend/src/cyclone/` is a single namespace. The two largest files are `api.py` (~3,548 LOC, the only large file) and `store.py` (~2,423 LOC, the `CycloneStore` facade — SP21 is in flight to split it into a `cyclone/store/` subpackage; the public API stays unchanged). Subpackages: `api_routers/` (acks, admin, health, ta1_acks), `clearhouse/` (Clearhouse + SftpClient), `edi/` (filenames), `parsers/` (X12 transaction parsers + models + validators + serializers), `workflow/` (placeholder for future sub-project 6). + +The store is the only read/write surface for the database; every mutating endpoint goes through it. All persistence flows through SQLAlchemy sessions via `db.SessionLocal()()`. SQLAlchemy ORM models live in `db.py`; 12 SQL migrations under `migrations/` (0001_initial through 0012_backups) are walked in order by `db_migrate.py`. + +The parser pipeline is a 5-stage `tokenize → segmentize → model → validate → write_to_store` flow used for every inbound X12 type. Per-transaction parsers: `parse_837.py`, `parse_835.py`, `parse_999.py`, `parse_ta1.py`, `parse_270.py`, `parse_271.py`, `parse_277ca.py`. Each has a matching Pydantic model module (`models.py`, `models_835.py`, …) and a writer (`writer.py` / `writer_835.py`). The 837P serializer (`serialize_837.py`) is the byte-faithful outbound counterpart used by both single-claim download (`/api/claims/{id}/serialize-837`) and the bulk rejected-resubmit bundle (`/api/inbox/rejected/resubmit?download=true`). + +The pubsub is `cyclone.pubsub.EventBus` — an in-process async fan-out broker. Publishers call `publish(kind, payload)`; subscribers receive via an async iterator. If a subscriber's per-kind queue is full, the oldest event is dropped so a slow consumer can't stall the producer. Bus is single-event-loop only (matches FastAPI/uvicorn). + +Config: `config/payers.yaml` is the on-disk source for providers / payers / clearhouse, schema-validated at boot against a Pydantic model. Reload with `POST /api/admin/reload-config`. Original in-code `PAYER_FACTORIES` dict in `cli.py` is kept as a fallback for ad-hoc testing. + +Secrets live in the macOS Keychain (via `keyring` + `cyclone.secrets`): SQLCipher key (service `cyclone`, account `cyclone.db.key`), SFTP password, backup passphrase. No secrets on disk in plaintext. + +## Frontend at a glance + +`src/` is React 18 + TypeScript + Vite. Routes register in `src/App.tsx` (11 pages, all under a `` route wrapper). Pages are pure renderers — every page pairs with a `use` data hook in `src/hooks/` and renders a `` + a table/list/KPI grid. Drawers (`ClaimDrawer/`, `RemitDrawer/`, plus the new `ProviderDrawer/` and `AckDrawer/`) are mounted by the page and their open/close state is mirrored to the URL via `useDrawerUrlState` so deep-links round-trip. Drill-stack navigation is provided by `` in `src/components/drill/`. + +State split: **server state** in TanStack Query (`@tanstack/react-query`); **ephemeral client state** in Zustand (`useTailStore` for live-tail append, plus the drill stack). The live-tail store is FIFO-capped at `TAIL_CAP = 10_000` per slice (`src/store/tail-store.ts:28`); `claims` and `remittances` are key-by-id with first-write-wins dedup, `activity` is an append-only array. + +UI primitives in `src/components/ui/` are Radix-backed (`button`, `dialog`, `table`, `select`, `pagination`, `empty-state`, `error-state`, `filter-chips`, `skeleton`, `input`, `label`, `card`, `badge`, `skip-link`, `claim-state-badge`). Don't import a new UI library without discussion. + +Path alias `@/` → `src/`. Configured in `vite.config.ts`, `vitest.config.ts`, and `tsconfig.app.json`. + +## CLI + +```bash +# Parser +python -m cyclone.cli parse-837 path/to/837p.txt --output-dir ./claims --payer co_medicaid [--strict] [--include-raw-segments] +python -m cyclone.cli parse-835 path/to/835.txt --output-dir ./remits +python -m cyclone.cli parse-999 inbound_999.txt +python -m cyclone.cli parse-ta1 inbound_ta1.txt +python -m cyclone.cli parse-277ca inbound_277ca.txt + +# Validators +python -m cyclone.cli validate-npi 1234567893 +python -m cyclone.cli validate-tin 721587149 + +# Other +python -m cyclone serve # uvicorn +python -m cyclone backup list +python -m cyclone backup create --reason manual +``` + +Exit codes are documented per subcommand in `cyclone-cli` — `0` for success, `2` for file-level failure, `1` for unexpected exceptions. + +## Things that are easy to get wrong + +- **`VITE_API_BASE_URL` matters.** With it empty, every `api` method throws `notConfiguredError()` and the UI falls back to the in-memory zustand store — parses are disabled and the live-tail streams never open. +- **Prodfiles vs fixtures.** Tests must reference `backend/tests/fixtures/.txt`, not `docs/prodfiles//.txt`. The prodfiles dir is the source-of-truth archive; the fixtures dir is the stable test surface. +- **SP-N merge shape.** No squash, no rebase. The merge commit *is* the audit trail. Squash collapses the per-commit history and breaks the SP-N audit trail. +- **Don't put domain logic in JSX.** Conditional renderings, table sorting, and KPI math all belong in the `use` hook or a pure helper under `src/lib/`. +- **Don't open a drawer via local `useState`.** Use `useDrawerUrlState()` so the URL is the single source of truth — deep-links and reload-restore depend on it. +- **Don't call `useTailStream` from inside a `use` hook.** The subscription lives on the page so the lifecycle ties to whoever mounts the hook, not to whoever happens to call it. +- **The store facade.** The public API of `cyclone.store` is preserved through SP21's split — call through the facade, not directly into the underlying modules. +- **Encryption is optional, not required.** When the Keychain entry is missing **or** `sqlcipher3` is not installed, the DB falls back to plain SQLite. Don't fail boot on missing encryption. +- **Local-only by design.** The backend binds to `127.0.0.1`, requires login (bcrypt + HttpOnly session cookie; see SP24 spec), and the threat model is still a stolen/imaged drive — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged. Don't add internet exposure. Don't disable auth without an explicit `CYCLONE_AUTH_DISABLED=1` env var (the escape hatch logs a WARNING at boot). + + \ No newline at end of file diff --git a/backend/src/cyclone/__main__.py b/backend/src/cyclone/__main__.py index 4a3f888..35b5c37 100644 --- a/backend/src/cyclone/__main__.py +++ b/backend/src/cyclone/__main__.py @@ -24,6 +24,20 @@ def main() -> None: from cyclone.auth import bootstrap bootstrap.run() + # SP24: if the AUTH_DISABLED escape hatch is on, scream at boot so a + # misconfigured production deploy fails loudly. The flag is flipped by + # ``CYCLONE_AUTH_DISABLED=1`` (see ``cyclone.auth.bootstrap``) and by the + # pytest conftest autouse fixture (see ``.superpowers/skills/cyclone-tests``). + from cyclone.auth import deps as _auth_deps + + if _auth_deps.AUTH_DISABLED: + import logging + + logging.getLogger("cyclone").warning( + "AUTH_DISABLED is set (CYCLONE_AUTH_DISABLED=1) — all requests " + "treated as admin, dev only. Do NOT enable this in production." + ) + if len(sys.argv) >= 2 and sys.argv[1] == "serve": port = os.environ.get("CYCLONE_PORT", "8000") reload = os.environ.get("CYCLONE_RELOAD", "0") == "1" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2337535..0516c4a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -13,7 +13,7 @@ ## 1. System overview -Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office. The first deployment target is Colorado Medicaid (one operator, one trading partner). The system is local-only on purpose: the backend binds to `127.0.0.1` and has no authentication because the threat model is a stolen or imaged drive, not a remote attacker. +Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office. The first deployment target is Colorado Medicaid (one operator, one trading partner). The system is local-only on purpose: the backend binds to `127.0.0.1` and requires login (bcrypt + HttpOnly session cookie). The threat model is still a stolen or imaged drive, not a remote attacker — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged. The SP23 product fork changes the threat model to "remote operator on LAN." Cyclone processes three X12 transaction pairs end-to-end: @@ -199,8 +199,9 @@ backend/src/cyclone/ | Module | Responsibility | |---|---| -| `__main__` | Dispatch `serve` vs CLI; honor `CYCLONE_PORT` / `CYCLONE_RELOAD` | +| `__main__` | Dispatch `serve` vs CLI; honor `CYCLONE_PORT` / `CYCLONE_RELOAD`; emit `AUTH_DISABLED` WARNING if `cyclone.auth.deps.AUTH_DISABLED` is True at boot | | `api` | FastAPI app, lifespan, route includes, 999/270/271 endpoints, exception → HTTP | +| `auth.*` | HTTP-layer login (bcrypt + HttpOnly session cookie). Submodules: `deps` (the `matrix_gate` FastAPI dependency, `AUTH_DISABLED` flag), `routes` (`/api/auth/login`, `/api/auth/logout`, `/api/auth/me`), `users`, `sessions`, `permissions` (role matrix), `bootstrap` (env-var first-admin), `cli` (`cyclone admin create-user` / `rotate-password`) | | `api_helpers` | Error → status code mapping, common response shapes | | `audit_log` | Append + verify a SHA-256 hash-chained log (SP11) | | `backup` | Pure crypto + file I/O for SP17 backup format | @@ -338,9 +339,9 @@ The client uses `fetch` + `ReadableStream` to consume the body and dispatches ea ## 6. Data model -### 6.1 Migrations 0001–0012 +### 6.1 Migrations 0001–0014 -The schema evolves in 12 ordered SQL files under `backend/src/cyclone/migrations/`. Each file starts with `-- version: N` and is applied in order by `db_migrate.py`. There is no migration framework — `db_migrate.py` is a 30-line file that checks the `schema_version` row and applies everything newer. +The schema evolves in ordered SQL files under `backend/src/cyclone/migrations/`. Each file starts with `-- version: N` and is applied in order by `db_migrate.py`. There is no migration framework — `db_migrate.py` is a 30-line file that checks the `schema_version` row and applies everything newer. The first 12 ship on `main`; 13 + 14 are the auth migrations that landed in `main` on 2026-06-23. | # | File | What it adds | |---|---|---| @@ -356,8 +357,10 @@ The schema evolves in 12 ordered SQL files under `backend/src/cyclone/migrations | 10 | `0010_payer_rejected_acknowledged.sql` | `claim.payer_rejected_acknowledged_*` columns (SP14 hand-off) | | 11 | `0011_processed_inbound_files.sql` | `processed_inbound_files` idempotency table (SP16) | | 12 | `0012_backups.sql` | `db_backups` table (SP17) | +| 13 | `0013_auth_users_and_sessions.sql` | `users` + `sessions` tables, `users.password_hash` (bcrypt) — landed 2026-06-23 | +| 14 | `0014_audit_log_user_id.sql` | `audit_log.user_id` FK to `users.id` (forward reference to 0013) — landed 2026-06-23 | -A 13th (`0013_claims_unique_constraint.sql`) and 14th (`0014_409_ux.sql`) are in-flight on the `claims-unique-fix` worktree but not yet on `main`. +The SP22 parse-then-decide migrations (drop claims UNIQUE constraint + relax PK to `(batch_id, id)`) are renumbered to 0015 + 0016 on the `claims-unique-fix` worktree so the auth migrations stay at the front of the chain; they'll keep those numbers when SP22 lands. ### 6.2 ERD (high level) @@ -614,7 +617,7 @@ Every API error is a JSON object with `{"error": "", "message": "", | `ValidationError` (R-codes) | 422 | `validation_failed` (issues in `detail`) | | `FileNotFoundError` | 404 | `not_found` | | `ValueError` | 400 | `bad_request` | -| `PermissionError` | 403 | `forbidden` (reserved — no auth today) | +| `PermissionError` | 403 | `forbidden` (raised by `matrix_gate` when the role is below the endpoint's required role) | | (anything else) | 500 | `internal` (with a request id for log correlation) | The 409-vs-422-vs-400 split is intentional: 409 for state-machine violations (the operator's input is well-formed but conflicts with current state), 422 for parse / validation, 400 for bad input shape. @@ -700,7 +703,7 @@ For a single operator on a single machine, the dev deployment IS the production The following are explicitly NOT built in v1, by design: -- **Multi-user / multi-host** — no auth, no LAN-bind, no Docker, no reverse proxy. The operator is one person, the host is one machine. (SP23 fork if this changes.) +- **Multi-user / multi-host** — login is required (single-user model today; SP23 adds RBAC). LAN-bind, Docker, reverse proxy still out of scope. (SP23 covers the LAN-bind / Docker / RBAC product fork.) - **Other trading partners** — only Colorado Medicaid via Gainwell is configured. The `PayerConfig` mechanism (SP9) supports adding more; the spec/plan/CLI work for that is on the backlog. - **837I / 837D / 834 / 820 / 275 / 278 / 276-277** — only 837P and 835 are first-class. Other transaction types are listed in [REQUIREMENTS.md §6.2](REQUIREMENTS.md) as backlog items. - **NPPES real-time lookup** — only local NPI checksum validation (SP20). The operator who wants real registry lookup has to wire it in later. diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 5cf4f83..2d38577 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -33,10 +33,10 @@ Every requirement (§3, §4) has an ID; every ID is traceable to a sub-project ( 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`, no auth, no internet exposure, single operator, single machine, one trading partner (Colorado Medicaid, currently). +- **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. No second party to authenticate; no second host to harden against. +- **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. @@ -151,7 +151,7 @@ Each requirement is `FR-NN`, traceable to one or more sub-projects. Verification | ID | NFR | Source / SP | |---|---|---| -| NFR-1 | **Local-only bind.** Backend binds `127.0.0.1:8000` (overridable via `CYCLONE_PORT`); CORS allowlist is exact (`http://localhost:5173`); no auth, no API key. | SP1 | +| 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 | @@ -406,6 +406,7 @@ Each row's "Spec" / "Plan" point to the design spec and implementation plan on d | SP20 | NPI checksum + Tax ID format validation | [spec](superpowers/specs/2026-06-21-cyclone-npi-validation-design.md) | (no plan file on disk; feat(sp20) commits on `main`) | Met | | SP21 | CycloneStore split (in-flight) + Universal drilldown (shipped) | [split spec](superpowers/specs/2026-06-21-cyclone-store-split-design.md), [drilldown spec](superpowers/specs/2026-06-21-cyclone-universal-drilldown-design.md) | [store-split plan](superpowers/plans/2026-06-21-cyclone-store-split.md), [drilldown plan](superpowers/plans/2026-06-21-cyclone-universal-drilldown.md) | Drilldown **Met**; split **In-flight** (`refactor/store-split` branch, not yet on `main`) | | SP22 | Parse-decide workflow + Pipeline agent | [parse-decide spec](superpowers/specs/2026-06-21-cyclone-parse-decide-workflow-design.md), [pipeline spec](superpowers/specs/2026-06-21-cyclone-pipeline-agent-design.md) | [parse-decide plan](superpowers/plans/2026-06-21-cyclone-parse-decide-workflow.md), [pipeline plan](superpowers/plans/2026-06-21-cyclone-pipeline-agent.md) | 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](superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md) | [plan](superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md) | Met (no code paths change; only docs + `__main__.py` WARNING + 3 skills addenda) | ### 6.2 Backlog (specs awaiting plans, or vice versa) @@ -625,7 +626,7 @@ Pulled forward from [`docs/reviews/2026-06-20-cyclone-completeness-review.md`](r | 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 | **Open** — addressed only if/when SP23 ships. | +| 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. | diff --git a/docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md b/docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md index d57ca1b..2a55425 100644 --- a/docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md +++ b/docs/superpowers/specs/2026-06-23-cyclone-auth-posture-alignment-design.md @@ -1,8 +1,8 @@ # Cyclone Auth Posture Alignment — Design **Date:** 2026-06-23 -**Status:** Draft, awaiting user sign-off -**Branch:** `main` (auth code already landed; this SP aligns docs + decisions) +**Status:** Approved (in implementation on `sp24-doc-posture-alignment`, 2026-06-23) +**Branch:** `sp24-doc-posture-alignment` (off `css-reduction` — `docs/REQUIREMENTS.md` and `docs/ARCHITECTURE.md` were added on `css-reduction` and do not yet exist on `main`) **Depends on:** The auth work that landed in main via the merge of `origin/main` on 2026-06-23 (commits `a25504b`..`39ae988`, 14 commits, the `feat(auth):`/`fix(auth):`/etc. series on `auth-rebased`). **Replaces:** The "no auth, no LAN-bind, single operator" claim in `CLAUDE.md`, `docs/REQUIREMENTS.md`, and `docs/ARCHITECTURE.md` — the codebase no longer matches the doc and the divergence needs to be resolved one way or the other.