Files
stack/notebooks/palliative_care_rfi.py
kert 61fbee71ec
Some checks failed
CI / lint (push) Failing after 29s
CI / notebooks-smoke (push) Successful in 1m28s
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 1m0s
Infra CI / zotero (push) Successful in 13s
Infra CI / docs (push) Successful in 13s
Infra CI / api (push) Successful in 1m15s
Infra CI / llm (push) Successful in 46s
Infra CI / mc (push) Successful in 17s
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 16m39s
Notebooks Integration / notebooks-integration (push) Successful in 7m55s
Zotero Sync / zotero-sync (push) Successful in 1m19s
Package Supply Chain / pkg-supply-chain (push) Successful in 1m0s
docs(notebooks): disclose 32B uncertain-adjudication pass in palliative PRISMA methods (refs #653, #655)
The 116 stage-2 uncertains (+2 no-tool-call stragglers) were re-screened
independently by qwen2.5:32b on the rig 4090 with first-pass rationales
withheld: 19 include / 6 exclude / 91 confirmed uncertain (stage-3
material). Final corpus: 4,997/4,999 screened — 2,264 inc / 2,642 exc /
91 unc. 14B rationale archive kept in session scratchpad.
2026-08-18 18:44:21 -04:00

900 lines
34 KiB
Python

import marimo
__generated_with = "0.23.1"
app = marimo.App(width="medium")
@app.cell(hide_code=True)
def _():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def _(fr_md, mo):
mo.md(f"""
# Community-Based Palliative Care RFI — Evidence Base & Payment Context
The CY2027 PFS NPRM (CMS-1848-P, {fr_md("91 FR 43842")}, docket
CMS-2026-2377) carries a Request for Information on
**Community-Based Palliative Care** at {fr_md("91 FR 43949")}-43950.
This notebook is the P42/P43 deliverable behind the comment we will
file to that docket: a PRISMA 2020 systematic evidence base for the
RFI's five questions, plus the payment context for the code
families the RFI itself names — read live from the repo's lake and
registries, nothing hard-coded.
The RFI asks five things:
- **(a)** where CMS should focus **fraud, waste, and abuse**
scrutiny in community-based palliative care;
- **(b)** whether eligibility for a serious-illness service should
be gated on **certified life expectancy**, and what interval the
evidence supports;
- **(c)** whether eligibility is better defined by **impact on
daily function (ADLs)** and/or **caregiver strain**;
- **(d)** which **care-management service elements**
(interdisciplinary teams, home visits, timely follow-up) should
be required — explicitly tied to section II.E of the same rule;
- **(e)** how **quality** of palliative care management should be
measured (MIPS Value Pathway reporting, CBE 3665 "feeling heard
and understood").
CMS asks that every statement be supported by peer-reviewed or
institutional evidence — so the evidence base *is* the response.
""")
return
@app.cell(hide_code=True)
def _():
import altair as alt
import polars as pl
from conf import connect
from conf.display import plain_years
from pfs.rules import RULES, proposed_for
connect.theme()
# connect.theme() puts assets/ on sys.path — reuse the HTI-5 design
# tokens: AMBER/TEAL are the gain/loss diverging pair, INDIGO_MED
# the default series, PLUM/GRAY categorical follow-ons.
from fhirworx import AMBER, GRAY, INDIGO_MED, PLUM, TEAL
# PFS reference data lives in the DuckLake lakehouse (M5, #514) —
# notebooks connect read-only.
con = connect.ducklake()
def q(sql):
return con.execute(sql).pl()
def cfr_md(ref, text=""):
"""eCFR jump link as markdown (P41, #641) — degrades to plain
text on a malformed cite so the notebook never breaks."""
try:
from bib import cfrlink
return cfrlink.md_link(ref, text=text)
except Exception:
return text or ref
def fr_md(ref, text=""):
"""FR web jump link as markdown (P40, #638) — degrades to plain
text when bib.sqlite or the anchor map is unavailable so the
notebook never breaks offline."""
try:
from bib import frlink
return frlink.md_link(ref, store=connect.bib(), text=text or ref)
except Exception:
return text or ref
return (
AMBER,
GRAY,
INDIGO_MED,
PLUM,
RULES,
TEAL,
alt,
cfr_md,
con,
connect,
fr_md,
pl,
plain_years,
proposed_for,
q,
)
# ── 1. PRISMA evidence base ─────────────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 1. PRISMA 2020 Evidence Base — `project:palliative-rfi`
**Identification** (run 2026-08-18,
`dev/scripts/search_pubmed_palliative_rfi.py`), three arms:
1. **Database searches** — eight relevance-sorted, **uncapped**
PubMed E-utilities searches: one per RFI part (a-e), plus three
standards-anchored searches supporting the proposed code family
in Section 3 (CAPC program standards, NASW serious-illness
social work practice, Joint Commission palliative
certification).
2. **Forward snowball** — NCBI elink *cited-by* chasing from five
verified anchor publications: the CAPC screening-criteria
consensus (Weissman & Meier 2011) and CAPC metrics consensus
(2010), the RFI's own two serious-illness-identification cites
(Kelley 2017; Kelley & Bollens-Lund 2018), and the scale paper
behind CBE 3665.
3. **Standards & cited documents** — the three professional
standards themselves plus the five sources the RFI cites are
stored directly as grey-literature items.
Everything lands in bib.sqlite and syncs to Zotero tagged
`module:palliative-rfi` with `rfi:{a-e|codeset}`,
`standard:{capc|nasw|tjc}`, and `source:{pubmed|snowball|standard|
rfi-cited}` — the identification table below is **queried live
from those tags**, so it tracks the corpus as it grows.
**Screening** (stage 2, title/abstract) runs through the repo's
PRISMA pipeline (`stack prisma screen palliative-rfi`) against the
project's criteria/reasons anchors in Zotero, using a **local
self-hosted model (qwen2.5:14b via Ollama on the server GPU)** —
decisions, reason codes, and rationales land back in Zotero as
tags and notes. The flow below is computed **live from those
tags**, so it always reflects current screening progress.
> **Methods disclosures:** single-reviewer LLM screening (no human
> dual-screening yet); records the 14B screener marked `uncertain`
> were adjudicated in a second pass by a larger model
> (qwen2.5:32b) with the first-pass rationale withheld for
> independence; stage-3 full-text eligibility and data extraction
> have not run; grey literature (standards documents,
> OIG/GAO/MedPAC/CMMI) bypasses clinical screening per the
> project criteria.
""")
return
@app.cell(hide_code=True)
def _(connect, mo, pl):
# Identification composition, live from Zotero tags.
_zcon = connect.zotero()
_rows = _zcon.execute("""
WITH proj AS (
SELECT it.itemID
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE t.name = 'module:palliative-rfi'
AND it.itemID NOT IN (
SELECT it2.itemID FROM itemTags it2
JOIN tags t2 ON it2.tagID = t2.tagID
WHERE t2.name LIKE 'prisma:%')
)
SELECT t.name, count(DISTINCT it.itemID) AS n
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE it.itemID IN (SELECT itemID FROM proj)
AND (t.name LIKE 'rfi:%' OR t.name LIKE 'standard:%'
OR t.name LIKE 'source:%')
GROUP BY t.name ORDER BY t.name
""").fetchall()
ident_comp = pl.DataFrame(
[{"tag": name, "records": n} for name, n in _rows]
)
mo.vstack(
[
mo.md("**Identification composition (live from Zotero):**"),
mo.ui.table(ident_comp.to_pandas(), label="Records per identification tag"),
]
)
return
@app.cell(hide_code=True)
def _(connect, mo):
from prisma import flow as _flow
class _RoDb:
"""Duck-typed shim: prisma.flow only touches ``.con``."""
def __init__(self, con):
self.con = con
_zcon = connect.zotero()
flow_counts = _flow.count(_RoDb(_zcon), "palliative-rfi")
flow_mermaid = _flow.mermaid(flow_counts, project="palliative-rfi")
_screened_pct = (
100 * flow_counts.screened / flow_counts.identified
if flow_counts.identified
else 0.0
)
_advancing = flow_counts.screened - flow_counts.excluded_stage2
mo.vstack(
[
mo.mermaid(flow_mermaid),
mo.md(f"""
**{flow_counts.identified:,} identified** →
**{flow_counts.screened:,} screened** ({_screened_pct:.0f}%
of identified) → **{_advancing:,} advance toward
full-text eligibility** (stage 3 pending;
{flow_counts.included:,} through full-text so far).
Diagram and counts are regenerated from Zotero tags on
every notebook run, so they track screening progress
automatically.
"""),
]
)
return (flow_counts,)
@app.cell(hide_code=True)
def _(AMBER, GRAY, TEAL, alt, connect, mo, pl):
# Screening status per RFI domain, live from Zotero tags.
_zcon = connect.zotero()
_rows = _zcon.execute("""
WITH proj AS (
SELECT it.itemID
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE t.name = 'project:palliative-rfi'
AND it.itemID NOT IN (
SELECT it2.itemID FROM itemTags it2
JOIN tags t2 ON it2.tagID = t2.tagID
WHERE t2.name LIKE 'prisma:%')
),
dom AS (
SELECT it.itemID, t.name AS rfi
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE t.name IN ('rfi:a','rfi:b','rfi:c','rfi:d','rfi:e','rfi:codeset')
AND it.itemID IN (SELECT itemID FROM proj)
),
decision AS (
SELECT it.itemID, t.name AS decision
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE t.name IN ('screen:include','screen:exclude','screen:uncertain')
)
SELECT dom.rfi,
coalesce(decision.decision, 'unscreened') AS decision,
count(*) AS n
FROM dom LEFT JOIN decision ON dom.itemID = decision.itemID
GROUP BY dom.rfi, decision.decision
ORDER BY dom.rfi
""").fetchall()
_labels = {
"rfi:a": "(a) fraud/waste/abuse",
"rfi:b": "(b) prognosis eligibility",
"rfi:c": "(c) function/caregiver",
"rfi:d": "(d) delivery & mgmt",
"rfi:e": "(e) quality measures",
"rfi:codeset": "code-family standards arm",
}
rfi_status = pl.DataFrame(
[
{"rfi_part": _labels[r], "decision": d.removeprefix("screen:"), "n": n}
for r, d, n in _rows
]
)
_order = ["include", "uncertain", "exclude", "unscreened"]
rfi_status_chart = (
alt.Chart(rfi_status.to_pandas())
.mark_bar()
.encode(
x=alt.X("n:Q", title="Records"),
y=alt.Y("rfi_part:N", title=None),
color=alt.Color(
"decision:N",
title="Stage-2 decision",
sort=_order,
scale=alt.Scale(
domain=_order,
range=[TEAL, "#C9A227", AMBER, GRAY],
),
),
order=alt.Order("color_decision_sort_index:Q"),
tooltip=["rfi_part", "decision", "n"],
)
.properties(
title="Screening Status by RFI Domain (live)", width=620, height=220
)
)
mo.vstack(
[
rfi_status_chart,
mo.md("""
Records retrieved by more than one domain search carry
multiple `rfi:*` tags and count once per domain here —
the PRISMA flow above deduplicates; this chart
deliberately does not, since a paper can inform two RFI
answers.
"""),
]
)
return
@app.cell(hide_code=True)
def _(connect, mo, pl, plain_years):
# Included studies, live: one row per include, with domain tags.
_zcon = connect.zotero()
_rows = _zcon.execute("""
WITH inc AS (
SELECT it.itemID
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
WHERE t.name = 'screen:include'
AND it.itemID IN (
SELECT it2.itemID FROM itemTags it2
JOIN tags t2 ON it2.tagID = t2.tagID
WHERE t2.name = 'project:palliative-rfi')
)
SELECT
(SELECT idv.value FROM itemData id
JOIN itemDataValues idv ON idv.valueID = id.valueID
WHERE id.itemID = inc.itemID
AND id.fieldID = (SELECT fieldID FROM fields
WHERE fieldName = 'title')) AS title,
(SELECT idv.value FROM itemData id
JOIN itemDataValues idv ON idv.valueID = id.valueID
WHERE id.itemID = inc.itemID
AND id.fieldID = (SELECT fieldID FROM fields
WHERE fieldName = 'publicationTitle')) AS journal,
(SELECT group_concat(t.name, ', ') FROM itemTags it
JOIN tags t ON it.tagID = t.tagID
WHERE it.itemID = inc.itemID
AND t.name LIKE 'rfi:%') AS rfi_parts,
(SELECT group_concat(t.name, ', ') FROM itemTags it
JOIN tags t ON it.tagID = t.tagID
WHERE it.itemID = inc.itemID
AND t.name LIKE 'year:%') AS year
FROM inc
ORDER BY year DESC
""").fetchall()
included = pl.DataFrame(
[
{
"year": (y or "").removeprefix("year:"),
"title": ti or "",
"journal": j or "",
"rfi_parts": r or "",
}
for ti, j, r, y in _rows
]
)
mo.vstack(
[
mo.md(f"""
### Included at title/abstract stage — {included.height} records (live)
Stage-2 includes plus their RFI-domain tags. Every include
still awaits stage-3 full-text eligibility, so this list
can only shrink; the LLM's per-record rationale sits on
each Zotero item as a child note for reviewer audit.
"""),
mo.ui.table(
plain_years(included).to_pandas(),
page_size=25,
label="Stage-2 included records",
),
]
)
return
# ── 2. Payment context ─────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(cfr_md, fr_md, mo):
mo.md(f"""
## 2. Payment Context — the Code Families the RFI Names
The RFI's background ({fr_md("91 FR 43949")}) frames the question
around whether current **E/M, care management, and ACP** billing
reflects palliative practice, and part (e) names **APCM (HCPCS
G0556-G0558)** explicitly. This section prices those families —
CY2026 Final (the current baseline) against the CY2027 NPRM
proposal — from `pfs.rvu` / `pfs.rvu_proposed`, at each vintage's
own non-QP standard CF from `pfs.rules`, national unadjusted
(GPCI = 1.0). Codes on the MSSP primary-care attribution list
({cfr_md("42 CFR 425.400(c)")}, `cms.primary_care_service_code`)
are flagged: for those, valuation moves clinician revenue **and**
ACO attribution denominators at once.
Families and code sets are declared in this cell's source —
membership in each PFS table is queried, never assumed, and codes
a table doesn't price are reported as such.
""")
return
@app.cell(hide_code=True)
def _(RULES, pl, proposed_for, q):
_families = {
"ACP": ["99497", "99498"],
"CCM": ["99437", "99439", "99487", "99489", "99490", "99491"],
"PCM": ["99424", "99425", "99426", "99427"],
"TCM": ["99495", "99496"],
"APCM": ["G0556", "G0557", "G0558"],
}
_fam_rows = [
{"hcpcs": c, "family": fam} for fam, codes in _families.items() for c in codes
]
fam_df = pl.DataFrame(_fam_rows)
_codes_sql = ",".join(f"'{c}'" for c in fam_df["hcpcs"])
_raw = q(f"""
WITH fin AS (
SELECT hcpcs, description,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_2026f
FROM pfs.rvu
WHERE year = 2026 AND (mod IS NULL OR mod = '')
AND hcpcs IN ({_codes_sql})
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
),
prop AS (
SELECT hcpcs, description AS description_p,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_2027p
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND (mod IS NULL OR mod = '')
AND hcpcs IN ({_codes_sql})
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
),
pcs AS (
SELECT hcpcs_code AS hcpcs, 1 AS attribution_code
FROM cms.primary_care_service_code
)
SELECT coalesce(fin.hcpcs, prop.hcpcs) AS hcpcs,
coalesce(fin.description, prop.description_p) AS description,
fin.total_2026f, prop.total_2027p,
coalesce(pcs.attribution_code, 0) = 1 AS mssp_attribution
FROM fin
FULL JOIN prop ON fin.hcpcs = prop.hcpcs
LEFT JOIN pcs ON coalesce(fin.hcpcs, prop.hcpcs) = pcs.hcpcs
""")
_cf26 = RULES[2026].conversion_factor
_cf27p = proposed_for(2027).conversion_factor
family_pay = (
fam_df.join(_raw, on="hcpcs", how="left")
.with_columns(
(pl.col("total_2026f") * _cf26).round(2).alias("dollar_2026final"),
(pl.col("total_2027p") * _cf27p).round(2).alias("dollar_2027proposed"),
)
.with_columns(
(pl.col("dollar_2027proposed") - pl.col("dollar_2026final"))
.round(2)
.alias("dollar_delta"),
pl.when(
pl.col("total_2026f").is_not_null()
& pl.col("total_2027p").is_not_null()
)
.then(
(
100
* (pl.col("dollar_2027proposed") - pl.col("dollar_2026final"))
/ pl.col("dollar_2026final")
).round(2)
)
.alias("pct_change"),
)
.sort("family", "hcpcs")
)
family_pay
return (family_pay,)
@app.cell(hide_code=True)
def _(AMBER, RULES, TEAL, alt, family_pay, fr_md, mo, pl, proposed_for):
_priced = family_pay.filter(
pl.col("dollar_2026final").is_not_null()
& pl.col("dollar_2027proposed").is_not_null()
).with_columns(
pl.when(pl.col("pct_change") >= 0)
.then(pl.lit("Gain"))
.otherwise(pl.lit("Loss"))
.alias("sign"),
(pl.col("hcpcs") + pl.when(pl.col("mssp_attribution")).then(pl.lit(" ●")).otherwise(pl.lit(""))).alias(
"code_label"
),
)
_unpriced = family_pay.filter(
pl.col("dollar_2026final").is_null() | pl.col("dollar_2027proposed").is_null()
)
family_chart = (
alt.Chart(_priced.to_pandas())
.mark_bar()
.encode(
x=alt.X("pct_change:Q", title="% change, CY2026 final → CY2027 proposed (national, GPCI=1.0)"),
y=alt.Y("code_label:N", title=None, sort=alt.SortField(field="pct_change", order="descending")),
color=alt.Color(
"sign:N",
title=None,
scale=alt.Scale(domain=["Gain", "Loss"], range=[TEAL, AMBER]),
legend=alt.Legend(orient="top"),
),
tooltip=[
"hcpcs", "family", "description", "mssp_attribution",
alt.Tooltip("dollar_2026final:Q", format="$.2f"),
alt.Tooltip("dollar_2027proposed:Q", format="$.2f"),
alt.Tooltip("pct_change:Q", format="+.2f"),
],
)
.properties(
title="ACP / Care-Management / APCM Codes — Proposed Payment Change",
width=620,
height=max(220, 16 * _priced.height),
)
.facet(row=alt.Row("family:N", title=None))
.resolve_scale(y="independent")
)
_missing_lines = (
"\n".join(
f" - `{r['hcpcs']}` ({r['family']}) — priced in "
+ (
"CY2026 final only"
if r["dollar_2026final"] is not None
else "CY2027 proposal only"
if r["dollar_2027proposed"] is not None
else "neither table"
)
for r in _unpriced.iter_rows(named=True)
)
or " - (none — every declared code prices in both tables)"
)
mo.vstack(
[
family_chart,
mo.md(f"""
● = MSSP designated primary care service (attribution
code). One-sided or unpriced codes, excluded from the
chart rather than shown with a fabricated baseline:
{_missing_lines}
**Sources:** `pfs.rvu` CY2026 Final
({fr_md(RULES[2026].federal_register_citation)});
`pfs.rvu_proposed` `CMS-1848-P`
({fr_md(proposed_for(2027).federal_register_citation)}) —
**final rule pending**. Dollars blend RVU and CF changes
deliberately: this is what the proposal does to realized
national payment for each service.
"""),
]
)
return
@app.cell(hide_code=True)
def _(AMBER, TEAL, alt, family_pay, mo, pl, q):
# Long-term RVU trajectory for the declared codes: YoY % heatmap,
# final rules 2015-2026 + CY2027 proposal.
_codes_sql = ",".join(f"'{c}'" for c in family_pay["hcpcs"])
_hist = q(f"""
WITH fin AS (
SELECT year, hcpcs,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_rvu
FROM pfs.rvu
WHERE (mod IS NULL OR mod = '') AND year >= 2015
AND hcpcs IN ({_codes_sql})
QUALIFY row_number() OVER (PARTITION BY hcpcs, year ORDER BY hcpcs) = 1
),
prop AS (
SELECT 2027 AS year, hcpcs,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND (mod IS NULL OR mod = '')
AND hcpcs IN ({_codes_sql})
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
)
SELECT * FROM fin UNION ALL SELECT * FROM prop
ORDER BY hcpcs, year
""")
_long = (
_hist.filter(pl.col("total_rvu").is_not_null())
.sort("hcpcs", "year")
.with_columns(
(
100
* (pl.col("total_rvu") / pl.col("total_rvu").shift(1).over("hcpcs") - 1)
)
.round(2)
.alias("rvu_yoy_pct")
)
.join(family_pay.select("hcpcs", "family"), on="hcpcs")
)
family_heatmap = (
alt.Chart(_long.to_pandas())
.mark_rect(stroke="#F7F5F0", strokeWidth=1)
.encode(
x=alt.X("year:O", title="Year (2027 = proposed)"),
y=alt.Y("hcpcs:N", title=None),
color=alt.Color(
"rvu_yoy_pct:Q",
title="RVU YoY %",
scale=alt.Scale(domainMid=0, range=[AMBER, "#EDEBE6", TEAL]),
),
tooltip=[
"hcpcs", "family", "year:O",
alt.Tooltip("total_rvu:Q", format=".2f"),
alt.Tooltip("rvu_yoy_pct:Q", format="+.2f"),
],
)
.properties(
title="Total-RVU Year-over-Year % — RFI Code Families, 2015-2027p",
width=560,
height=16 * _long["hcpcs"].n_unique(),
)
)
mo.vstack(
[
family_heatmap,
mo.md("""
First priced year per code is blank (no prior year). Late
first-appearances are themselves informative: PCM
(99424-7) enters in 2022, APCM (G0556-8) in 2025 — the
between-visit care infrastructure the RFI's part (d)
builds on is recent, which is part of why CMS is asking
what the *next* care-management construct should require.
"""),
]
)
return
# ── 3. Proposed palliative care management code family ─────────────────
@app.cell(hide_code=True)
def _(fr_md, mo):
mo.md(f"""
## 3. Proposed Palliative Care Management Code Family
RFI parts (d) and (e) ({fr_md("91 FR 43950")}) ask which service
elements a future care-management service for seriously ill
beneficiaries must include and how its quality should be
safeguarded. Rather than answer in the abstract, the comment will
propose a **three-code palliative care management family** —
structured the way CMS already structures ACP (initial +
follow-on) and APCM (monthly bundle) — with draft descriptors
grounded in three professional standards, each captured in the
evidence base with its citing literature snowballed
(`rfi:codeset` arm above):
- **CAPC** — program standards for specialty palliative care
(basic standards, May 2025) and the CAPC consensus reports on
screening criteria (Weissman & Meier 2011) and clinical/customer
metrics (2010);
- **NASW** — *Practice Standards for Serious Illness Care: Hospice
and Palliative Social Work*, especially Standard 3 (Assessment),
Standard 4 (Intervention/Treatment Planning), Standard 7
(Documentation), and Standard 8 (Interdisciplinary Teamwork);
- **The Joint Commission** — the PAL performance measure set
behind Advanced Certification for Palliative Care: PAL-01 Pain
Screening, PAL-02 Pain Assessment, PAL-03 Dyspnea Screening,
PAL-04 Treatment Preferences and Goals of Care, PAL-05
Treatment Preferences Discharge Document, with the measure-set
population defined by a face-to-face encounter with a member of
the palliative care core interdisciplinary team (physician,
RN/APN, chaplain/spiritual-care professional, social worker).
> **Draft descriptors offered for comment — not valued codes.**
> Nothing below models RUC valuation, RVU inputs, or budget
> neutrality; eligibility for the family is the subject of RFI
> parts (b)-(c) and is deliberately left to that evidence. The
> point is to give CMS a concrete, standards-anchored structure
> to react to.
""")
return
@app.cell(hide_code=True)
def _(mo, pl):
proposed_codes = pl.DataFrame(
[
{
"code": "PCM-1",
"short_descriptor": "Palliative care screening & initial assessment",
"draft_descriptor": (
"Palliative care screening and comprehensive initial "
"assessment of a patient with serious illness, by a "
"member of the palliative care core interdisciplinary "
"team; including validated symptom screening (pain, "
"dyspnea), assessment of functional status and "
"caregiver strain, psychosocial and spiritual "
"screening, and documentation of screening results in "
"the medical record; first 60 minutes"
),
"capc_basis": "Screening-criteria consensus (Weissman & Meier 2011); daily symptom-assessment metric domain",
"nasw_basis": "Standard 3 — Assessment",
"tjc_basis": "PAL-01 Pain Screening; PAL-02 Pain Assessment; PAL-03 Dyspnea Screening; core-team population definition",
"rfi_parts": "b, c, d",
"pfs_analogs": "99497 (ACP, first 30 min); 99495 (TCM contact window)",
},
{
"code": "PCM-2",
"short_descriptor": "Palliative care planning",
"draft_descriptor": (
"Development and documentation of a patient-centered "
"palliative care plan by the interdisciplinary team "
"with patient and caregiver participation; including "
"goals of care, treatment preferences, advance "
"directive reconciliation, and shared "
"decision-making; recorded as an electronic care "
"plan available at transitions of care, including "
"discharge"
),
"capc_basis": "Patient-centered goals-of-care metric domain (2010 consensus)",
"nasw_basis": "Standard 4 — Intervention/Treatment Planning; Standard 7 — Documentation",
"tjc_basis": "PAL-04 Treatment Preferences and Goals of Care; PAL-05 Treatment Preferences Discharge Document",
"rfi_parts": "d",
"pfs_analogs": "99497/99498 (ACP discussion); care-plan element of 99490 (CCM)",
},
{
"code": "PCM-3",
"short_descriptor": "Palliative care follow-up",
"draft_descriptor": (
"Palliative care follow-up management, per calendar "
"month: symptom reassessment and care-plan revision; "
"continuity with a designated team member; access to "
"timely clinical support; coordination with treating "
"physicians; patient and caregiver education and "
"support; and timely follow-up after emergency "
"department visit or facility discharge"
),
"capc_basis": "Transitions-across-care-sites and caregiver-support metric domains (2010 consensus)",
"nasw_basis": "Standard 8 — Interdisciplinary Teamwork; Standard 6 — Empowerment & Advocacy",
"tjc_basis": "Core-team continuity underlying the PAL population definition",
"rfi_parts": "d, e",
"pfs_analogs": "G0556-G0558 (APCM monthly bundle); 99490/99487 (CCM monthly)",
},
]
)
mo.vstack(
[
mo.ui.table(
proposed_codes.to_pandas(),
label="Proposed palliative care management family — draft descriptors & standards crosswalk",
),
mo.md("""
Service elements in the follow-up descriptor deliberately
track the RFI's own part-(d) example list (continuity with
a designated team member, timely clinical support,
comprehensive symptom and caregiver assessment, electronic
care plans, coordination with treating physicians,
patient/caregiver education, timely follow-up after
ED/discharge) — the crosswalk shows each element is also a
professional-standards requirement, not an invention of
this comment.
"""),
]
)
return (proposed_codes,)
@app.cell(hide_code=True)
def _(family_pay, mo, pl, proposed_codes):
# Live valuation anchors: what the named PFS analogs pay today and
# under the CY2027 proposal (from Section 2's family_pay frame).
_analog_codes = ["99497", "99498", "99495", "99490", "99487", "G0556", "G0557", "G0558"]
analog_anchor = (
family_pay.filter(pl.col("hcpcs").is_in(_analog_codes))
.select(
"hcpcs", "family", "description",
"dollar_2026final", "dollar_2027proposed", "mssp_attribution",
)
.sort("family", "hcpcs")
)
mo.vstack(
[
mo.md("""
### Valuation anchors (live from the lake)
The analog codes each proposed descriptor cites, priced at
CY2026 final and the CY2027 proposal — the payment
neighborhood a valued palliative care management family
would plausibly land in, and the reference points a RUC
crosswalk would start from. (Read live from Section 2's
pricing frame; re-runs track every future ingest.)
"""),
mo.ui.table(
analog_anchor.to_pandas(),
label="PFS analogs for the proposed family",
),
mo.md("""
A quality safeguard consistent with RFI part (e): tie the
monthly follow-up code (PCM-3) to reporting of CBE 3665
("feeling heard and understood") the way APCM ties
G0556-G0558 to the primary-care MIPS Value Pathway — the
included-evidence table in Section 1 carries the measure's
supporting literature (`rfi:e`).
"""),
]
)
return
# ── 4. Provenance ──────────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(fr_md, mo, proposed_for):
from datetime import date
_prop = proposed_for(2027)
_close = _prop.comment_close
_today = date.today()
_open = _today <= _close
_status = "OPEN" if _open else "CLOSED"
mo.callout(
mo.md(f"""
**Comment period: {_status}** — {_prop.cms_rule_id}
({fr_md(_prop.federal_register_citation)}) published
{_prop.published.isoformat()}, comments due
**{_close.isoformat()}** (`proposed_for(2027).comment_close`,
read live; today is {_today.isoformat()}). This notebook's
exhibits and the PRISMA evidence base feed the P44 comment to
docket CMS-2026-2377.
"""),
kind="success" if _open else "danger",
)
return
@app.cell(hide_code=True)
def _(mo, q):
provenance = q("""
SELECT run_id, ingested_at, module, table_name, rule_id,
source_file, sha256, rows, fr_citation, pincite_key
FROM cms.ingest_log
WHERE table_name IN ('pfs.rvu', 'pfs.rvu_proposed')
ORDER BY ingested_at DESC
""")
mo.vstack(
[
mo.md("""
## 4. Provenance
Ingest-log rows for the lake tables Section 2 reads, plus
the evidence-base pipeline: identification —
`dev/scripts/search_pubmed_palliative_rfi.py` (E-utilities
database searches, uncapped, plus elink forward snowball
and grey-literature standards documents; run 2026-08-18);
screening — `stack prisma screen palliative-rfi` with the
project's Zotero criteria anchors; flow — `prisma.flow`
over live Zotero tags. `cms.primary_care_service_code` is
the aco module's attribution reference list (P39), not an
FR ingest.
"""),
mo.ui.table(
provenance.to_pandas(),
label="Ingest Log — Lake Tables Used in Section 2",
),
]
)
return
if __name__ == "__main__":
app.run()