# Sub-project 24 — Reissue Claims CLI + IG-Correctness Regression Test: Design Spec **Date:** 2026-07-08 **Status:** Draft, awaiting user sign-off **Branch:** `sp24-reissue-claims` **Aesthetic direction:** No new UI (operator-only CLI; the workflow is the on-call page already documented in `docs/RUNBOOK.md`). ## 1. Scope On 2026-07-08 the operator rebuilt the four dzinesco July-8 837P batches into 358 single-claim, IG-correct X12 files (zipped at `/home/tyler/dev/cyclone/xxxclaims.zip`) using a one-shot script at `scripts/reissue_claims.py`. The script solved an immediate crisis: an Edifabric 999 had rejected the original four multi-claim files because the serializer was unconditionally emitting the Loop 2000C patient hierarchy (`HL*3 → PAT*01 → NM1*QC`) when `SBR02 == "18"` (Self-pay), which is forbidden by X12 005010X222A1. The serializer default was fixed in `serialize_837.py` (see commit `ee1a397` on `sp41-inwindow-rebill-pipeline`), but the workflow that proved the fix — parse + per-claim re-serialize + HCPF-spec filename + zip — still lives in a script the rest of the cyclone pipeline doesn't know about. SP24 brings that workflow into the canonical CLI surface as `cyclone reissue-claims`, behind a regression test that locks in the IG-correct serializer default forever so a future code change cannot silently reintroduce the same 999 rejection. **In scope:** - A new subpackage `backend/src/cyclone/reissue/` containing the pure functions `parse_inputs`, `emit_outputs`, `zip_outputs`, and `ig_correctness_check`. No Click imports inside the core module — it accepts plain Python types so the CLI is a thin wrapper and the functions are unit-testable without `click.testing.CliRunner`. - A new CLI subcommand `cyclone reissue-claims` registered against the existing `@main` Click group in `backend/src/cyclone/cli.py`, following the conventions in `cyclone-cli` (long-form flags, exit codes 0/1/2, `click.echo(..., err=True)` for errors). - A new module-level public constant in `serialize_837.py`: `PATIENT_LOOP_DEFAULT_INCLUDED = False`, exported in the module's `__all__`. The default is read by the IG-correctness guard instead of `inspect.signature`-ing a private helper, so a future rename of `_build_subscriber_block` cannot silently break the guard. - A regression test `test_serialize_837_patient_loop_default_is_false` in `backend/tests/test_serialize_837.py` that asserts the constant is `False` and that calling `_build_subscriber_block` with no `include_patient_loop` kwarg emits `HL*2 child_count=0` (no `HL*3`). - Smoke tests under `backend/tests/test_cli_reissue_claims.py` covering the help screen, an empty-input path, a happy-path re-issue, and the IG-correctness guard firing when the constant is monkeypatched. - Pure-unit tests under `backend/tests/test_reissue_core.py` covering parse_inputs (AppleDouble filter, per-file failure tolerance), emit_outputs (HCPF-spec filenames, unique-per-millisecond, sorted by claim_id), zip_outputs (round-trip integrity check), and ig_correctness_check (fires on mismatch, silent on match). - A deprecation shim at `scripts/reissue_claims.py` that re-exports the new CLI subcommand as a thin wrapper with a WARNING printed at import time telling the operator `cyclone reissue-claims` is the canonical entry. The shim is removed in a follow-up increment. - A `docs/RUNBOOK.md` entry under "Operator workflows" pointing at the new CLI subcommand with a worked example against the dzinesco July-8 billing layout. **Out of scope:** - New UI exposure for the reissue workflow. The CLI is the operator's on-call surface today; a dashboard widget is a future increment. - A bulk-ingest pipeline for the reissue path. The operator's flow is point-in-time ("rebuild yesterday's 358 files from this batch"), not a streaming one. `cyclone bulk-ingest` already handles the streaming case for inbound files; the outbound reissue path stays explicit. - The SFTP-upload step. `reissue-claims` writes files to a local directory (and optionally zips them). The operator moves the resulting zip to Gainwell via their SFTP client per the "Manual SFTP mode" posture documented in `docs/RUNBOOK.md`. - An automatic regeneration of historical files. The four dzinesco July-8 batches are the immediate operator concern; running `reissue-claims` against older batches is a deliberate operator action with the explicit `--input-dir` path. - Edifabric validation. The original script did not gate through Edifabric (the July-8 files were validated by the operator's manual 999 audit). A follow-up SP wires the SP41 SP40 Edifabric gate into the reissue path; this increment keeps the surface minimal. ## 2. Decisions (locked during brainstorming) 1. **Subpackage layout, not a single `cli.py` block.** The other cross-cutting workflows (`cyclone.rebill`, `cyclone.submission`, `cyclone.bulk_ingest`) follow the `cyclone//` layout. Putting the reissue logic inside `cli.py` directly — like `rebill-from-835` does — would make `cli.py` grow past 2,200 LOC (already at 2,054) and force the unit tests to live in the CLI-flavored `test_cli_*.py` file. The subpackage layout keeps the pure functions unit-testable without Click, matches the rest of the codebase, and lets `cli.py` stay a thin registration surface. 2. **Public `PATIENT_LOOP_DEFAULT_INCLUDED` constant, not `inspect.signature` introspection.** The original script introspected the private `_build_subscriber_block` helper via `inspect.signature`, which is fragile against rename / refactor. Promoting the default to a named public constant in `serialize_837.py` and exporting it in `__all__` gives the guard a stable handle that survives any future refactor of the helper. 3. **`cyclone reissue-claims` long-form CLI flags.** `--input-dir`, `--output-root`, `--date`, `--pipeline`, `--payer`, `--sender-id`, `--receiver-id`, `--submitter-name`, `--submitter-contact-name`, `--submitter-contact-email`, `--receiver-name`, `--zip-output`, `--no-clean`, `--log-level`. Long-form, not positional, so the destructive surface is self-documenting. Defaults match the dzinesco canonical sender (`11525703`) / receiver (`COMEDASSISTPROG`) so the typical invocation is just `cyclone reissue-claims --input-dir `. 4. **IG-correctness guard fires on every CLI invocation.** Not gated behind a `--strict` flag — the constant is part of the canonical contract the spec requires. The guard's only job is to fail loud if the constant flips to `True` in a future refactor; running it on every call is the cheapest possible regression test. 5. **Exit codes 0 / 1 / 2 mirror the existing CLI conventions.** `0` = success (all files written, zip optional); `1` = IG-correctness guard tripped OR a configuration / unexpected error; `2` = parse / validation failure on a file (per-file failures reported in stdout, but other files continue; only exits 2 if **zero** claims survived). 6. **Deprecation shim, not deletion.** The original `scripts/reissue_claims.py` stays as a 1-line shim that prints a WARNING at import time and re-exports the new module's `main()` function. The shim is removed in a follow-up increment once the operator's muscle memory has switched over. 7. **The CLI subcommand does NOT call `cyclone.submission.submit_file`.** Reissue is an offline-rebuild workflow; it produces local X12 files for the operator to inspect / upload manually. Wiring the SFTP path would conflate "regenerate clean files" with "submit them to Gainwell", and the operator wants the inspection step in between. 8. **HCPF-spec filename unchanged.** The canonical `cyclone.edi.filenames.build_outbound_filename(sender_id, "837P")` produces the file names already in use (`tp11525703-837P-...-1of1.x12`); no change to the filename format. Per-claim 1 ms timestamp offsets (via `datetime.now(tz=ZoneInfo("America/Denver"))` + per-claim `timedelta(milliseconds=i)`) guarantee uniqueness within a batch. ## 3. Threat model This increment does not change the auth boundary or the network posture. The threat model is unchanged from SP24 (this spec): the auth boundary is HTTP (login required, bcrypt + HttpOnly session cookie); the file-system threat is local-only and handled by SQLCipher at rest and the macOS Keychain. The CLI subcommand runs against the operator's local environment only — there is no HTTP surface, no auth gate, no network call. The IG-correctness guard is a static check against the serializer's documented default; it does not change the security posture. The regression test in `test_serialize_837.py` is structural: it pins a constant to a specific value. If a future refactor flips the constant back to `True` (the broken pattern), the test fails, the CLI guard fires, and the operator sees a clear `REFUSING to run` log line before any X12 byte is emitted. ## 4. Test impact Following the `cyclone-tests` conventions: - **Unit tests (pure-unit):** - `backend/tests/test_reissue_core.py` — covers `parse_inputs` (AppleDouble metadata filter, per-file failure tolerance, claim validation error path), `emit_outputs` (HCPF-spec filename format, sorted by `claim_id`, per-claim unique timestamps, directory creation), `zip_outputs` (round-trip integrity via `testzip()`), and `ig_correctness_check` (fires on mismatch, silent on match). Each test uses `tmp_path` + a fixture `*.x12` file containing a minimal valid 837P — no prodfiles dropped into `fixtures/`. - `backend/tests/test_serialize_837.py` — adds `test_serialize_837_patient_loop_default_is_false` asserting that `serialize_837.PATIENT_LOOP_DEFAULT_INCLUDED is False` and that calling `_build_subscriber_block` with no kwargs emits `HL*2` with `child_count = 0` (no `HL*3`). This test is the long-term regression guard; flipping it should require an explicit PR-level discussion. - **Integration tests (CLI smoke):** - `backend/tests/test_cli_reissue_claims.py` — `CliRunner`-based smoke tests for `cyclone reissue-claims`. Covers: - `--help` exits 0 with the expected long-form flags listed. - happy path: a single-file input dir produces the expected number of output files under `tmp_path/out`. - empty input dir: exits 2 with a clear "no claims found" message. - IG-correctness guard: monkeypatches `serialize_837.PATIENT_LOOP_DEFAULT_INCLUDED` to `True`, asserts the CLI exits 1 with the `REFUSING to run` log line in the captured stderr. - **Fixture additions:** none. The CLI smoke test synthesizes a minimal 837P file inline via `parse_837` round-tripping a stub `ClaimOutput`. No prodfiles are dropped into `fixtures/`. - **Frontend tests:** none (no UI in this increment). - **Live-test smoke:** a canonical end-to-end run against `/home/tyler/dev/cyclone/july8billing/` to confirm the CLI produces the same 358 files + zip as the original script. The smoke command is documented at the top of `/tmp/refactor-cyclone.md`. ## 5. Spec & plan anchors - **Spec anchor (this file):** `docs/superpowers/specs/2026-07-08-cyclone-reissue-claims-design.md` - **Plan anchor:** `docs/superpowers/plans/2026-07-08-cyclone-reissue-claims.md` - **Related serializer fix:** commit `ee1a397` on `sp41-inwindow-rebill-pipeline` (already merged to `main`), which flipped the `_build_subscriber_block.include_patient_loop` default from `True` to `False` and updated the docstring. - **Original operator workflow:** `scripts/reissue_claims.py` (the one-shot script this increment supersedes). - **Edifabric 999 that surfaced the bug:** `/tmp/ig_check.log` (the operator's 999 audit reference for the IG rule). ## 6. Acceptance criteria 1. `cyclone reissue-claims --help` exits 0 and lists the long-form flags listed in §2 Decision 3. 2. `cyclone reissue-claims --input-dir /home/tyler/dev/cyclone/july8billing --date 2026-07-08 --output-root /tmp/sp24-acceptance-out` produces exactly 358 `.x12` files under `/tmp/sp24-acceptance-out/2026-07-08/initial/`, plus a `_serialize_summary.json` sidecar with one row per file. 3. `cyclone reissue-claims --input-dir ... --zip-output /tmp/sp24-acceptance.zip` produces a zip whose `testzip()` returns `None` (integrity OK) and which contains exactly 358 entries. 4. The IG-correctness guard exits 1 with the `REFUSING to run` log line when the constant is monkeypatched to `True`. Verified by the `test_cli_reissue_claims.py` smoke test. 5. `pytest backend/tests/test_reissue_core.py -v` exits 0 with all cases passing. 6. `pytest backend/tests/test_cli_reissue_claims.py -v` exits 0 with all cases passing. 7. `pytest backend/tests/test_serialize_837.py -v` exits 0 with the new `test_serialize_837_patient_loop_default_is_false` case passing alongside the existing 56 cases. 8. `pytest` (full backend suite) exits 0; no regressions. 9. The CLI runs without requiring a Keychain entry or DB connection — `reissue-claims` is a pure parser + serializer + filesystem workflow. This matches the original script's posture. 10. After the merge to `main`, `scripts/reissue_claims.py` is a 1-line shim that prints a WARNING at import time telling the operator `cyclone reissue-claims` is the canonical entry. The shim does NOT duplicate the argparse surface.