diff --git a/docs/superpowers/specs/2026-07-02-cyclone-rendering-npi-extraction-design.md b/docs/superpowers/specs/2026-07-02-cyclone-rendering-npi-extraction-design.md index 658c5ae..3596178 100644 --- a/docs/superpowers/specs/2026-07-02-cyclone-rendering-npi-extraction-design.md +++ b/docs/superpowers/specs/2026-07-02-cyclone-rendering-npi-extraction-design.md @@ -11,7 +11,7 @@ Status: Draft, awaiting user sign-off. - **Persist** all three new fields in their typed columns AND mirror to `raw_json` for audit and fallback reads. - **Backfill** existing rows by re-parsing on-disk files via a new CLI subcommand. - **Re-reconcile** open claim/remit pairs after backfill so the NPI arm of SP31's 2-of-3 content-match rule can actually fire on historical data. -- **Widen** `_content_keys_match` in `reconcile.py` to read the new NPI sources (typed-column primary, `raw_json` fallback) and to treat a comma-separated `Remittance.rendering_provider_npi` as a set. +- **Widen** `_content_keys_match` in `reconcile.py` to read the new NPI sources (typed-column primary, `raw_json` fallback). ### Out of scope @@ -34,9 +34,9 @@ Add `ClaimPayment.service_provider_npi: str | None` to `backend/src/cyclone/pars Add `Claim.rendering_provider_npi: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)` to `backend/src/cyclone/db.py`. Mirrors the existing `Claim.provider_npi` column. Schema migration `0016_add_rendering_and_service_provider_npis.sql` adds both this and the next column. -### D4 — Per-Remit aggregated column for the matcher +### D4 — Per-Remit column for the matcher -Add `Remittance.rendering_provider_npi: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)` (length-64 accommodates comma-separated lists up to ~6 NPI strings with separators). For multi-CLP remits, the writer concatenates each `ClaimPayment.service_provider_npi` with `","` as the separator (e.g. `"1234567890,0987654321"`). Single-CLP remits store the single NPI as-is. `None` means no CLP in the remit had a service-provider NPI. +Add `Remittance.rendering_provider_npi: Mapped[Optional[str]] = mapped_column(String(16), nullable=True)`. Today, each `Remittance` row corresponds to exactly one `ClaimPayment` (the write layer keys Remittance by `cp.payer_claim_control_number`; see `store/write.py:134`), so the column holds the single NPI for that segment (or `None`). The column is left unaggregated to keep the schema simple — if multi-CLP-per-Remit support is added in a future SP, the schema is large enough (`String(16)`) to upgrade to comma-joined storage without re-migration. The matcher's NPI arm reads this column directly. ### D5 — `raw_json` mirrors @@ -110,7 +110,7 @@ No `UPDATE` statements in the migration — backfill is a separate, deliberate s | `NM1*1P` missing or `1P` qualifier absent in a CLP segment | `ClaimPayment.service_provider_npi = None`. Matcher's NPI arm simply doesn't fire for that key. | | `NM1*82` missing or empty in an 837p claim | `Claim.rendering_provider_npi = None`. Same graceful degrade. | | `NM1*1P` with `NM109` empty | Treat as missing — store `None`, never empty string. | -| Multi-CLP remit with mixed NPIs | Writer aggregates `",,..."` on `Remittance.rendering_provider_npi`. Per-segment NPIs still on `ClaimPayment.service_provider_npi`. | +| Multi-CLP per Remit in a future SP | Schema-size forward note (D4). Today, each Remit = one CLP = single NPI, no aggregation needed. | | 837p rendering NPI == billing NPI (solo practice) | Both columns populated independently. Matcher compares against the service-provider NPI; identity is coincidence in this case. | | Backfill on a deleted-on-disk file | CLI logs a WARNING, skips the batch. Typed columns stay NULL for unmatched rows. Existing match state untouched. | | Re-reconcile creates new `score-auto` matches | Each emits an `auto_matched_835` ActivityEvent per SP31 D8 (D8). Audit trail is explicit. |