feat(pfs): ProposedRule registry — CY2026 NPRM parameters w/ pincite (refs #591)

This commit is contained in:
kert
2026-08-12 13:12:48 -04:00
parent 2c39737060
commit 0c92ee20e2
2 changed files with 161 additions and 1 deletions

View File

@@ -66,6 +66,71 @@ class CfSegment(BaseModel):
(``"CAA 2024 §3201"``, etc.) or derivation note.""" (``"CAA 2024 §3201"``, etc.) or derivation note."""
class ProposedRule(BaseModel):
"""Parameters as *proposed* in the year's NPRM (pre-final).
CMS publishes a Notice of Proposed Rulemaking (NPRM) months before
the Final Rule; the two can and do differ (comment period,
statutory adjustments, and CMS's own re-estimates all move the
numbers between proposed and final). This model captures the
NPRM's own stated values so they can be compared against the
eventual ``RuleYear`` final values — it is never itself used for
payment math.
CY2026 values are sourced from the CY 2026 PFS NPRM (CMS-1832-P,
90 FR 32352, July 16, 2025), ingested into Zotero as
:pincite:`2KVJ2HKX`."""
cms_rule_id: str
"""CMS internal rule identifier, e.g. ``"CMS-1832-P"`` (the ``-P``
suffix denotes "proposed"; the Final Rule reuses the number with
``-F``)."""
fr_document_number: str
"""Federal Register document number, e.g. ``"2025-13271"`` — the
identifier stamped on the captured full-text file."""
federal_register_citation: str
"""Federal Register citation of the NPRM, e.g. ``"90 FR 32352"``
— from the txt header."""
published: date
"""NPRM publication date in the Federal Register."""
comment_close: date
"""End of the public comment period (from the NPRM's DATES
section)."""
conversion_factor: float
"""Proposed CY2026 nonqualifying-APM (non-QP) standard conversion
factor. See :pincite:`2KVJ2HKX`."""
cf_qp: float | None = None
"""Proposed CY2026 qualifying-APM (QP) standard conversion
factor. See :pincite:`2KVJ2HKX`."""
anesthesia_cf: float | None = None
"""Proposed CY2026 nonqualifying-APM (non-QP) anesthesia
conversion factor. See :pincite:`2KVJ2HKX`."""
anesthesia_cf_qp: float | None = None
"""Proposed CY2026 qualifying-APM (QP) anesthesia conversion
factor. See :pincite:`2KVJ2HKX`."""
budget_neutrality_adjustor: float = 1.0
"""Budget-neutrality adjustment the NPRM applied when deriving
the proposed CFs from the CY2025 conversion factor. See
:pincite:`2KVJ2HKX`."""
telehealth_originating_site_fee: float | None = None
"""Proposed telehealth originating site facility fee ($) for
HCPCS code Q3014. See :pincite:`2KVJ2HKX`."""
notes: str = ""
"""Free-text transcription notes — caveats, values not stated in
the NPRM, or discrepancies with the eventual Final Rule."""
class RuleYear(BaseModel): class RuleYear(BaseModel):
"""Annual PFS rule parameters for a single calendar year.""" """Annual PFS rule parameters for a single calendar year."""
@@ -161,6 +226,14 @@ class RuleYear(BaseModel):
federal_register_citation: str = "" federal_register_citation: str = ""
"""Federal Register citation (e.g. '89 FR 67668').""" """Federal Register citation (e.g. '89 FR 67668')."""
proposed: ProposedRule | None = None
"""NPRM (proposed-rule) parameters for this year, when captured.
The fields above this one are the **Final Rule** values and remain
authoritative for all payment math — ``proposed`` is informational
only, for comparing what CMS proposed against what it finalized.
Never read ``proposed.*`` in place of a final field."""
# ── Derived lookups ─────────────────────────────────────────── # ── Derived lookups ───────────────────────────────────────────
def cf_for_date(self, d: date) -> float: def cf_for_date(self, d: date) -> float:
@@ -346,6 +419,29 @@ RULES: dict[int, RuleYear] = {
clinical_labor_rate_update=True, clinical_labor_rate_update=True,
telehealth_originating_site_fee=30.38, telehealth_originating_site_fee=30.38,
federal_register_citation="90 FR 101174", federal_register_citation="90 FR 101174",
# NPRM values (pre-final) per :pincite:`2KVJ2HKX` — CMS-1832-P,
# 90 FR 32352, transcribed from data/fr_downloads/2025-13271.txt.
proposed=ProposedRule(
cms_rule_id="CMS-1832-P",
fr_document_number="2025-13271",
federal_register_citation="90 FR 32352",
published=date(2025, 7, 16),
comment_close=date(2025, 9, 12),
conversion_factor=33.4209, # nonqualifying APM standard CF
cf_qp=33.5875, # qualifying APM standard CF
anesthesia_cf=20.5728, # nonqualifying APM anesthesia CF
anesthesia_cf_qp=20.6754, # qualifying APM anesthesia CF
budget_neutrality_adjustor=1.0055, # "0.55 percent positive"
telehealth_originating_site_fee=31.85, # HCPCS Q3014
notes=(
"Transcribed from data/fr_downloads/2025-13271.txt "
"(NPRM full text) per :pincite:`2KVJ2HKX`. CFs and "
"anesthesia CFs at file line ~39205-39220 (90 FR "
"32800); telehealth fee at line ~4710 (90 FR 32396); "
"comment-close DATES at line ~106 (90 FR 32352). "
"Final Rule values differ (see fields above)."
),
),
), ),
} }

