fix(pfs): correct CY2026 final-rule FR citation (90 FR 49266) + citation/robustness cleanups (refs #591 #593 #595 #597)
This commit is contained in:
@@ -20,7 +20,7 @@ def _(mo):
|
|||||||
(CMS-1832-P, 90 FR 32352, July 16, 2025) proposed the **first-ever
|
(CMS-1832-P, 90 FR 32352, July 16, 2025) proposed the **first-ever
|
||||||
split conversion factor** — separate dollar amounts for clinicians who
|
split conversion factor** — separate dollar amounts for clinicians who
|
||||||
qualify as Advanced APM Participants (QPs) versus everyone else — and
|
qualify as Advanced APM Participants (QPs) versus everyone else — and
|
||||||
the Final Rule (90 FR 101174) changed several of those numbers again
|
the Final Rule (90 FR 49266) changed several of those numbers again
|
||||||
before taking effect.
|
before taking effect.
|
||||||
|
|
||||||
**Mechanism:** MACRA 2015 (section 1848(d)(20) of the Act) sets two
|
**Mechanism:** MACRA 2015 (section 1848(d)(20) of the Act) sets two
|
||||||
@@ -142,7 +142,7 @@ def _(alt, cf_walk, mo):
|
|||||||
`pfs.rules` registry yet.*
|
`pfs.rules` registry yet.*
|
||||||
|
|
||||||
**Sources:** CY2025 — 89 FR 98452. CY2026 NPRM — 90 FR 32352
|
**Sources:** CY2025 — 89 FR 98452. CY2026 NPRM — 90 FR 32352
|
||||||
(CMS-1832-P). CY2026 Final Rule — 90 FR 101174.
|
(CMS-1832-P). CY2026 Final Rule — 90 FR 49266.
|
||||||
"""),
|
"""),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -261,14 +261,15 @@ def _(mo):
|
|||||||
mo.md("""
|
mo.md("""
|
||||||
## 2. RVU-Level Deltas — Which HCPCS Codes Move the Most
|
## 2. RVU-Level Deltas — Which HCPCS Codes Move the Most
|
||||||
|
|
||||||
`pfs.rvu_proposed` (14,169 rows, CMS-1832-P) is compared against
|
`pfs.rvu_proposed` (14,169 rows as of this ingest, CMS-1832-P) is
|
||||||
`pfs.rvu` for CY2025 (pre-rule baseline) and CY2026 (what actually got
|
compared against `pfs.rvu` for CY2025 (pre-rule baseline) and CY2026
|
||||||
finalized). Both `pfs.rvu` and `pfs.rvu_proposed` are deduplicated to
|
(what actually got finalized). Both `pfs.rvu` and `pfs.rvu_proposed`
|
||||||
one row per HCPCS base code (no modifier), `status_code = 'A'`
|
are deduplicated to one row per HCPCS base code (no modifier),
|
||||||
(actively priced), and — for the proposed table specifically — a
|
`status_code = 'A'` (actively priced), and — for the proposed table
|
||||||
non-null non-facility PE RVU, since ~29% of `pfs.rvu_proposed` rows
|
specifically — a non-null non-facility PE RVU, since ~29% of
|
||||||
carry a null non-facility *or* facility PE RVU (CMS's Addendum B only
|
`pfs.rvu_proposed` rows (as of this ingest) carry a null
|
||||||
populates the setting a code is actually priced in).
|
non-facility *or* facility PE RVU (CMS's Addendum B only populates
|
||||||
|
the setting a code is actually priced in).
|
||||||
""")
|
""")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -737,8 +738,8 @@ def _(alt, mo, qp_differential):
|
|||||||
[
|
[
|
||||||
differential_chart,
|
differential_chart,
|
||||||
mo.md("""
|
mo.md("""
|
||||||
**Sources:** RVUs — `pfs.rvu`, CY2026 (90 FR 101174). CFs —
|
**Sources:** RVUs — `pfs.rvu`, CY2026 (90 FR 49266). CFs —
|
||||||
`pfs.rules.RULES[2026]` (final, 90 FR 101174) and
|
`pfs.rules.RULES[2026]` (final, 90 FR 49266) and
|
||||||
`RULES[2026].proposed` (NPRM, 90 FR 32352). National unadjusted
|
`RULES[2026].proposed` (NPRM, 90 FR 32352). National unadjusted
|
||||||
means GPCI = 1.0 — actual payment varies by locality.
|
means GPCI = 1.0 — actual payment varies by locality.
|
||||||
"""),
|
"""),
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ def _attachment_text(store: Store, item_key: str) -> str:
|
|||||||
return "\n\n".join(parts)
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
_TAG_RE = re.compile(r"<[^>]+>")
|
_TAG_RE = re.compile(r"</?[A-Za-z][^>]*>")
|
||||||
|
|
||||||
|
|
||||||
def _strip_html_wrapper(text: str) -> str:
|
def _strip_html_wrapper(text: str) -> str:
|
||||||
@@ -138,12 +138,15 @@ def _strip_html_wrapper(text: str) -> str:
|
|||||||
|
|
||||||
The FR text itself also carries inline HTML — e.g. Cloudflare's
|
The FR text itself also carries inline HTML — e.g. Cloudflare's
|
||||||
obfuscated ``<a href="/cdn-cgi/l/email-protection#...">`` mailto
|
obfuscated ``<a href="/cdn-cgi/l/email-protection#...">`` mailto
|
||||||
anchors and page-break anchors. Those are stripped with a plain
|
anchors and page-break anchors. Those are stripped with a
|
||||||
``<[^>]+>`` regex (this is a controlled, known corpus — not
|
``</?[A-Za-z][^>]*>`` regex (this is a controlled, known corpus —
|
||||||
attacker-supplied HTML — so no HTML-parsing dependency is needed),
|
not attacker-supplied HTML — so no HTML-parsing dependency is
|
||||||
keeping the anchor's inner text. HTML entities (``&`` etc.) are
|
needed), keeping the anchor's inner text. The tag must start with a
|
||||||
then unescaped. Plain-text markers like ``[[Page NNNNN]]`` contain no
|
letter (optionally preceded by ``/``) so free-text comparison
|
||||||
angle brackets and pass through untouched.
|
fragments like ``p<0.05`` or ``x>2`` are never mistaken for markup.
|
||||||
|
HTML entities (``&`` etc.) are then unescaped. Plain-text
|
||||||
|
markers like ``[[Page NNNNN]]`` contain no angle brackets and pass
|
||||||
|
through untouched.
|
||||||
"""
|
"""
|
||||||
idx = text.find("<pre>")
|
idx = text.find("<pre>")
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
@@ -173,7 +176,9 @@ def _rule_text(store: Store, item_key: str) -> str:
|
|||||||
for (storage_path,) in rows:
|
for (storage_path,) in rows:
|
||||||
path = Path(storage_path)
|
path = Path(storage_path)
|
||||||
if path.suffix.lower() == ".txt" and path.exists():
|
if path.suffix.lower() == ".txt" and path.exists():
|
||||||
return _strip_html_wrapper(path.read_text())
|
return _strip_html_wrapper(
|
||||||
|
path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
)
|
||||||
return _attachment_text(store, item_key)
|
return _attachment_text(store, item_key)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -418,7 +418,13 @@ RULES: dict[int, RuleYear] = {
|
|||||||
budget_neutrality_adjustor=1.0049, # +0.49% BN adjustment
|
budget_neutrality_adjustor=1.0049, # +0.49% BN adjustment
|
||||||
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",
|
# Corrected from "90 FR 101174" (mistranscribed; no such page in
|
||||||
|
# this rule). The Final Rule (RIN 0938-AV50, Vol. 90 No. 212,
|
||||||
|
# Nov 5, 2025) runs 90 FR 49266-50481; per this registry's
|
||||||
|
# convention (see other years above) the citation is the rule's
|
||||||
|
# first page. The CF statement itself ($33.5675 QP / $33.4009
|
||||||
|
# non-QP) is at the pincite 90 FR 49961.
|
||||||
|
federal_register_citation="90 FR 49266",
|
||||||
# NPRM values (pre-final) per :pincite:`2KVJ2HKX` — CMS-1832-P,
|
# NPRM values (pre-final) per :pincite:`2KVJ2HKX` — CMS-1832-P,
|
||||||
# 90 FR 32352, transcribed from data/fr_downloads/2025-13271.txt.
|
# 90 FR 32352, transcribed from data/fr_downloads/2025-13271.txt.
|
||||||
proposed=ProposedRule(
|
proposed=ProposedRule(
|
||||||
|
|||||||
@@ -258,7 +258,10 @@ class QppYear(BaseModel):
|
|||||||
# the same NPRM) — it reads as stale boilerplate describing the
|
# the same NPRM) — it reads as stale boilerplate describing the
|
||||||
# pre-2023 -> 2023 statutory step-up, not a change CMS-1832-P
|
# pre-2023 -> 2023 statutory step-up, not a change CMS-1832-P
|
||||||
# itself proposes.
|
# itself proposes.
|
||||||
# - Final Rule 90 FR 49928 (line 69796-69798) drops the "increase
|
# - Final Rule 90 FR 49980 (line 69796-69798; corrected from a
|
||||||
|
# mistranscribed "90 FR 49928" — that page marker is at line 64990,
|
||||||
|
# these lines fall between the [[Page 49980]] marker at line 69792
|
||||||
|
# and the [[Page 49981]] marker at line 69913) drops the "increase
|
||||||
# from" framing entirely: "The thresholds to achieve QP status in
|
# from" framing entirely: "The thresholds to achieve QP status in
|
||||||
# the 2026 QP Performance Period (2028 payment year) are set to
|
# the 2026 QP Performance Period (2028 payment year) are set to
|
||||||
# 75 percent for the payment amount, and 50 percent for the
|
# 75 percent for the payment amount, and 50 percent for the
|
||||||
@@ -373,7 +376,12 @@ QPP: dict[int, QppYear] = {
|
|||||||
risk_standards=_risk_standards(),
|
risk_standards=_risk_standards(),
|
||||||
apm_incentive_pct=None,
|
apm_incentive_pct=None,
|
||||||
qp_cf_applies=True,
|
qp_cf_applies=True,
|
||||||
citation="90 FR 49928",
|
# Corrected from "90 FR 49928" alone: the QP threshold statement
|
||||||
|
# itself is at 90 FR 49980 (see docstring pincite above); 49928
|
||||||
|
# is retained alongside it since it's independently the source
|
||||||
|
# for the Partial QP payment-amount confirmation (docstring
|
||||||
|
# pincite, line 65021-65026).
|
||||||
|
citation="90 FR 49928, 49980",
|
||||||
# NPRM proposals per :pincite:`2KVJ2HKX` — CMS-1832-P, 90 FR
|
# NPRM proposals per :pincite:`2KVJ2HKX` — CMS-1832-P, 90 FR
|
||||||
# 32352, transcribed from data/fr_downloads/2025-13271.txt;
|
# 32352, transcribed from data/fr_downloads/2025-13271.txt;
|
||||||
# disposition from data/fr_downloads/2025-19787.txt.
|
# disposition from data/fr_downloads/2025-19787.txt.
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class TestRuleDocs:
|
|||||||
"[[Page 12345]]\n"
|
"[[Page 12345]]\n"
|
||||||
'Contact us at <a href="/cdn-cgi/l/email-protection#x">'
|
'Contact us at <a href="/cdn-cgi/l/email-protection#x">'
|
||||||
"someone</a> regarding CMS & PFS.\n"
|
"someone</a> regarding CMS & PFS.\n"
|
||||||
|
"A finding was significant (p<0.05 and >2 cm).\n"
|
||||||
"</pre></body></html>"
|
"</pre></body></html>"
|
||||||
)
|
)
|
||||||
store.attach_file(rule_key, txt)
|
store.attach_file(rule_key, txt)
|
||||||
@@ -165,6 +166,7 @@ class TestRuleDocs:
|
|||||||
assert "someone" in text
|
assert "someone" in text
|
||||||
assert "CMS & PFS" in text
|
assert "CMS & PFS" in text
|
||||||
assert "[[Page 12345]]" in text
|
assert "[[Page 12345]]" in text
|
||||||
|
assert "p<0.05 and >2 cm" in text
|
||||||
|
|
||||||
def test_pdf_only_falls_back_to_extract_attachment(self, store, rule_key, tmp_path):
|
def test_pdf_only_falls_back_to_extract_attachment(self, store, rule_key, tmp_path):
|
||||||
pdf = tmp_path / "rule.pdf"
|
pdf = tmp_path / "rule.pdf"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class TestSpecificValues:
|
|||||||
assert cy2026.telehealth_originating_site_fee == 30.38
|
assert cy2026.telehealth_originating_site_fee == 30.38
|
||||||
|
|
||||||
def test_cy2026_federal_register(self):
|
def test_cy2026_federal_register(self):
|
||||||
assert cy2026.federal_register_citation == "90 FR 101174"
|
assert cy2026.federal_register_citation == "90 FR 49266"
|
||||||
|
|
||||||
def test_cy2026_clinical_labor_update(self):
|
def test_cy2026_clinical_labor_update(self):
|
||||||
assert cy2026.clinical_labor_rate_update is True
|
assert cy2026.clinical_labor_rate_update is True
|
||||||
|
|||||||
Reference in New Issue
Block a user