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 (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.
"""), """),

View File

@@ -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 (``&amp;`` 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 (``&amp;`` 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)

View File

@@ -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(

View File

@@ -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.

View File

@@ -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 &amp; PFS.\n" "someone</a> regarding CMS &amp; 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"

View File

@@ -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