fix(pfs): correct CY2026 final-rule FR citation (90 FR 49266) + citation/robustness cleanups (refs #591 #593 #595 #597)

This commit is contained in:
kert
2026-08-13 16:24:50 -04:00
parent 58234ebb70
commit a444d75f39
6 changed files with 46 additions and 24 deletions

View File

@@ -20,7 +20,7 @@ def _(mo):
(CMS-1832-P, 90 FR 32352, July 16, 2025) proposed the **first-ever
split conversion factor** — separate dollar amounts for clinicians who
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.
**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.*
**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("""
## 2. RVU-Level Deltas — Which HCPCS Codes Move the Most
`pfs.rvu_proposed` (14,169 rows, CMS-1832-P) is compared against
`pfs.rvu` for CY2025 (pre-rule baseline) and CY2026 (what actually got
finalized). Both `pfs.rvu` and `pfs.rvu_proposed` are deduplicated to
one row per HCPCS base code (no modifier), `status_code = 'A'`
(actively priced), and — for the proposed table specifically — a
non-null non-facility PE RVU, since ~29% of `pfs.rvu_proposed` rows
carry a null non-facility *or* facility PE RVU (CMS's Addendum B only
populates the setting a code is actually priced in).
`pfs.rvu_proposed` (14,169 rows as of this ingest, CMS-1832-P) is
compared against `pfs.rvu` for CY2025 (pre-rule baseline) and CY2026
(what actually got finalized). Both `pfs.rvu` and `pfs.rvu_proposed`
are deduplicated to one row per HCPCS base code (no modifier),
`status_code = 'A'` (actively priced), and — for the proposed table
specifically — a non-null non-facility PE RVU, since ~29% of
`pfs.rvu_proposed` rows (as of this ingest) carry a null
non-facility *or* facility PE RVU (CMS's Addendum B only populates
the setting a code is actually priced in).
""")
return
@@ -737,8 +738,8 @@ def _(alt, mo, qp_differential):
[
differential_chart,
mo.md("""
**Sources:** RVUs — `pfs.rvu`, CY2026 (90 FR 101174). CFs —
`pfs.rules.RULES[2026]` (final, 90 FR 101174) and
**Sources:** RVUs — `pfs.rvu`, CY2026 (90 FR 49266). CFs —
`pfs.rules.RULES[2026]` (final, 90 FR 49266) and
`RULES[2026].proposed` (NPRM, 90 FR 32352). National unadjusted
means GPCI = 1.0 — actual payment varies by locality.
"""),

View File

@@ -125,7 +125,7 @@ def _attachment_text(store: Store, item_key: str) -> str:
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:
@@ -138,12 +138,15 @@ def _strip_html_wrapper(text: str) -> str:
The FR text itself also carries inline HTML — e.g. Cloudflare's
obfuscated ``<a href="/cdn-cgi/l/email-protection#...">`` mailto
anchors and page-break anchors. Those are stripped with a plain
``<[^>]+>`` regex (this is a controlled, known corpus — not
attacker-supplied HTML — so no HTML-parsing dependency is needed),
keeping the anchor's inner text. HTML entities (``&amp;`` etc.) are
then unescaped. Plain-text markers like ``[[Page NNNNN]]`` contain no
angle brackets and pass through untouched.
anchors and page-break anchors. Those are stripped with a
``</?[A-Za-z][^>]*>`` regex (this is a controlled, known corpus —
not attacker-supplied HTML — so no HTML-parsing dependency is
needed), keeping the anchor's inner text. The tag must start with a
letter (optionally preceded by ``/``) so free-text comparison
fragments like ``p<0.05`` or ``x>2`` are never mistaken for markup.
HTML entities (``&amp;`` etc.) are then unescaped. Plain-text
markers like ``[[Page NNNNN]]`` contain no angle brackets and pass
through untouched.
"""
idx = text.find("<pre>")
if idx == -1:
@@ -173,7 +176,9 @@ def _rule_text(store: Store, item_key: str) -> str:
for (storage_path,) in rows:
path = Path(storage_path)
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)

View File

@@ -418,7 +418,13 @@ RULES: dict[int, RuleYear] = {
budget_neutrality_adjustor=1.0049, # +0.49% BN adjustment
clinical_labor_rate_update=True,
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,
# 90 FR 32352, transcribed from data/fr_downloads/2025-13271.txt.
proposed=ProposedRule(

View File

@@ -258,7 +258,10 @@ class QppYear(BaseModel):
# the same NPRM) — it reads as stale boilerplate describing the
# pre-2023 -> 2023 statutory step-up, not a change CMS-1832-P
# 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
# the 2026 QP Performance Period (2028 payment year) are set to
# 75 percent for the payment amount, and 50 percent for the
@@ -373,7 +376,12 @@ QPP: dict[int, QppYear] = {
risk_standards=_risk_standards(),
apm_incentive_pct=None,
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
# 32352, transcribed from data/fr_downloads/2025-13271.txt;
# disposition from data/fr_downloads/2025-19787.txt.

View File

@@ -152,6 +152,7 @@ class TestRuleDocs:
"[[Page 12345]]\n"
'Contact us at <a href="/cdn-cgi/l/email-protection#x">'
"someone</a> regarding CMS &amp; PFS.\n"
"A finding was significant (p<0.05 and >2 cm).\n"
"</pre></body></html>"
)
store.attach_file(rule_key, txt)
@@ -165,6 +166,7 @@ class TestRuleDocs:
assert "someone" in text
assert "CMS & PFS" 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):
pdf = tmp_path / "rule.pdf"

View File

@@ -149,7 +149,7 @@ class TestSpecificValues:
assert cy2026.telehealth_originating_site_fee == 30.38
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):
assert cy2026.clinical_labor_rate_update is True