View File

@@ -11,7 +11,7 @@ from __future__ import annotations
import pytest import pytest
from pfs.rules import RULES, RuleYear, cy2024, cy2025, cy2026 from pfs.rules import RULES, ProposedRule, RuleYear, cy2024, cy2025, cy2026
# ── RuleYear model ─────────────────────────────────────────────── # ── RuleYear model ───────────────────────────────────────────────
@@ -232,3 +232,67 @@ class TestCfForDate:
) )
with pytest.raises(ValueError, match="outside every CF segment"): with pytest.raises(ValueError, match="outside every CF segment"):
ry.cf_for_date(date(2024, 1, 15)) ry.cf_for_date(date(2024, 1, 15))
# ── ProposedRule — CY2026 NPRM parameters ────────────────────────
class TestProposedRule:
"""ProposedRule model and RULES[2026].proposed values."""
def test_cy2026_has_proposed_rule(self):
p = RULES[2026].proposed
assert p is not None
assert p.cms_rule_id == "CMS-1832-P"
assert p.fr_document_number == "2025-13271"
assert p.published.year == 2025
assert p.comment_close > p.published
# exact transcribed values — from data/fr_downloads/2025-13271.txt
assert p.conversion_factor == 33.4209
assert p.cf_qp == 33.5875
assert p.conversion_factor != RULES[2026].conversion_factor # proposed ≠ final
def test_cy2026_proposed_anesthesia_cfs(self):
p = RULES[2026].proposed
assert p.anesthesia_cf == 20.5728
assert p.anesthesia_cf_qp == 20.6754
def test_cy2026_proposed_budget_neutrality_adjustor(self):
assert RULES[2026].proposed.budget_neutrality_adjustor == 1.0055
def test_cy2026_proposed_telehealth_fee(self):
assert RULES[2026].proposed.telehealth_originating_site_fee == 31.85
def test_cy2026_proposed_federal_register_citation(self):
assert RULES[2026].proposed.federal_register_citation == "90 FR 32352"
def test_cy2026_proposed_published_date(self):
from datetime import date
assert RULES[2026].proposed.published == date(2025, 7, 16)
def test_cy2026_proposed_comment_close_date(self):
from datetime import date
assert RULES[2026].proposed.comment_close == date(2025, 9, 12)
def test_pre_2026_years_have_no_proposed_block(self):
assert RULES[2025].proposed is None
def test_proposed_rule_default_notes(self):
from datetime import date
p = ProposedRule(
cms_rule_id="CMS-0000-P",
fr_document_number="2099-00000",
federal_register_citation="99 FR 1",
published=date(2099, 1, 1),
comment_close=date(2099, 3, 1),
conversion_factor=1.0,
)
assert p.notes == ""
assert p.cf_qp is None
assert p.anesthesia_cf is None
assert p.anesthesia_cf_qp is None
assert p.budget_neutrality_adjustor == 1.0
assert p.telehealth_originating_site_fee is None