Locks in:
- cyclone.reissue subpackage (parse_inputs / emit_outputs /
zip_outputs / ig_correctness_check — pure functions, no Click).
- @main.command('reissue-claims') thin wrapper in cli.py.
- PATIENT_LOOP_DEFAULT_INCLUDED public constant in serialize_837.py
(replaces inspect.signature on the private _build_subscriber_block).
- IG-correctness regression test in test_serialize_837.py pinning the
constant to False.
- deprecation shim at scripts/reissue_claims.py.
- RUNBOOK entry under 'Operator workflows → Reissue claims'.
Workflow coverage: the dzinesco July-8 batches (4 files, 358 claims)
that triggered the 999 '2000C HL must be absent when 2000B SBR02=18'
rejection on 2026-07-08.
13 KiB
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 functionsparse_inputs,emit_outputs,zip_outputs, andig_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 withoutclick.testing.CliRunner. - A new CLI subcommand
cyclone reissue-claimsregistered against the existing@mainClick group inbackend/src/cyclone/cli.py, following the conventions incyclone-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 ofinspect.signature-ing a private helper, so a future rename of_build_subscriber_blockcannot silently break the guard. - A regression test
test_serialize_837_patient_loop_default_is_falseinbackend/tests/test_serialize_837.pythat asserts the constant isFalseand that calling_build_subscriber_blockwith noinclude_patient_loopkwarg emitsHL*2 child_count=0(noHL*3). - Smoke tests under
backend/tests/test_cli_reissue_claims.pycovering 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.pycovering 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.pythat re-exports the new CLI subcommand as a thin wrapper with a WARNING printed at import time telling the operatorcyclone reissue-claimsis the canonical entry. The shim is removed in a follow-up increment. - A
docs/RUNBOOK.mdentry 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-ingestalready handles the streaming case for inbound files; the outbound reissue path stays explicit. - The SFTP-upload step.
reissue-claimswrites 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 indocs/RUNBOOK.md. - An automatic regeneration of historical files. The four dzinesco
July-8 batches are the immediate operator concern; running
reissue-claimsagainst older batches is a deliberate operator action with the explicit--input-dirpath. - 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)
-
Subpackage layout, not a single
cli.pyblock. The other cross-cutting workflows (cyclone.rebill,cyclone.submission,cyclone.bulk_ingest) follow thecyclone/<verb>/layout. Putting the reissue logic insidecli.pydirectly — likerebill-from-835does — would makecli.pygrow past 2,200 LOC (already at 2,054) and force the unit tests to live in the CLI-flavoredtest_cli_*.pyfile. The subpackage layout keeps the pure functions unit-testable without Click, matches the rest of the codebase, and letscli.pystay a thin registration surface. -
Public
PATIENT_LOOP_DEFAULT_INCLUDEDconstant, notinspect.signatureintrospection. The original script introspected the private_build_subscriber_blockhelper viainspect.signature, which is fragile against rename / refactor. Promoting the default to a named public constant inserialize_837.pyand exporting it in__all__gives the guard a stable handle that survives any future refactor of the helper. -
cyclone reissue-claimslong-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 justcyclone reissue-claims --input-dir <batch>. -
IG-correctness guard fires on every CLI invocation. Not gated behind a
--strictflag — the constant is part of the canonical contract the spec requires. The guard's only job is to fail loud if the constant flips toTruein a future refactor; running it on every call is the cheapest possible regression test. -
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). -
Deprecation shim, not deletion. The original
scripts/reissue_claims.pystays as a 1-line shim that prints a WARNING at import time and re-exports the new module'smain()function. The shim is removed in a follow-up increment once the operator's muscle memory has switched over. -
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. -
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 (viadatetime.now(tz=ZoneInfo("America/Denver"))+ per-claimtimedelta(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— coversparse_inputs(AppleDouble metadata filter, per-file failure tolerance, claim validation error path),emit_outputs(HCPF-spec filename format, sorted byclaim_id, per-claim unique timestamps, directory creation),zip_outputs(round-trip integrity viatestzip()), andig_correctness_check(fires on mismatch, silent on match). Each test usestmp_path+ a fixture*.x12file containing a minimal valid 837P — no prodfiles dropped intofixtures/.backend/tests/test_serialize_837.py— addstest_serialize_837_patient_loop_default_is_falseasserting thatserialize_837.PATIENT_LOOP_DEFAULT_INCLUDED is Falseand that calling_build_subscriber_blockwith no kwargs emitsHL*2withchild_count = 0(noHL*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 forcyclone reissue-claims. Covers:--helpexits 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_INCLUDEDtoTrue, asserts the CLI exits 1 with theREFUSING to runlog line in the captured stderr.
- Fixture additions: none. The CLI smoke test synthesizes a
minimal 837P file inline via
parse_837round-tripping a stubClaimOutput. No prodfiles are dropped intofixtures/. - 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
ee1a397onsp41-inwindow-rebill-pipeline(already merged tomain), which flipped the_build_subscriber_block.include_patient_loopdefault fromTruetoFalseand 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
cyclone reissue-claims --helpexits 0 and lists the long-form flags listed in §2 Decision 3.cyclone reissue-claims --input-dir /home/tyler/dev/cyclone/july8billing --date 2026-07-08 --output-root /tmp/sp24-acceptance-outproduces exactly 358.x12files under/tmp/sp24-acceptance-out/2026-07-08/initial/, plus a_serialize_summary.jsonsidecar with one row per file.cyclone reissue-claims --input-dir ... --zip-output /tmp/sp24-acceptance.zipproduces a zip whosetestzip()returnsNone(integrity OK) and which contains exactly 358 entries.- The IG-correctness guard exits 1 with the
REFUSING to runlog line when the constant is monkeypatched toTrue. Verified by thetest_cli_reissue_claims.pysmoke test. pytest backend/tests/test_reissue_core.py -vexits 0 with all cases passing.pytest backend/tests/test_cli_reissue_claims.py -vexits 0 with all cases passing.pytest backend/tests/test_serialize_837.py -vexits 0 with the newtest_serialize_837_patient_loop_default_is_falsecase passing alongside the existing 56 cases.pytest(full backend suite) exits 0; no regressions.- The CLI runs without requiring a Keychain entry or DB connection —
reissue-claimsis a pure parser + serializer + filesystem workflow. This matches the original script's posture. - After the merge to
main,scripts/reissue_claims.pyis a 1-line shim that prints a WARNING at import time telling the operatorcyclone reissue-claimsis the canonical entry. The shim does NOT duplicate the argparse surface.