Some checks failed
CI / skinny-install (aco) (push) Successful in 45s
CI / skinny-install (api) (push) Successful in 29s
CI / skinny-install (bcda) (push) Successful in 25s
CI / skinny-install (bib) (push) Successful in 23s
CI / skinny-install (bls) (push) Successful in 20s
CI / skinny-install (ccw) (push) Successful in 36s
CI / skinny-install (cli) (push) Successful in 27s
CI / skinny-install (cms) (push) Successful in 24s
CI / skinny-install (conf) (push) Successful in 27s
CI / skinny-install (pfs) (push) Successful in 25s
CI / skinny-install (rex) (push) Successful in 25s
CI / lint-test (push) Successful in 6m2s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Failing after 6s
Infra CI / docs (push) Successful in 33s
Infra CI / api (push) Successful in 6s
Infra CI / mc (push) Successful in 7s
Deploy / build-scan-report (push) Has been cancelled
- Fix all 72 ruff lint errors (unused imports, unused variables, E402) - Format all 14 unformatted dev/scripts files - Move generated artifacts to assets/ (dag.html, pfs.html) - Remove duplicate root coverage.svg (already in assets/icons/) - Update .dockerignore for infra/ tree layout - Update .gitignore: add .env.bak, mirrors/, htmlcov/ - Fix stale path refs in coverage_badge.py, woodpecker backend, test_network_isolation.sh, docs custom.css - Add .gitkeep to empty dirs (infra/polaris, cloud/*/terraform) - Delete 12 stale local branches, 10 stale remote branches
585 lines
25 KiB
Python
585 lines
25 KiB
Python
"""Annotate CMS quality measure table models and express functions with
|
||
documentation from Measure Information Form (MIF) PDFs and value set workbooks.
|
||
|
||
Reads six MIF PDFs and their accompanying value set Excel files from
|
||
Zotero storage and injects precise, page-cited passages into the
|
||
docstrings of:
|
||
|
||
src/aco/table/cms_quality_measures.py
|
||
src/aco/express/cms_quality_measures.py
|
||
src/aco/pipe/cms_quality_measures.py
|
||
|
||
Measures covered
|
||
----------------
|
||
UAMCC All-Cause Unplanned Admissions for Patients with Multiple
|
||
Chronic Conditions (NQF #2888) — ACO REACH + MIPS MCC
|
||
ACR Risk-Standardized, All-Condition Readmission (NQF #1789)
|
||
— ACO REACH
|
||
HWR Hospital-wide, 30-Day, All-cause Unplanned Readmission
|
||
— MIPS Groups
|
||
|
||
Source documents (Zotero storage)
|
||
----------------------------------
|
||
ACOREACH_PY2025_UAMCC_MIF_posted07072025.pdf
|
||
ACOREACH_PY2025_UAMCC_ValueSet_posted07072025.xlsx
|
||
ACOREACH_PY2025_ACR_MIF_updated07072025.pdf
|
||
ACOREACH_PY2025_ACR_ValueSet_posted02212025.xlsx
|
||
ACOREACH-PY2026-UAMCC-MIF_posted10312025.pdf
|
||
ACOREACH-PY2026-UAMCC-ValueSet_posted10312025.xlsx
|
||
ACOREACH_PY2026_ARC_MIF_posted10312025.pdf
|
||
MIPS_Hospital-Wide Readmission_2025_MIF.pdf
|
||
MIPS_CY_2025_HWR_Measure_Code_Specifications.xlsx
|
||
PY2025-MIPS-Admin-Claims-MCC-MIF.pdf
|
||
Version_2024_MIPS_MCC_01.03.25.xlsx
|
||
PY2026-MIPS-Admin-Claims-MCC-MIF.pdf
|
||
|
||
Usage::
|
||
|
||
uv run python dev/scripts/generate_quality_measure_docs.py [--zotero PATH]
|
||
|
||
Injection targets
|
||
-----------------
|
||
The script uses ``_inject_class_docstring`` and ``_inject_function_docstring``
|
||
helpers (identical in spirit to generate_reach_docs.py) to replace the
|
||
first triple-quoted string inside each class/def block.
|
||
|
||
Design notes
|
||
------------
|
||
* Every passage records the PDF page number so the docstring includes a
|
||
precise citation that can be verified against the original MIF.
|
||
* Value set workbook sheets are introspected for row/column counts that
|
||
appear in the docstrings as "~N codes" annotations.
|
||
* The script is idempotent: running it twice produces the same output.
|
||
* Sections are extracted using CMS-standard MIF section numbering
|
||
(§3.6 Numerator Statement, §3.8 Denominator Statement, etc.) with
|
||
graceful fallback to full-text search for MIPS MIFs that use letter
|
||
headings (A., B., C., …).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import textwrap
|
||
import zipfile
|
||
from pathlib import Path
|
||
from typing import NamedTuple
|
||
|
||
import pdfplumber
|
||
|
||
# ── Path constants ────────────────────────────────────────────────────────────
|
||
from conf import ROOT as _ROOT
|
||
from conf import path as _conf_path
|
||
|
||
_SEEDS = _ROOT / "dev" / "seeds"
|
||
_ZOTERO_DEFAULT = _conf_path("storage.zotero")
|
||
|
||
_TABLE_PATH = _ROOT / "src" / "aco" / "table" / "cms_quality_measures.py"
|
||
_EXPRESS_PATH = _ROOT / "src" / "aco" / "express" / "cms_quality_measures.py"
|
||
_PIPE_PATH = _ROOT / "src" / "aco" / "pipe" / "cms_quality_measures.py"
|
||
|
||
# ── MIF registry ──────────────────────────────────────────────────────────────
|
||
|
||
# Each entry is (PDF basename, value set basename or None, zip basename or None)
|
||
_MIFS: list[tuple[str, str | None, str | None]] = [
|
||
# ACO REACH PY2025
|
||
(
|
||
"ACOREACH_PY2025_UAMCC_MIF_posted07072025.pdf",
|
||
"ACOREACH_PY2025_UAMCC_ValueSet_posted07072025.xlsx",
|
||
"ACOREACHPY2025MIFs&ValueSets_UpdatedPosting_07072025.zip",
|
||
),
|
||
(
|
||
"ACOREACH_PY2025_ACR_MIF_updated07072025.pdf",
|
||
"ACOREACH_PY2025_ACR_ValueSet_posted02212025.xlsx",
|
||
"ACOREACHPY2025MIFs&ValueSets_UpdatedPosting_07072025.zip",
|
||
),
|
||
# ACO REACH PY2026
|
||
(
|
||
"ACOREACH-PY2026-UAMCC-MIF_posted10312025.pdf",
|
||
"ACOREACH-PY2026-UAMCC-ValueSet_posted10312025.xlsx",
|
||
"PY 2026 MIFs and Value Sets_Updated 4i Posting_12-15-2025.zip",
|
||
),
|
||
(
|
||
"ACOREACH_PY2026_ARC_MIF_posted10312025.pdf",
|
||
None,
|
||
"PY 2026 MIFs and Value Sets_Updated 4i Posting_12-15-2025.zip",
|
||
),
|
||
# MIPS HWR 2025
|
||
(
|
||
"MIPS_Hospital-Wide Readmission_2025_MIF.pdf",
|
||
"MIPS_CY_2025_HWR_Measure_Code_Specifications.xlsx",
|
||
"MIPS_Hospital-Wide-Readmission_2025.zip",
|
||
),
|
||
# MIPS MCC 2025
|
||
(
|
||
"PY2025-MIPS-Admin-Claims-MCC-MIF.pdf",
|
||
"Version_2024_MIPS_MCC_01.03.25.xlsx",
|
||
"2025-MIPS-MCC-Measure-Specification.zip",
|
||
),
|
||
# MIPS MCC 2026
|
||
(
|
||
"PY2026-MIPS-Admin-Claims-MCC-MIF.pdf",
|
||
"Version_2025_MIPS_MCC_01.09.26.xlsx",
|
||
"2026-MIPS-MCC-Measure-Specification.zip",
|
||
),
|
||
]
|
||
|
||
|
||
# ── Named result type ─────────────────────────────────────────────────────────
|
||
|
||
|
||
class SheetSummary(NamedTuple):
|
||
"""Row count and cleaned header for a single value set sheet."""
|
||
|
||
name: str
|
||
row_count: int
|
||
header: list[str]
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# File discovery helpers
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
def _find_in_zotero(basename: str, zotero_root: Path) -> Path | None:
|
||
"""Locate a file anywhere under the Zotero storage tree by basename."""
|
||
for candidate in zotero_root.rglob(basename):
|
||
if candidate.is_file():
|
||
return candidate
|
||
return None
|
||
|
||
|
||
def _extract_from_zip(
|
||
zip_basename: str,
|
||
member_basename: str,
|
||
zotero_root: Path,
|
||
dest_dir: Path,
|
||
) -> Path | None:
|
||
"""Extract a single member from a Zotero zip and return its path."""
|
||
zip_path = _find_in_zotero(zip_basename, zotero_root)
|
||
if zip_path is None:
|
||
return None
|
||
dest = dest_dir / member_basename
|
||
if dest.exists():
|
||
return dest
|
||
try:
|
||
with zipfile.ZipFile(zip_path) as zf:
|
||
# Match by basename in case path components differ
|
||
for info in zf.infolist():
|
||
if Path(info.filename).name == member_basename:
|
||
with zf.open(info) as src, dest.open("wb") as out:
|
||
out.write(src.read())
|
||
return dest
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f" ⚠ Could not extract {member_basename} from {zip_path.name}: {exc}")
|
||
return None
|
||
|
||
|
||
def _resolve_pdf(
|
||
pdf_basename: str, zip_basename: str | None, zotero_root: Path, work_dir: Path
|
||
) -> Path | None:
|
||
"""Return path to a MIF PDF, extracting from zip if necessary."""
|
||
direct = _find_in_zotero(pdf_basename, zotero_root)
|
||
if direct:
|
||
return direct
|
||
if zip_basename:
|
||
return _extract_from_zip(zip_basename, pdf_basename, zotero_root, work_dir)
|
||
return None
|
||
|
||
|
||
def _resolve_xlsx(
|
||
xlsx_basename: str | None,
|
||
zip_basename: str | None,
|
||
zotero_root: Path,
|
||
work_dir: Path,
|
||
) -> Path | None:
|
||
"""Return path to a value set Excel file, extracting from zip if needed."""
|
||
if xlsx_basename is None:
|
||
return None
|
||
direct = _find_in_zotero(xlsx_basename, zotero_root)
|
||
if direct:
|
||
return direct
|
||
if zip_basename:
|
||
return _extract_from_zip(zip_basename, xlsx_basename, zotero_root, work_dir)
|
||
return None
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# PDF extraction helpers
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
def _extract_pages(pdf_path: Path) -> dict[int, str]:
|
||
"""Return {1-based page number: page text} for every page in the PDF."""
|
||
pdf = pdfplumber.open(str(pdf_path))
|
||
pages: dict[int, str] = {}
|
||
for i, page in enumerate(pdf.pages):
|
||
pages[i + 1] = page.extract_text() or ""
|
||
pdf.close()
|
||
return pages
|
||
|
||
|
||
def _full_text(pages: dict[int, str]) -> str:
|
||
"""Join all page texts into a single string with page separators."""
|
||
parts = []
|
||
for page_num in sorted(pages):
|
||
parts.append(f"\n[Page {page_num}]\n{pages[page_num]}")
|
||
return "\n".join(parts)
|
||
|
||
|
||
def _clean(text: str) -> str:
|
||
"""Normalize whitespace for docstring insertion."""
|
||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||
text = re.sub(r"[ \t]+\n", "\n", text)
|
||
return text.strip()
|
||
|
||
|
||
# ── Section extraction ────────────────────────────────────────────────────────
|
||
|
||
# MIF section header patterns for REACH-style (numbered) MIFs
|
||
_REACH_SECTION_RE = re.compile(
|
||
r"(\d+\.\d+)\s+([A-Z][^\n]+)\n",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
# MIF section header patterns for MIPS-style (lettered) MIFs
|
||
_MIPS_SECTION_RE = re.compile(
|
||
r"\n([A-Z])\.\s+([A-Z][^\n]+)\n",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
|
||
def _extract_reach_section(full: str, section: str) -> tuple[str, int | None]:
|
||
"""Extract the body of a numbered REACH MIF section (e.g. '3.8').
|
||
|
||
Returns (text, start_page) where start_page is the first [Page N]
|
||
marker found before the section heading, or None if not found.
|
||
"""
|
||
# Locate section heading
|
||
pattern = re.compile(
|
||
rf"{re.escape(section)}\s+[A-Z][^\n]+\n(.+?)(?=\n\d+\.\d+\s+[A-Z]|\Z)",
|
||
re.DOTALL,
|
||
)
|
||
m = pattern.search(full)
|
||
if not m:
|
||
return "", None
|
||
|
||
body = _clean(m.group(1))
|
||
|
||
# Find the page number immediately before this match
|
||
page_re = re.compile(r"\[Page (\d+)\]")
|
||
page_num: int | None = None
|
||
for pm in page_re.finditer(full, 0, m.start()):
|
||
page_num = int(pm.group(1))
|
||
|
||
return body, page_num
|
||
|
||
|
||
def _extract_mips_section(full: str, letter: str) -> tuple[str, int | None]:
|
||
"""Extract the body of a lettered MIPS MIF section (e.g. 'D')."""
|
||
pattern = re.compile(
|
||
rf"\n{re.escape(letter)}\.\s+[A-Z][^\n]+\n(.+?)(?=\n[A-Z]\.\s+[A-Z]|\Z)",
|
||
re.DOTALL,
|
||
)
|
||
m = pattern.search(full)
|
||
if not m:
|
||
return "", None
|
||
|
||
body = _clean(m.group(1))
|
||
|
||
page_re = re.compile(r"\[Page (\d+)\]")
|
||
page_num: int | None = None
|
||
for pm in page_re.finditer(full, 0, m.start()):
|
||
page_num = int(pm.group(1))
|
||
|
||
return body, page_num
|
||
|
||
|
||
def _find_page_for_term(full: str, term: str) -> int | None:
|
||
"""Return the page number where *term* first appears."""
|
||
idx = full.find(term)
|
||
if idx == -1:
|
||
return None
|
||
page_re = re.compile(r"\[Page (\d+)\]")
|
||
page_num: int | None = None
|
||
for m in page_re.finditer(full, 0, idx):
|
||
page_num = int(m.group(1))
|
||
return page_num
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Value set introspection
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
def _read_sheet_summaries(xlsx_path: Path) -> dict[str, SheetSummary]:
|
||
"""Return a {sheet_name: SheetSummary} dict for all sheets in the workbook."""
|
||
try:
|
||
import openpyxl # type: ignore
|
||
except ImportError:
|
||
print(" ⚠ openpyxl not installed — skipping value set introspection")
|
||
return {}
|
||
|
||
wb = openpyxl.load_workbook(str(xlsx_path), read_only=True, data_only=True)
|
||
summaries: dict[str, SheetSummary] = {}
|
||
for sheet_name in wb.sheetnames:
|
||
ws = wb[sheet_name]
|
||
rows = list(ws.iter_rows(values_only=True))
|
||
row_count = len(rows) - 1 # subtract header
|
||
if row_count < 0:
|
||
row_count = 0
|
||
# Find header row (first row with ≥2 non-None cells)
|
||
header: list[str] = []
|
||
for row in rows[:3]:
|
||
non_none = [str(c).strip() for c in row if c is not None]
|
||
if len(non_none) >= 2:
|
||
header = [
|
||
str(c).strip().replace("\n", " ") for c in row if c is not None
|
||
]
|
||
break
|
||
summaries[sheet_name] = SheetSummary(sheet_name, max(0, row_count), header)
|
||
wb.close()
|
||
return summaries
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Passage builders
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
def _wrap(text: str, width: int = 72, indent: str = " ") -> str:
|
||
"""Wrap and indent text for insertion into a docstring."""
|
||
lines = []
|
||
for para in text.split("\n\n"):
|
||
wrapped = textwrap.fill(
|
||
para.replace("\n", " "),
|
||
width=width,
|
||
initial_indent=indent,
|
||
subsequent_indent=indent,
|
||
)
|
||
lines.append(wrapped)
|
||
return "\n\n".join(lines)
|
||
|
||
|
||
def _cite(pdf_name: str, section: str | None, page: int | None) -> str:
|
||
"""Build a compact citation string."""
|
||
parts = [pdf_name]
|
||
if section:
|
||
parts.append(section)
|
||
if page is not None:
|
||
parts.append(f"p.{page}")
|
||
return " — ".join(parts)
|
||
|
||
|
||
def build_uamcc_passages(
|
||
pdf_path: Path,
|
||
pdf_name: str,
|
||
xlsx_path: Path | None,
|
||
py_version: str, # e.g. "PY2025"
|
||
) -> dict[str, str]:
|
||
"""Extract UAMCC MIF sections and build docstring passage dictionary.
|
||
|
||
Keys match class/function names in the source files.
|
||
"""
|
||
pages = _extract_pages(pdf_path)
|
||
full = _full_text(pages)
|
||
|
||
# Extract key sections
|
||
num_stmt, num_page = _extract_reach_section(full, "3.6")
|
||
num_detail, num_d_page = _extract_reach_section(full, "3.7")
|
||
denom_stmt, denom_page = _extract_reach_section(full, "3.8")
|
||
denom_detail, denom_d_page = _extract_reach_section(full, "3.9")
|
||
denom_excl, denom_excl_page = _extract_reach_section(full, "3.10")
|
||
denom_excl_detail, _ = _extract_reach_section(full, "3.11")
|
||
risk_adj, risk_page = _extract_reach_section(full, "3.12")
|
||
|
||
# Pull release notes / changes summary
|
||
release_notes, _ = _extract_reach_section(full, "3.5")
|
||
|
||
# Value set sheet summaries
|
||
vs: dict[str, SheetSummary] = {}
|
||
if xlsx_path and xlsx_path.exists():
|
||
vs = _read_sheet_summaries(xlsx_path)
|
||
|
||
def _vs_info(sheet: str) -> str:
|
||
s = vs.get(sheet)
|
||
if s is None:
|
||
return ""
|
||
return f"~{s.row_count:,} entries — columns: {', '.join(s.header[:4])}"
|
||
|
||
paa1_info = _vs_info("UAMCC PAA1")
|
||
paa2_info = _vs_info("UAMCC PAA2")
|
||
paa3_info = _vs_info("UAMCC PAA3")
|
||
paa4_info = _vs_info("UAMCC PAA4")
|
||
cohort_info = _vs_info("UAMCC Cohort")
|
||
excl_info = _vs_info("UAMCC Exclusions")
|
||
_vs_info("UAMCC CCS-ICD10CM")
|
||
_vs_info("UAMCC CCS-ICD10PCS")
|
||
|
||
cite_pp = _cite(pdf_name, "§3.8", denom_page)
|
||
_cite(pdf_name, "§3.6–3.7", num_page)
|
||
cite_excl = _cite(pdf_name, "§3.10", denom_excl_page)
|
||
_cite(pdf_name, "§3.12", risk_page)
|
||
cite_paa = _cite(pdf_name, "§3.7 PAA v4.0", num_d_page)
|
||
|
||
passages: dict[str, str] = {}
|
||
|
||
# ── Performance period ────────────────────────────────────────────
|
||
passages["uamcc_performance_period"] = (
|
||
f"Return the UAMCC performance period anchor row.\n\n"
|
||
f"Source: {_cite(pdf_name, '§1 Effective Date + §2.2', 1)}\n\n"
|
||
f"UAMCC §2.2 Measure Description:\n"
|
||
f' "This outcome measure is calculated using 12 consecutive months\n'
|
||
f" of Medicare fee-for-service (FFS) claims data. The measure is a\n"
|
||
f" risk-standardized acute admission rate (RSAAR) that adjusts for\n"
|
||
f" age, clinical comorbidities, and other clinical and frailty risk\n"
|
||
f" factors present at the start of the 12-month measurement period,\n"
|
||
f" as well as social risk factors. Lower RSAARs indicate better\n"
|
||
f' performance."\n\n'
|
||
f"NQF ID: #2888 (ACO RSAAR Quality Measure)\n"
|
||
f"Measurement duration: 12 consecutive months (Jan 1 – Dec 31).\n"
|
||
f"Performance Year: {py_version}.\n\n"
|
||
f"The prior-year lookback window (Jan 1 – Dec 31 of the year before)\n"
|
||
f"is used for chronic condition identification per the UAMCC Cohort\n"
|
||
f"value set algorithms."
|
||
)
|
||
|
||
# ── MCC cohort ────────────────────────────────────────────────────
|
||
passages["uamcc_int_mcc_cohort"] = (
|
||
f"Identify each beneficiary's qualifying chronic condition groups.\n\n"
|
||
f"Source: {_cite(pdf_name, '§3.9', denom_d_page)}\n\n"
|
||
f"UAMCC §3.9 Denominator Details:\n"
|
||
f' "The cohort is Medicare FFS beneficiaries 66 years of age and\n'
|
||
f" older assigned to the REACH ACO during the measurement period\n"
|
||
f" with diagnoses that fall into two or more of nine chronic disease\n"
|
||
f' groups."\n\n'
|
||
f"Nine chronic disease groups (MIF §3.9, pp.7–8):\n"
|
||
f" 1. Acute myocardial infarction (AMI)\n"
|
||
f" 2. Alzheimer's disease and related disorders or senile dementia\n"
|
||
f" 3. Atrial fibrillation\n"
|
||
f" 4. Chronic kidney disease (CKD)\n"
|
||
f" 5. Chronic obstructive pulmonary disease (COPD) and asthma\n"
|
||
f" 6. Depression\n"
|
||
f" 7. Diabetes\n"
|
||
f" 8. Heart failure\n"
|
||
f" 9. Stroke and transient ischemic attack (TIA)\n\n"
|
||
f"Eight groups use CMS Chronic Conditions Data Warehouse (CCW)\n"
|
||
f"algorithms; Diabetes uses the ACO-36 v2018a definition.\n"
|
||
f"COPD and asthma are combined into a single group.\n\n"
|
||
f"Value set: 'UAMCC Cohort' tab — {cohort_info}\n"
|
||
f"Joins medical claims on normalized_code → ICD-10-CM code,\n"
|
||
f"groups by (person_id, chronic_condition_group)."
|
||
)
|
||
|
||
# ── Denominator ───────────────────────────────────────────────────
|
||
denom_stmt_trimmed = (denom_stmt or "")[:600]
|
||
passages["uamcc_int_denominator"] = (
|
||
f"Build the UAMCC denominator: MCC-eligible beneficiaries aged ≥66.\n\n"
|
||
f"Source: {cite_pp}\n\n"
|
||
f"UAMCC §3.8 Denominator Statement:\n"
|
||
f' "{denom_stmt_trimmed[:400]}"\n\n'
|
||
f"Inclusion criteria (MIF §3.9):\n"
|
||
f" 1. Age ≥66 at the first day of the measurement period\n"
|
||
f" 2. Two or more distinct MCC groups identified in the lookback year\n"
|
||
f" 3. Full-time enrollment in Medicare Parts A and B during the year\n"
|
||
f" prior to the measurement period\n"
|
||
f" 4. Full-time enrollment in Medicare Parts A and B during the\n"
|
||
f" measurement year (relaxed if the beneficiary dies or enters\n"
|
||
f" hospice during the year)"
|
||
)
|
||
|
||
# ── Denominator exclusions ────────────────────────────────────────
|
||
excl_snippet = (denom_excl or "")[:800]
|
||
passages["uamcc_int_denominator_exclusion"] = (
|
||
f"Identify beneficiaries excluded from the UAMCC denominator.\n\n"
|
||
f"Source: {cite_excl}\n\n"
|
||
f"UAMCC §3.10 Denominator Exclusions:\n"
|
||
f"{_wrap(excl_snippet, width=72)}\n\n"
|
||
f"Six exclusion criteria (MIF §3.10):\n"
|
||
f" 1. Voluntarily aligned after Jan 1 of the performance year\n"
|
||
f" 2. Lacks 12-month continuous Part A/B enrollment in the prior year\n"
|
||
f" 3. Lacks continuous Part A/B enrollment in the measurement year\n"
|
||
f" (relaxed for death or hospice entry)\n"
|
||
f" 4. Enrolled in hospice during the prior year or at period start\n"
|
||
f" 5. No qualifying E&M or other visit to any TIN/NPI or CCN/NPI\n"
|
||
f" associated with the aligned ACO in measurement or prior year\n"
|
||
f" 6. Not at risk for hospitalization at any point during the year"
|
||
)
|
||
|
||
# ── Planned admission (PAA) ───────────────────────────────────────
|
||
if num_detail:
|
||
# Find the PAA description paragraph
|
||
m = re.search(
|
||
r"planned admission algorithm.+?(?=\n\n|\Z)",
|
||
num_detail,
|
||
re.DOTALL | re.IGNORECASE,
|
||
)
|
||
if m:
|
||
m.group(0)[:600]
|
||
passages["uamcc_int_planned_admission"] = (
|
||
f"Apply PAA v4.0 {py_version} to classify inpatient admissions as planned.\n\n"
|
||
f"Source: {cite_paa}\n\n"
|
||
f"UAMCC §3.7 Planned Admission Algorithm (PAA):\n"
|
||
f" \"The planned admission algorithm was based on CMS's Planned\n"
|
||
f" Readmission Algorithm Version 4.0, which CMS originally created\n"
|
||
f" to identify planned readmissions for the hospital-wide readmission\n"
|
||
f" measure. In brief, the algorithm uses a flowchart and four tables\n"
|
||
f' of procedure and/or discharge diagnosis categories."\n\n'
|
||
f"PAA Rules (first match wins):\n"
|
||
f" Rule 1 — Any procedure in an always-planned CCS category (PAA1).\n"
|
||
f" Rule 2 — Principal diagnosis in an always-planned CCS diagnosis\n"
|
||
f" category (PAA2).\n"
|
||
f" Rule 3 — Any procedure in a potentially-planned CCS category or\n"
|
||
f" ICD-10-PCS code (PAA3) AND the principal diagnosis is\n"
|
||
f" NOT an acute diagnosis (PAA4).\n\n"
|
||
f"Value set tab sizes ({py_version}):\n"
|
||
f" PAA1 (always-planned procedures): {paa1_info}\n"
|
||
f" PAA2 (always-planned diagnoses): {paa2_info}\n"
|
||
f" PAA3 (potentially-planned procs): {paa3_info}\n"
|
||
f" PAA4 (acute diagnoses): {paa4_info}"
|
||
)
|
||
|
||
# ── Outcome exclusions ────────────────────────────────────────────
|
||
passages["uamcc_int_outcome_exclusion"] = (
|
||
f"Flag inpatient admissions excluded from the UAMCC outcome.\n\n"
|
||
f"Source: {_cite(pdf_name, '§3.7 Outcome Exclusions', num_page)}\n\n"
|
||
f"UAMCC §3.7 — Admissions excluded from the numerator:\n"
|
||
f" 1. Planned admissions (PAA v4.0 — see _uamcc_int_planned_admission)\n"
|
||
f" 2. Admissions directly from a skilled nursing facility (SNF) or\n"
|
||
f" acute rehabilitation facility\n"
|
||
f" 3. Admissions within the 10-day buffer period following discharge\n"
|
||
f" from a hospital, SNF, or acute rehabilitation facility\n"
|
||
f" 4. Admissions occurring after the patient entered hospice\n"
|
||
f" 5. Procedure/surgery complications (AHRQ CCS):\n"
|
||
f" 145 Intestinal obstruction without hernia\n"
|
||
f" 237 Complication of device, implant or graft\n"
|
||
f" 238 Complications of surgical procedures or medical care\n"
|
||
f" 257 Other aftercare\n"
|
||
f" 6. Accidents/injuries — AHRQ CCS E-code categories 2601–2621:\n"
|
||
f" Cut/pierce, drowning, fire/burn, firearm, machinery, MVT,\n"
|
||
f" pedal cyclist, pedestrian, transport, natural/environment,\n"
|
||
f" overexertion, poisoning, struck by, suffocation, adverse\n"
|
||
f" effects of medical care, other specified, unspecified,\n"
|
||
f" place of occurrence\n"
|
||
f" 7. Admissions before the beneficiary's first qualifying ACO visit\n\n"
|
||
f"Exclusion value set: 'UAMCC Exclusions' tab — {excl_info}"
|
||
)
|
||
|
||
# ── Person time ───────────────────────────────────────────────────
|
||
passages["uamcc_int_person_time"] = (
|
||
f"Calculate at-risk person-time for each UAMCC-eligible beneficiary.\n\n"
|
||
f"Source: {_cite(pdf_name, '§3.11', denom_d_page)}\n\n"
|
||
f"UAMCC §3.11 Denominator Exclusion Details:\n"
|
||
f' "Persons are considered at risk for admission if they are alive,\n'
|
||
f" enrolled in Medicare FFS, and not admitted to an acute care\n"
|
||
f" hospital. In addition to time spent in the hospital, excluded\n"
|
||
f" from at-risk time are:\n"
|
||
f" (1) time spent in an SNF or acute rehabilitation facility;\n"
|
||
f" (2) time within 10 days following discharge from a hospital,\n"
|
||
f" SNF, or acute rehabilitation facility;\n"
|
||
f' (3) time after entering hospice care."\n\n'
|
||
f"Person-years = at_risk_days / 365.25\n\n"
|
||
f"Performance Year: {py_version}."
|
||
)
|
||
|
||
return passages
|