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.
23 KiB
Reissue Claims CLI + IG-Correctness Regression Test Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Move scripts/reissue_claims.py into a cyclone.reissue subpackage, expose it as cyclone reissue-claims, and lock in the IG-correct serializer default via a regression test.
Architecture: cyclone/reissue/core.py (pure functions, no Click) + a thin @main.command("reissue-claims") wrapper in cli.py. The IG-correctness default is promoted from inspect.signature introspection of _build_subscriber_block to a public PATIENT_LOOP_DEFAULT_INCLUDED constant in serialize_837.py, exposed via __all__ and read by the guard.
Tech Stack: Python 3.11, Click 8, pytest, cyclone.parsers.parse_837 + cyclone.parsers.serialize_837 + cyclone.edi.filenames.build_outbound_filename.
Spec: docs/superpowers/specs/2026-07-08-cyclone-reissue-claims-design.md
File structure
backend/src/cyclone/
reissue/ ← new subpackage
__init__.py re-exports the public surface
core.py parse_inputs / emit_outputs /
zip_outputs / ig_correctness_check
cli.py ← registers @main.command("reissue-claims")
(~50 LOC, calls into cyclone.reissue.core)
parsers/serialize_837.py ← adds PATIENT_LOOP_DEFAULT_INCLUDED constant
in __all__; _build_subscriber_block reads
from it instead of hardcoding `False`
backend/tests/
test_reissue_core.py ← new (pure-unit)
test_cli_reissue_claims.py ← new (CliRunner smoke)
test_serialize_837.py ← adds test_serialize_837_patient_loop_default_is_false
scripts/
reissue_claims.py ← 1-line deprecation shim; prints WARNING
at import time, delegates to cyclone.reissue.core
docs/
RUNBOOK.md ← adds "Operator workflows → Reissue claims"
section pointing at `cyclone reissue-claims`
/tmp/refactor-cyclone.md ← progress tracker (already created)
Task 0: branch + worktree
- Branch
sp24-reissue-claimsfrommain(no worktree — the user is already in the main checkout; the branch is local). - Verify
git log --oneline main -1showsea68ae8 feat(sp41): add CYCLONE_EDIFABRIC_DISABLED=1 bypass hook. - First commit on the branch:
docs(spec): design for reissue-claims CLI + IG-correctness regression test (Step 0)(commits the spec file).
Task 1: promote PATIENT_LOOP_DEFAULT_INCLUDED to a public constant
-
In
backend/src/cyclone/parsers/serialize_837.py, add the constant after the imports (around line 65):```python #: Default value for ``_build_subscriber_block(include_patient_loop=...)``. #: Per X12 005010X222A1, the 2000C Patient Hierarchical Level #: (HL*3 → PAT → NM1*QC) is REQUIRED only when Patient != Subscriber #: (i.e. SBR02 != "18"). When SBR02 == "18" (Self), the 2000C loop #: MUST be absent. The serializer's default of False matches the #: CO-Medicaid IHSS workflow and is the IG-correct shape. #: #: The IG-correctness regression test #: ``test_serialize_837_patient_loop_default_is_false`` pins this #: value; flipping it requires an explicit PR-level discussion. PATIENT_LOOP_DEFAULT_INCLUDED: bool = False ``` -
Replace the existing hardcoded
include_patient_loop: bool = Falsedefault on_build_subscriber_block(...)withinclude_patient_loop: bool = PATIENT_LOOP_DEFAULT_INCLUDED. -
Add
PATIENT_LOOP_DEFAULT_INCLUDEDto the module's__all__tuple (find the existing__all__near the top of the file; if missing, create one with the public functions plus the constant). -
Live-test:
cd backend && .venv/bin/pytest tests/test_serialize_837.py -v(56 cases should still pass; the default behavior is unchanged). -
Autoreview: no flake, no lint regressions. Run
cd backend && .venv/bin/python -c "from cyclone.parsers.serialize_837 import PATIENT_LOOP_DEFAULT_INCLUDED; print(PATIENT_LOOP_DEFAULT_INCLUDED)"and confirm it printsFalse. -
Commit:
feat(sp24): expose PATIENT_LOOP_DEFAULT_INCLUDED constant in serialize_837.
Task 2: create cyclone.reissue subpackage
-
Create
backend/src/cyclone/reissue/__init__.pywith re-exports:```python """cyclone.reissue — offline 837P re-emission workflow. See :mod:`cyclone.reissue.core` for the public functions. """ from cyclone.reissue.core import ( parse_inputs, emit_outputs, zip_outputs, ig_correctness_check, ) __all__ = [ "parse_inputs", "emit_outputs", "zip_outputs", "ig_correctness_check", ] ``` -
Create
backend/src/cyclone/reissue/core.pymirroring the script's logic but as pure functions. The signatures match the script:```python def ig_correctness_check(*, logger: logging.Logger | None = None) -> bool: """Return True if the serializer's IG-correct patient-loop default is in place.""" ... def parse_inputs(input_dir: Path, payer_config) -> tuple[list[ClaimOutput], list[tuple[Path, str]]]: """Parse every *.x12 / *.txt / *.edi under input_dir. Tolerates per-file failures.""" ... def emit_outputs( claims: list[ClaimOutput], *, payer_config, output_dir: Path, sender_id: str, receiver_id: str, submitter_name: str, submitter_contact_name: str, submitter_contact_email: str, receiver_name: str, ) -> list[Path]: """Emit one X12 file per claim under output_dir using HCPF-spec filenames.""" ... def zip_outputs(files: list[Path], zip_path: Path) -> None: """Zip the per-claim X12 files into a single flat archive with integrity check.""" ... ``` Implementation details: - `ig_correctness_check` reads `cyclone.parsers.serialize_837.PATIENT_LOOP_DEFAULT_INCLUDED`; returns True iff it is False. Logs a WARNING (via the optional `logger` argument, or `logging.getLogger(__name__)`) if False is not what the caller expected. - `parse_inputs` skips AppleDouble (`._*`) files, tolerates per-file parse errors (returns them as the second tuple element), and skips claims with hard validation errors. - `emit_outputs` uses `datetime.now(tz=ZoneInfo("America/Denver"))` for the base timestamp + `timedelta(milliseconds=i)` for per-claim uniqueness. Sorts claims by `claim_id`. Writes `_serialize_summary.json` next to the files (caller does this). - `zip_outputs` uses `zipfile.ZIP_DEFLATED` and runs `ZipFile.testzip()` after writing; raises `RuntimeError` if any entry is corrupt. -
Live-test:
cd backend && .venv/bin/python -c "from cyclone.reissue import parse_inputs, emit_outputs, zip_outputs, ig_correctness_check; print('ok')"should printok. -
Commit:
feat(sp24): add cyclone.reissue subpackage with pure core functions.
Task 3: write test_reissue_core.py (pure-unit)
-
Create
backend/tests/test_reissue_core.pywith: -test_ig_correctness_check_returns_true_when_default_is_false(default state — guard silent). -test_ig_correctness_check_returns_false_when_default_flipped(monkeypatchesPATIENT_LOOP_DEFAULT_INCLUDED = Trueviamonkeypatch.setattr; assert False). -test_parse_inputs_skips_apple_double_metadata(createstmp_path/in/foo.x12andtmp_path/in/._foo.x12; asserts only foo.x12 is parsed). -test_parse_inputs_tolerates_per_file_failures(creates one valid + one malformed file; asserts the malformed one is in the errors list and the valid one is in claims). -test_emit_outputs_writes_hcpf_spec_filenames(parses a minimal 837P, emits totmp_path/out, asserts the filenames matchtp*-837P-*-1of1.x12). -test_emit_outputs_sorts_claims_by_claim_id(two-claim fixture, asserts the filename suffixes are sorted by claim_id). -test_zip_outputs_round_trip_integrity(writes a tiny zip, assertsZipFile.testzip()returns None). -
Live-test:
cd backend && .venv/bin/pytest tests/test_reissue_core.py -v(all new cases should pass). -
Autoreview: confirm no test relies on network, no test reaches into
docs/prodfiles/, no test uses wall-clock sleep. -
Commit:
test(sp24): pure-unit tests for cyclone.reissue.core.
Task 4: register @main.command("reissue-claims") in cli.py
-
Append the CLI subcommand to
backend/src/cyclone/cli.pyafter the existingrebill-from-835block. The Click signature follows §2 Decision 3 of the spec:```python @main.command("reissue-claims") @click.option("--input-dir", type=click.Path(exists=True, file_okay=False, path_type=Path), required=True) @click.option("--output-root", type=click.Path(file_okay=False, path_type=Path), default=Path("dev/rebills"), show_default=True) @click.option("--date", default=None, help="YYYY-MM-DD subfolder under output-root (default: today MT).") @click.option("--pipeline", default="initial", show_default=True) @click.option("--payer", default="co_medicaid", show_default=True) @click.option("--sender-id", default="11525703", show_default=True) @click.option("--receiver-id", default="COMEDASSISTPROG", show_default=True) @click.option("--submitter-name", default="Dzinesco", show_default=True) @click.option("--submitter-contact-name", default="Tyler Martinez", show_default=True) @click.option("--submitter-contact-email", default="tyler@dzinesco.com", show_default=True) @click.option("--receiver-name", default="COLORADO MEDICAL ASSISTANCE PROGRAM", show_default=True) @click.option("--zip-output", type=click.Path(file_okay=False, path_type=Path), default=None) @click.option("--no-clean", is_flag=True, default=False, help="Don't wipe prior .x12 outputs in the pipeline subdir before writing.") @click.option("--log-level", default="INFO", show_default=True, type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"])) def reissue_claims( input_dir: Path, output_root: Path, date: str | None, pipeline: str, payer: str, sender_id: str, receiver_id: str, submitter_name: str, submitter_contact_name: str, submitter_contact_email: str, receiver_name: str, zip_output: Path | None, no_clean: bool, log_level: str, ) -> None: """Re-parse raw 837P files and emit one IG-correct single-claim X12 per claim (SP24). Workflow: 1. Parse every *.x12 / *.txt / *.edi under --input-dir. 2. Validate each parsed claim; skip hard-error claims. 3. Serialize each surviving claim via the IG-correct serializer. 4. Write one X12 file per claim under ``--output-root/--date/--pipeline/`` using HCPF-spec filenames. 5. Optionally zip the output to --zip-output. Exit codes: 0 success; 1 IG-correctness guard tripped or unexpected error; 2 parse / validation failure on a file (zero claims survived). """ setup_logging(level=log_level) import logging as _logging from cyclone.reissue import ( parse_inputs, emit_outputs, zip_outputs, ig_correctness_check, ) from cyclone.parsers.payer import PayerConfig log = _logging.getLogger("reissue-claims") if not ig_correctness_check(logger=log): click.echo("REFUSING to run: PATIENT_LOOP_DEFAULT_INCLUDED is True; " "the 2000C patient loop is forbidden when SBR02='18'. " "Restore the IG-correct default (False) in serialize_837.py.", err=True) sys.exit(1) payer_factories = {"co_medicaid": PayerConfig.co_medicaid} if payer not in payer_factories: raise click.BadParameter(f"Unknown payer {payer!r}. Choose from: {', '.join(payer_factories)}") payer_config = payer_factories[payer]() date_label = date or datetime.now().strftime("%Y-%m-%d") out_dir = output_root / date_label / pipeline if out_dir.exists() and not no_clean: for p in out_dir.glob("tp*.x12"): p.unlink() log.info("cleaned prior .x12 outputs under %s", out_dir) log.info("parse: input_dir=%s payer=%s", input_dir, payer) claims, parse_errors = parse_inputs(input_dir, payer_config) log.info("parse: %d claims read, %d file-level errors", len(claims), len(parse_errors)) for src, err in parse_errors[:10]: log.warning(" %s: %s", src, err) if parse_errors and not claims: click.echo(f"PARSE FAILED: zero claims survived; first error: {parse_errors[0][1]}", err=True) sys.exit(2) log.info("serialize: output_dir=%s", out_dir) written = emit_outputs( claims, payer_config=payer_config, output_dir=out_dir, sender_id=sender_id, receiver_id=receiver_id, submitter_name=submitter_name, submitter_contact_name=submitter_contact_name, submitter_contact_email=submitter_contact_email, receiver_name=receiver_name, ) import json as _json summary_path = out_dir / "_serialize_summary.json" summary = [ {"claim_id": c.claim_id, "output_file": str(p), "byte_size": p.stat().st_size} for c, p in zip(sorted(claims, key=lambda c: c.claim_id), written) ] summary_path.write_text(_json.dumps(summary, indent=2), encoding="utf-8") log.info("serialize: %d files, %d bytes, summary=%s", len(written), sum(p.stat().st_size for p in written), summary_path) if zip_output: zip_outputs(written, zip_output) log.info("zip: %s (%d bytes)", zip_output, zip_output.stat().st_size) click.echo(f"DONE files={len(written)} errors={len(parse_errors)} out={out_dir}") if parse_errors: sys.exit(2) ``` -
Live-test:
cd backend && .venv/bin/python -m cyclone.cli reissue-claims --helpshould print the help with the long-form flags listed in §2 Decision 3. -
Live-test end-to-end:
cd backend && .venv/bin/python -m cyclone.cli reissue-claims \ --input-dir /home/tyler/dev/cyclone/july8billing \ --date 2026-07-08 \ --output-root /tmp/sp24-smoke \ --zip-output /tmp/sp24-smoke.zipshould produce/tmp/sp24-smoke/2026-07-08/initial/with 358 files and/tmp/sp24-smoke.zipwith 358 entries. -
Autoreview: confirm the help text is readable, exit codes behave per the spec, and no click.UsageError leaks a CycloneParseError stack trace.
-
Commit:
feat(sp24): register cyclone reissue-claims CLI subcommand.
Task 5: write test_cli_reissue_claims.py (CliRunner smoke)
-
Create
backend/tests/test_cli_reissue_claims.pywith: -test_reissue_claims_help_renders—CliRunner().invoke(main, ["reissue-claims", "--help"])exits 0 and contains--input-dir,--output-root,--zip-output,--no-clean. -test_reissue_claims_happy_path— writes a minimal 837P fixture totmp_path/in, runs the CLI with--input-dir tmp_path/in --output-root tmp_path/out --date 2026-07-08, asserts exit 0 and 1+ .x12 file intmp_path/out/2026-07-08/initial/. -test_reissue_claims_empty_input_exits_2— empty input dir, asserts exit 2 with "no claims" / "PARSE FAILED" in stderr. -test_reissue_claims_ig_correctness_guard_fires—monkeypatch.setattr("cyclone.parsers.serialize_837.PATIENT_LOOP_DEFAULT_INCLUDED", True, raising=False), asserts exit 1 withREFUSING to runin captured output. Usemonkeypatch.undo()via awith-block oraddfinalizerso other tests are unaffected. -test_reissue_claims_zip_output_round_trip— happy path + zip, asserts the zip file exists andZipFile(path).testzip() is None. -
Live-test:
cd backend && .venv/bin/pytest tests/test_cli_reissue_claims.py -v(all new cases should pass). -
Commit:
test(sp24): CliRunner smoke tests for reissue-claims.
Task 6: write the IG-correctness regression test in test_serialize_837.py
-
Add a new test to
backend/tests/test_serialize_837.py:```python def test_serialize_837_patient_loop_default_is_false(): """SP24: the IG-correct default for include_patient_loop is False. Per X12 005010X222A1, the 2000C Patient Hierarchical Level (HL*3 → PAT → NM1*QC) is REQUIRED only when Patient != Subscriber. For SBR02 == "18" (Self-pay, the CO Medicaid IHSS workflow), the 2000C loop MUST be absent. Flipping this default back to True reintroduces the Edifabric 999 "2000C HL must be absent when 2000B SBR02 = '18'" rejection — see the 2026-07-08 audit. """ from cyclone.parsers.serialize_837 import ( PATIENT_LOOP_DEFAULT_INCLUDED, _build_subscriber_block, ) from cyclone.parsers.models import Subscriber assert PATIENT_LOOP_DEFAULT_INCLUDED is False, ( f"PATIENT_LOOP_DEFAULT_INCLUDED must be False (got {PATIENT_LOOP_DEFAULT_INCLUDED!r}); " "the IG-correct serializer shape for SBR02='18' claims is 2000C-absent." ) # Construct a minimal subscriber + payer fixture and call # _build_subscriber_block without the kwarg; verify no HL*3 # segment appears in the output. from datetime import date sub = Subscriber( member_id="TEST123", first_name="Test", last_name="Patient", dob=date(1990, 1, 1), gender="F", ) out = _build_subscriber_block(sub, "MC") assert not any(seg.startswith("HL*3") for seg in out), ( f"HL*3 emitted with the IG-correct default; segments={out}" ) ``` -
Live-test:
cd backend && .venv/bin/pytest tests/test_serialize_837.py::test_serialize_837_patient_loop_default_is_false -vpasses. -
Autoreview: try flipping the constant to
Truelocally, re-run the test, confirm it fails with the assertion message; restore the constant. -
Commit:
test(sp24): regression test pinning PATIENT_LOOP_DEFAULT_INCLUDED to False.
Task 7: deprecation shim for scripts/reissue_claims.py
-
Replace
scripts/reissue_claims.pywith a 1-line shim:```python """DEPRECATED shim — use `cyclone reissue-claims` instead. See `docs/superpowers/specs/2026-07-08-cyclone-reissue-claims-design.md` for the canonical entry point. """ import warnings warnings.warn( "`scripts/reissue_claims.py` is deprecated; use " "`cyclone reissue-claims` (the Click subcommand in cli.py) " "for the canonical offline reissue workflow. This shim will be " "removed in a follow-up increment.", DeprecationWarning, stacklevel=2, ) from cyclone.cli import main import sys if __name__ == "__main__": # Strip the script argv prefix so `python scripts/reissue_claims.py --help` # behaves like `cyclone reissue-claims --help`. The Click group # expects the subcommand as argv[1]. sys.exit(main()) ``` -
Live-test:
python scripts/reissue_claims.py --helpprints the canonical Click help for thecyclonegroup (and emits a DeprecationWarning). -
[ [ ] Commit:
refactor(sp24): deprecation shim for scripts/reissue_claims.py.
Task 8: docs/RUNBOOK.md operator-workflow entry
-
Add a section under "Operator workflows" pointing at the new CLI subcommand with a worked example:
```markdown ### Reissue claims (SP24) The canonical workflow to rebuild a batch of 837P files into single-claim, IG-correct X12 files: ```bash cd backend && .venv/bin/python -m cyclone.cli reissue-claims \ --input-dir <path-to-raw-837ps> \ --date 2026-07-08 \ --output-root dev/rebills \ --zip-output xxxclaims.zip ``` Exit codes: 0 = success; 1 = IG-correctness guard tripped or unexpected error; 2 = parse / validation failure on a file. ``` -
Commit:
docs(sp24): RUNBOOK entry for cyclone reissue-claims.
Task 9: autoreview (the merge gate)
cd backend && .venv/bin/pytest -q— full backend suite passes.cd backend && .venv/bin/pytest tests/test_serialize_837.py tests/test_reissue_core.py tests/test_cli_reissue_claims.py -v— all new + adjacent tests pass.cd backend && .venv/bin/python -m cyclone.cli reissue-claims --help— CLI help renders cleanly.- End-to-end live-test against
/home/tyler/dev/cyclone/july8billing/produces 358 files + a valid zip (per acceptance criterion #2 + #3). - No
git status --shortshows any unexpected untracked files. - No regressions in the existing
rebill-from-835orsubmit-batchCLIs (they don't import fromcyclone.reissue).
Task 10: merge to main
git checkout main && git merge --no-ff --no-squash --no-rebase sp24-reissue-claims -m "merge: SP24 reissue-claims CLI + IG-correctness regression test into main"git log --oneline main -1shows the merge commit with the expected subject.git push origin mainonly after the operator has reviewed and approved the merge (per cyclone-spec — the merge commit is the audit trail; the operator must approve).