Files
stack/docs/superpowers/specs/2026-08-17-cy2027-notebook-gpci-pqm-design.md
kert c559cc10e2
All checks were successful
CI / lint (push) Successful in 26s
CI / notebooks-smoke (push) Successful in 1m31s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m1s
Infra CI / zotero (push) Successful in 14s
Infra CI / docs (push) Successful in 1m39s
Infra CI / api (push) Successful in 1m1s
Infra CI / llm (push) Successful in 47s
Infra CI / mc (push) Successful in 23s
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 13m39s
docs(spec): P39 build outcomes; plan checked off (refs #629-#633)
2026-08-17 12:40:22 -04:00

204 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CY2027 NPRM Notebook — GPCI & Primary-Care (PQM) Deep-Dive (P39)
Date: 2026-08-17. Milestone: P39. Requested as: "fix the notebook, and
also ensure that the notebook focuses both on the GPCI AND focuses on
more in-depth analysis of the PQM services (i.e. long-term and
near-term comparison of each individual service that is also a
designated primary care service for purposes of ACO attribution)."
## Problem
`notebooks/cy2027_pfs_proposed_rule.py` executes clean (verified via
`nb_integration.py` in the production notebooks container, 2026-08-17)
but is analytically incomplete in exactly the two ways the request
names:
1. **GPCI.** Every dollar figure is national/unadjusted — the intro
discloses "GPCI = 1.0 … real payment varies by locality via
`pfs.gpci`, which this notebook does not join." The CY2027 NPRM's
proposed GPCIs (Addendum E) were captured to disk in P37
(`data/cms/pfs_nprm/2027/`) but never loaded to any table; the
lake's `pfs.gpci` holds final-rule GPCIs 20162026 only.
2. **Primary-care service-level analysis.** No section examines
individual services. "PQM services" is defined by the request's own
gloss as the **designated primary care services used for MSSP/ACO
beneficiary assignment** (42 CFR § 425.400(c)). The authoritative
in-repo list is `cms_provider_attribution._primary_care_hcpcs_codes`
(aco monolith, 109 HCPCS codes, current-law list incl. APCM
G0556G0558, CHI/PIN G0019G0024, G2211, G3002G3003).
"Fix the notebook" therefore means: close those two gaps, and update
the intro + provenance sections that currently disclaim them.
## Data facts (verified 2026-08-17)
- `pfs.rvu` (lake): 20152026, ~1619k rows/year.
- `pfs.rvu_proposed` (lake): CMS-1832-P (CY2026 NPRM) + CMS-1848-P
(CY2027 NPRM) partitions.
- `pfs.gpci` (lake): 20162026; columns `mac, locality, locality_name,
work_gpci, pe_gpci, mp_gpci, year, state`; ~109112 localities/year.
- `pfs.rules.RULES`: 20142026 incl. `conversion_factor` for every
year 20152026; `proposed_for(2027)` has both CY2027 proposed CFs.
- NPRM Addendum E workbooks on disk for 2026 and 2027 (from
`fetch_nprm_addenda.py`); **not** attached in bib, **not** loaded.
- `cms_provider_attribution._primary_care_hcpcs_codes`: 109 rows
(`hcpcs_code`, `description`), in `aco.duckdb` / `aco.ro.duckdb`
only — not in the lake, so the notebook cannot read it today.
- Pipeline shape: ingest → `aco.duckdb` monolith (host) →
`dev/scripts/publish_reference_to_lake.py` (runs **inside the
notebooks container**) → DuckLake → notebooks read via
`connect.ducklake()`.
- `pfs.calcs.payment.payment(rvu, gpci, cf)` already implements the
locality formula `(work×work_gpci + pe×pe_gpci + mp×mp_gpci) × CF`.
- **Landmine:** the DuckLake catalog metadata has been migrated to a
version only duckdb ≥1.5 attaches. The notebooks venv (1.5.4) works;
the main project venv and api container (1.4.4, from `uv.lock`) fail
with "Only DuckLake versions 0.10.3 supported." Nothing on the
critical path uses the broken combination today, but host-side
`connect.ducklake()` is dead until the pin moves.
## Design
### D1 — `pfs.gpci_proposed` (new lake table)
Mirror of the `rvu_proposed` pattern: one partition per `cms_rule_id`
(CMS-1832-P, CMS-1848-P), loaded from the NPRM **Addendum E** workbook.
Verified layouts: the CY2027 file carries one proposed year (2027
PW/PE/MP); the CY2026 file is wide — 2025 baseline plus proposed 2026
**and** 2027 columns (GPCIs phase in biennially). Load long: columns
`mac, state, locality, locality_name, work_gpci, pe_gpci, mp_gpci,
gpci_year, cms_rule_id`, taking every year-column ≥ the NPRM's rule
year (CMS-1832-P → gpci_year 2026, 2027; CMS-1848-P → gpci_year 2027;
the 2025 baseline column is a restatement, skipped). That makes the
"1832-P's projected 2027 vs 1848-P's actually-proposed 2027"
comparison a plain self-join. Footnote rows (non-numeric MAC) are
filtered; proposed PW GPCIs are published **without** the statutory
1.0 work floor — values load as published and the notebook labels the
floor status rather than silently re-flooring. Loader lives in
`src/pfs/nprm.py` beside
`load_rvu_proposed`, discovers the workbook through the bib attachment
tagged `sup:<year>_PFS_NPRM` (same mechanism as Addendum B), writes
`cms.ingest_log` provenance, and deletes/reinserts only its own
partition. `fetch_nprm_addenda.py` CONFIGS gain an Addendum E
attach (title `CY{year} PFS NPRM Addendum E (proposed GPCIs)`) using
the existing title-guarded idempotent register.
Addendum D (GAFs) is **not** loaded — the GPCI components are the
primitive; GAF adds no information the notebook needs (YAGNI).
### D2 — primary-care code list to the lake
`publish_reference_to_lake.py` currently publishes same-named tables
from three schemas. Extend it with an explicit rename map and publish
`cms_provider_attribution._primary_care_hcpcs_codes` →
**`cms.primary_care_service_code`** (109 rows, `hcpcs_code`,
`description`). `pfs.gpci_proposed` joins the existing `pfs` list.
The publish runs in the notebooks container as documented in that
script's header.
### D3 — notebook changes
New **Section: GPCI & Locality** (between current Sections 2 and 3):
- Near-term: CY2027-proposed vs CY2026-final `work/pe/mp` GPCI deltas
per locality — table of biggest movers + diverging-bar chart; counts
of localities up/down/flat.
- Long-term: GPCI trend 20162026 (+2027 proposed, visually flagged
"final rule pending") for the highest/lowest-GPCI localities and the
floor story (work GPCI floor).
- Payment effect: locality-adjusted payment via `pfs.calcs.payment`
for example codes (reusing the notebook's existing 99213/27447/70553
trio), best vs worst locality vs national.
New **Section: Primary Care Services (ACO attribution set)**:
- Definition callout: the 109-code § 425.400(c) list, read live from
`cms.primary_care_service_code`, with the disclosure that the
current-law designation list is applied retrospectively across
years (the list itself is not versioned in-repo).
- Near-term, **every code individually**: CY2026-final vs
CY2027-proposed work/total RVUs and national payment (both CFs
known), full 109-row sortable table + chart ranked by % payment
change; codes present in only one side surfaced explicitly (new
APCM-family codes have no CY2026 final RVU).
- Long-term, per code: total-RVU and dollar (RVU × that year's final
CF) trajectories 20152026 with the 2027 proposed point appended and
labeled; heatmap (code × year, % change) plus small-multiple lines
for the codes with the largest cumulative moves.
- GPCI cross-cut: the near-term payment delta for a chosen
primary-care code shown at the extreme localities (joins D1).
- Aggregation is unweighted (no utilization data in the lake);
disclosed inline.
**Fixes to existing cells:** intro rewritten (GPCI = 1.0 disclaimer
replaced by a pointer to the GPCI section; the remaining
national-unadjusted figures keep a scoped note), provenance section
extended to `pfs.gpci`, `pfs.gpci_proposed` ingest-log rows and the
primary-care list's source note. Everything else (CF walk, RVU deltas,
APM section) stays.
### D4 — duckdb/DuckLake version alignment
Bump the project's duckdb to ≥1.5.4 (`uv lock`), so host tooling can
attach the migrated catalog again; verify the test suite and that the
monolith (written by 1.4.4) opens cleanly under 1.5.4. If the bump
breaks tests, defer with findings on the issue rather than forcing it
— nothing in P39's critical path requires it (loads target the
monolith; publish + notebook run in the notebooks container).
## Testing
- Loader: unit tests beside the existing `rvu_proposed` NPRM tests —
header detection on the real CY2027 Addendum E layout, partition
isolation (loading 2027 leaves the 1832-P partition untouched),
numeric coercion.
- Publisher: rename-map unit test (pure function).
- Notebook: `nb_integration.py` single-notebook run in the production
notebooks container must stay green; data-sanity assertions inside
the notebook (e.g. 109 codes read, locality counts match) fail loud
in that harness.
## Out of scope
- Utilization-weighted impact (no claims-volume reference in the lake).
- Addendum D / GAF ingestion.
- Versioning the primary-care designation list by rule year.
- Retiring the monolith (#514) — P39 adds one publisher entry, no more.
## P39 build outcomes (2026-08-17)
Issues #629#633, milestone P39 (5 commits, 39dfaa2..afeec59, direct on
main — mail-poller live-mounts this checkout and Tasks ran against the
live monolith/lake). All closed same-day; notebook gate `pass=1` in the
prod notebooks container after each notebook change.
- **#629 (39dfaa2 + the Addendum-E register commit)**:
`load_gpci_proposed` → `pfs.gpci_proposed` (long format with
`gpci_year`; CMS-1832-P carries 2026+2027, CMS-1848-P carries 2027 —
109 localities each; 2025 baseline restatement skipped; PW loaded
floor-free as published). Addendum E attached in bib for both years
(title-guarded, canonical storage paths). 3 new loader tests;
`ingest_pfs.py --nprm` logs `pfs.gpci_proposed` ingest rows.
- **#630 (fc26d5f)**: publisher gained `gpci_proposed` + a rename map;
`cms_provider_attribution._primary_care_hcpcs_codes` →
`cms.primary_care_service_code` (109 rows). Verified from
`connect.ducklake()` in the notebooks container.
- **#631/#632 (8c4e13c, one combined commit — the sections were a
single splice)**: new Sections 3 (GPCI & locality) and 4 (primary
care / attribution set); APM → 5, Provenance → 6; intro's "GPCI=1.0,
does not join pfs.gpci" disclaimer replaced; provenance covers the
gpci tables and sources the attribution list narratively.
- **#633 (afeec59)**: duckdb floor 1.0 → ≥1.5.4 (resolved 1.5.5); full
suite green under the new pin; monolith + replica open cleanly;
host-side `connect.ducklake()` no longer version-blocked (DNS to the
compose-internal catalog remains, by design).
- **Findings worth keeping**: the CY2027 NPRM re-proposes the CY2026
NPRM's 2027 GPCI phase-in for 103 of 109 localities (6 revised);
every designated primary-care code that prices anywhere prices in
*both* CY2026-final and CY2027-proposed tables — the 12 unmatched
are all deleted legacy codes still on the § 425.400(c) list (the
notebook's one-sided narrative was corrected against live data
before commit).
- **Deviation**: #631/#632 shipped as one commit; Task-3 ingest ran via
a targeted driver (same duckdb_batch/ingest_log/replica plumbing)
instead of the full `ingest_pfs.py --nprm` reload, to avoid an
unnecessary multi-minute `load_all` pass.