Files
cyclone/docs/superpowers/specs/2026-06-21-cyclone-npi-validation-design.md
Tyler 1942a22629 feat(sp20): NPI Luhn checksum + Tax ID (EIN) format validation
Adds pure local validators for the 10-digit NPI Luhn checksum (CMS-
published algorithm with the '80840' NPPES prefix) and 9-digit EIN
format (rejects reserved prefixes 00/07/80-89). No NPPES round-trip,
no IRS e-file lookup — catches the 99% typo case at parse time.

Surface:
- cyclone.npi.is_valid_npi / is_valid_tax_id / normalize_tax_id
- CLI: 'cyclone validate-npi <npi>' and 'cyclone validate-tax-id <ein>'
- API: GET /api/admin/validate-provider?npi=&tax_id=
- Parser validator: new R021_npi_checksum rule (warning, not error,
  to keep test fixtures with placeholder NPIs ingestible)
- minimal_837p.txt fixture NPI updated from '1234567890' to the
  Luhn-valid '1993999998' so strict-mode CLI parses still pass

Tests:
- test_npi.py — 27 cases (Luhn math, valid/invalid NPIs, EIN cases,
  normalize_tax_id edge cases)
- test_api_validate_provider.py — 4 cases (both valid, both invalid,
  omitted NPI, omitted tax_id)
- test_cli_validate.py — 8 cases (valid/invalid for both subcommands,
  exit codes, malformed inputs)
- test_validator.py — 4 new R021 cases (valid Luhn silent, bad Luhn
  warning, skipped when format bad, skipped when NPI missing)

Total: 923 tests pass.
2026-06-21 10:46:10 -06:00

2.4 KiB

SP20 — NPI Checksum + Tax ID Format Validation

Date: 2026-06-21 Branch: sp20-npi-validation Status: Shipped Scope: Backend only. No frontend changes.


1. Why this exists

Cyclone's completeness review (docs/reviews/2026-06-20-cyclone-completeness-review.md §3.1.10) flags this gap:

No NPI validation. NPIs are captured (claim.party.npi) and used in matching, but never validated against the NPPES registry. Bad NPIs (typos, deactivated) silently propagate.

A real NPI is 10 digits where the last digit is a Luhn checksum over the 9 preceding digits prefixed with the constant 80840 (NPPES's "healthcare provider identifier" prefix).

Tax ID format: 9 digits, optionally formatted XX-XXXXXXX. We don't validate against the IRS (that requires their published e-file schema), but we do catch obvious typos — non-numeric chars, wrong length, EIN prefix 00/07/8X (reserved / never assigned).

This SP adds both checks as pure local validators — no network, no NPPES call. Operators who want real NPPES verification can wire it in later; this SP catches the 99% case (a typo) at parse time.

2. Operator surface

Surface Usage
Python from cyclone.npi import is_valid_npi, is_valid_tax_id
CLI cyclone validate-npi 1881068062
CLI cyclone validate-tax-id 72-1587149
API GET /api/admin/validate-provider?npi=1881068062&tax_id=72-1587149

The parser's claim-level validator (cyclone.parsers.validator) gains a new R-rule that flags bad NPIs as a validation.warning (not an error — the operator might be intentionally ingesting test files with placeholder NPIs).

3. Files

  • cyclone/npi.py — new module (~120 LOC). is_valid_npi(), is_valid_tax_id(), npi_checksum(), plus the NPPES constant.
  • cyclone.parsers.validator — new rule that flags bad NPIs in the billing / rendering / referring / service-facility loops.
  • cyclone.api — new admin endpoint.
  • cyclone.clivalidate-npi + validate-tax-id subcommands.
  • Tests: test_npi.py (12), test_api_validate_provider.py (4), test_cli_validate.py (4) — 20 new tests.

4. Threat model

NPIs and tax IDs are sensitive (PHI under HIPAA). The validators run locally; nothing leaves the process. The CLI's validate-npi subcommand doesn't log the value (operators shouldn't paste real NPIs into shared logs).