fix(pfs): rounds — per-field citations, model fields only from plain sentences covering the code, maintain/recap/hypothetical fixes
Some checks failed
CI / lint (push) Successful in 1m5s
CI / notebooks-smoke (push) Successful in 2m5s
Deploy / changes (push) Successful in 17s
CI / test (push) Failing after 4m32s
Infra CI / notebooks (push) Successful in 1m4s
Infra CI / zotero (push) Successful in 18s
Infra CI / api (push) Successful in 1m27s
Infra CI / docs (push) Successful in 1m40s
Infra CI / llm (push) Successful in 1m5s
Infra CI / mc (push) Successful in 18s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Infra CI / bcda (push) Successful in 1m56s
Deploy / api (push) Successful in 1m32s
Deploy / bcda (push) Successful in 1m47s
Deploy / mc (push) Has been skipped
Deploy / llm (push) Successful in 1m8s
Deploy / report (push) Successful in 17s

R27: RoundRow.field_cites (JSON {field: p_id | "page:N"}, list for a joined
PE note); stamped per source, carried by merge with the winning value, set by
the paragraph rules; a derived decision cites the acted value's source.
R28: model numbers survive only in a non-recap, non-hypothetical sentence
covering the code; comparators/societies/quote only from such sentences; a
model label needs the RUC value and the acted value.
R29: maintain only as CMS's own 'are maintaining'/'will maintain', never after
'recommend(ed) (that)'; Response:/Comment: lead-ins and 'CY 20XX final rule'
recaps; 'would be the/consistent/...' no longer hypothetical; stated
interim/reject labels are not overridden by the numbers.
This commit is contained in:
kert
2026-11-12 01:37:59 -05:00
parent 9a2f1c3db7
commit 07e134f570
4 changed files with 655 additions and 82 deletions

View File

@@ -313,6 +313,10 @@ class RoundRow:
final_p_id: int = 0
#: the paragraph stating the RUC value / percentile / survey data (R26)
ruc_p_id: int = 0
#: R27: JSON object citing each non-empty value field to its source:
#: ``{field: p_id}`` (a paragraph of ``item_key``) or ``"page:N"`` (an FR
#: table cell); ``pe_note`` joined from several sources cites a list
field_cites: str = ""
@dataclass(frozen=True)
@@ -379,7 +383,8 @@ CREATE TABLE IF NOT EXISTS pfs.valuation_round (
survey_total_min DOUBLE, comparators VARCHAR, surveying_societies VARCHAR,
requested_work_rvu DOUBLE, proposed_work_rvu DOUBLE, final_work_rvu DOUBLE,
decision VARCHAR, rationale_p_id INTEGER, rationale_quote VARCHAR,
pe_note VARCHAR, extractor VARCHAR, final_p_id INTEGER, ruc_p_id INTEGER);
pe_note VARCHAR, extractor VARCHAR, final_p_id INTEGER, ruc_p_id INTEGER,
field_cites VARCHAR);
"""
@@ -422,6 +427,10 @@ def ensure_tables(con: Any) -> None:
con.execute(
"ALTER TABLE pfs.valuation_round ADD COLUMN IF NOT EXISTS ruc_p_id INTEGER"
)
# R27: per-field citations.
con.execute(
"ALTER TABLE pfs.valuation_round ADD COLUMN IF NOT EXISTS field_cites VARCHAR"
)
def _insert(con: Any, table: str, rows: Sequence[Any]) -> int:

View File

@@ -12,6 +12,7 @@ model; first non-empty value wins) and ``family_rounds`` runs the lot.
from __future__ import annotations
import json
import re
from collections.abc import Sequence
from dataclasses import dataclass, fields, replace
@@ -210,6 +211,68 @@ def decision_of(ruc: float | None, final: float | None, basis_text: str) -> str:
return ""
# ── per-field citations (R27) ─────────────────────────────────────────
#: value fields ``RoundRow.field_cites`` cites to the paragraph (``p_id``)
#: or FR table page (``"page:N"``) that states them
CITE_FIELDS = (
"ruc_work_rvu",
"ruc_percentile",
"survey_intra_min",
"survey_total_min",
"comparators",
"surveying_societies",
"requested_work_rvu",
"proposed_work_rvu",
"final_work_rvu",
"decision",
"rationale_quote",
"pe_note",
)
def _empty(v: Any) -> bool:
return v is None or v == "" or (type(v) is int and v == 0)
def field_cites(row: RoundRow) -> dict[str, Any]:
"""``row.field_cites`` decoded: ``{field: p_id | "page:N"}`` (a list of
those for a PE note joined from several sources)."""
return json.loads(row.field_cites) if row.field_cites else {}
def _encode(cites: dict[str, Any]) -> str:
return json.dumps(cites, sort_keys=True) if cites else ""
def _stamp(row: RoundRow, cite: int | str) -> RoundRow:
"""A single-source row: every non-empty value field cites *cite*."""
return replace(
row,
field_cites=_encode(
{f: cite for f in CITE_FIELDS if not _empty(getattr(row, f))}
),
)
def _put(row: RoundRow, cite: Any, **changes: Any) -> RoundRow:
"""*row* with *changes*; each changed value field now cites *cite*
(an emptied field, or a ``None`` cite, drops its citation)."""
cites = field_cites(row)
for f, v in changes.items():
if f in CITE_FIELDS:
cites.pop(f, None)
if not _empty(v) and cite is not None:
cites[f] = cite
return replace(row, **changes, field_cites=_encode(cites))
def _prune(row: RoundRow) -> RoundRow:
"""Drop the citations of fields that ended up empty."""
cites = {k: v for k, v in field_cites(row).items() if not _empty(getattr(row, k))}
return replace(row, field_cites=_encode(cites))
def table_rounds(
table: FrTable,
*,
@@ -245,32 +308,35 @@ def table_rounds(
and re.search(r"refine|disagree", basis, re.I)
):
note = f"basis says {basis}; work RVU equals RUC"
pages = table.row_pages
page = table.row_pages[idx] if table.row_pages else table.page
out.append(
RoundRow(
family=family,
code=row[0],
rule_year=rule_year,
item_key=item_key,
rule_kind=rule_kind,
p_id=0,
page=pages[idx] if pages else table.page,
source="fr-table",
table_title=table.title,
ruc_work_rvu=ruc,
ruc_percentile="",
survey_intra_min=None,
survey_total_min=None,
comparators="",
surveying_societies="",
requested_work_rvu=_num(cell(row, "requested")),
proposed_work_rvu=_num(cell(row, "proposed")),
final_work_rvu=final,
decision=decision,
rationale_p_id=0,
rationale_quote=basis[:300],
pe_note=note,
extractor="fr-table",
_stamp(
RoundRow(
family=family,
code=row[0],
rule_year=rule_year,
item_key=item_key,
rule_kind=rule_kind,
p_id=0,
page=page,
source="fr-table",
table_title=table.title,
ruc_work_rvu=ruc,
ruc_percentile="",
survey_intra_min=None,
survey_total_min=None,
comparators="",
surveying_societies="",
requested_work_rvu=_num(cell(row, "requested")),
proposed_work_rvu=_num(cell(row, "proposed")),
final_work_rvu=final,
decision=decision,
rationale_p_id=0,
rationale_quote=basis[:300],
pe_note=note,
extractor="fr-table",
),
f"page:{page}",
)
)
return out
@@ -311,7 +377,19 @@ _NEGATED = re.compile(
re.I,
)
_RUC_VERB = re.compile(r"RUC[- ]recommended|RUC recommends", re.I)
_MAINTAIN = re.compile(r"\b(?:we|are|is)\s+(?:(?:are|is)\s+)?maintain\w*", re.I)
#: CMS's own "are maintaining"/"will maintain" (a proposal in a proposed
#: rule, a final in a final one); a bare "we maintain that ..." argues, and
#: "recommended that we maintain ..." is someone else's request (N3-a)
_MAINTAIN = re.compile(
"".join(
f"(?<!recommend{s} {t}{w})"
for s in ("", "s", "ed")
for t in ("", "that ")
for w in ("", "we ")
)
+ r"\b(?:(?:we\s+)?are\s+maintaining|we\s+will\s+maintain)\b",
re.I,
)
_VERBS = (
("maintain", _MAINTAIN),
("proposed", _PROPOSE),
@@ -327,19 +405,19 @@ _FIELD = {
_CONDITIONAL = re.compile(
r"\bwould (?:need|be|increase|decrease)\b|\bif we were to\b", re.I
)
#: "In the CY 2023 PFS final rule, we ..." / "(CY 2023 PFS final rule)": a
#: recap of another year's rule, not this rule's own valuation
#: "In the CY 2023 PFS final rule, we ..." / "(CY 2023 PFS final rule)" /
#: "Response: In the CY 2023 final rule, ...": a recap of another year's
#: rule, not this rule's own valuation (a mid-sentence "which we accepted
#: in the CY 2009 PFS final rule" is not one: the sentence states a value)
_RECAP = re.compile(
r"^\W*(?:In (?:the )?)?\(?CY\s?(\d{4})\s+(?:PFS|Physician Fee Schedule)"
r"[^,.]{0,40}?\brule\b",
r"^\W*(?:(?:Response|Comment)s?:\s*)?(?:In (?:the )?)?\(?CY\s?(\d{4})\s+"
r"(?:(?:PFS|Physician Fee Schedule)[^,.]{0,40}?\brule\b|(?:proposed|final)\s+rule\b)",
re.I,
)
#: hypotheticals and illustrations: no value is stated ("would be more
#: accurate to finalize ..." is not one: it names what CMS is doing)
#: hypotheticals: no value is stated ("would be more accurate to finalize
#: ..." and "X would be the second highest" are not: they state the value)
_HYPOTHETICAL = re.compile(
r"\bif we were to\b|\bwere we to\b|\bif the RUC\b|\bwould need to\b"
r"|\bwould be (?:the|consistent|higher|lower|equal|similar)\b",
re.I,
r"\bif we were to\b|\bwere we to\b|\bif the RUC\b|\bwould need to\b", re.I
)
_PRINTED_PAGE = re.compile(r"\(\s*printed page \d+\s*\)", re.I)
_FAMILY_AS_PROPOSED = re.compile(
@@ -547,13 +625,15 @@ def _list_codes(text: str) -> list[str]:
return _ANY_CODE.findall(text)
def _percentiles(text: str) -> dict[str, str]:
def _percentiles(text: str, rule_year: int | None = None) -> dict[str, str]:
"""``{code: "25th"|"median"|"75th"}`` for the codes the paragraph
states a survey value for, and only those: (P1) a value followed by
``for [CPT] code(s) <list>`` in one clause, (P2) ``<code> … at the
<value>`` before the next code, (P3) ``<value> for all … codes,
except <list> … <value2>``. A code the paragraph names as deleted
never gets one; a code with two different statements gets none."""
never gets one; a code with two different statements gets none; a
hypothetical sentence, or (given *rule_year*) one recapping another
year's rule, states none."""
text = _PRINTED_PAGE.sub(" ", _expand_ranges(text))
found: dict[str, set[str]] = {}
deleted: set[str] = set()
@@ -562,7 +642,7 @@ def _percentiles(text: str) -> dict[str, str]:
deleted.update(_list_codes(sent))
for sent in _sentences(text):
terms = [(m.start(), m.end(), _label(m)) for m in _TERM.finditer(sent)]
if not terms:
if not terms or _recap(sent, rule_year) or _HYPOTHETICAL.search(sent):
continue
coll = _COLLECTIVE_FOR.search(sent)
if coll and terms[0][0] < coll.start():
@@ -700,7 +780,7 @@ def guard(row: RoundRow, text: str) -> RoundRow:
changes: dict[str, Any] = {
f: None for f in _NUM_FIELDS if getattr(row, f) not in present
}
if row.ruc_percentile != _percentiles(text).get(row.code):
if row.ruc_percentile != _percentiles(text, row.rule_year).get(row.code):
changes["ruc_percentile"] = ""
if row.rationale_quote and row.rationale_quote not in text:
changes["rationale_quote"] = ""
@@ -755,11 +835,11 @@ def regex_rounds(para: dict, codes: set[str], *, family: str) -> list[RoundRow]:
if field == "ruc_work_rvu":
extra = {"ruc_p_id": para["p_id"]}
rows[code] = replace(row, **{field: value}, **extra)
for code, pct in _percentiles(para["text"]).items():
for code, pct in _percentiles(para["text"], year).items():
if code in codes:
row = rows.get(code) or _blank(family, code, para, "regex")
rows[code] = replace(row, ruc_percentile=pct, ruc_p_id=para["p_id"])
return list(rows.values())
return [_stamp(r, para["p_id"]) for r in rows.values()]
def _range_note(text: str) -> str:
@@ -787,7 +867,8 @@ def _supported(
) -> bool:
"""Is *value* stated as a proposal/finalization for *code*: a sentence
naming the code and the value carries a proposing (or finalizing, in a
final rule) verb and no "would need to"/"if we were to" hedge."""
final rule) verb and no "would need to"/"if we were to" hedge, and
neither recaps another year's rule nor is hypothetical."""
verb = _PROPOSE if field == "proposed_work_rvu" else _FINALIZE
if field == "final_work_rvu" and kind != "final":
return False
@@ -798,6 +879,7 @@ def _supported(
and not _recap(sent, year)
and verb.search(sent)
and not _CONDITIONAL.search(sent)
and not _HYPOTHETICAL.search(sent)
and not (field == "final_work_rvu" and _NEGATED.search(sent))
and value in _numbers(sent)
):
@@ -830,15 +912,31 @@ def _pe_note(note: str | None) -> str:
return "" if note.lower().startswith("none") else note
#: labels the numbers never override: the model keeps them only when the
#: code's sentence says the word (N3-d)
_STATED_LABELS = ("interim", "reject")
def _acted_field(row: RoundRow) -> str:
"""The value CMS acted on: the final in a final rule, else the proposal."""
return "final_work_rvu" if row.rule_kind == "final" else "proposed_work_rvu"
def _numbers_decide(row: RoundRow) -> RoundRow:
"""When the RUC value and the value CMS acted on (the final in a final
rule, the proposal in a proposed one) are both present, the numbers
decide the label ("accept" next to 2.27 -> 2.50 is a refinement); a
model label survives only where they cannot."""
acted = row.final_work_rvu if row.rule_kind == "final" else row.proposed_work_rvu
if row.ruc_work_rvu is None or acted is None:
"""When the RUC value and the value CMS acted on are both present, the
numbers decide the label ("accept" next to 2.27 -> 2.50 is a
refinement) and it cites the acted value's source; a model label
survives only where they cannot, or when it is a stated
"interim"/"reject"."""
acted_f = _acted_field(row)
acted = getattr(row, acted_f)
if row.ruc_work_rvu is None or acted is None or row.decision in _STATED_LABELS:
return row
return replace(row, decision=decision_of(row.ruc_work_rvu, acted, ""))
return _put(
row,
field_cites(row).get(acted_f),
decision=decision_of(row.ruc_work_rvu, acted, ""),
)
def has_value(row: RoundRow) -> bool:
@@ -851,17 +949,60 @@ def has_content(row: RoundRow) -> bool:
return has_value(row) or bool(row.decision)
def _plain_sentences(text: str, year: int | None) -> list[str]:
"""The sentences that state something: neither a recap of another
year's rule nor a hypothetical."""
return [
s
for s in _sentences(text)
if not _recap(s, year) and not _HYPOTHETICAL.search(s)
]
def _covers(sent: str, code: str) -> bool:
"""*sent* names *code* (ranges expanded), or speaks collectively ("for
all eight CPT codes ...") without naming any code."""
expanded = _expand_ranges(sent)
if re.search(rf"\b{re.escape(code)}\b", expanded):
return True
return bool(_COLLECTIVE_FOR.search(sent)) and not _ANY_CODE.search(expanded)
def _stated(code: str, value: Any, plain: Sequence[str]) -> Any:
"""*value* when a plain sentence covering *code* carries it, else None."""
if value is None:
return None
return (
value if any(_covers(s, code) and value in _numbers(s) for s in plain) else None
)
def _in_plain(phrase: str, plain: Sequence[str]) -> bool:
return any(phrase.lower() in s.lower() for s in plain)
def rows_from_output(
out: dict, para: dict, codes: set[str], *, family: str, model: str
) -> list[RoundRow]:
"""Build guarded rows from one model reply (a ledger output)."""
"""Build guarded rows from one model reply (a ledger output). R28: a
model field counts only when a plain sentence of the paragraph (no
recap of another year's rule, no hypothetical) states it: a number in
one that covers the code, a comparator/society/quote within one; a
label needs the RUC value and the value CMS acted on, and the numbers
decide it unless the code's sentence says "interim"/"reject". Every
surviving field cites the paragraph."""
year, kind = _para_meta(para)
text = para["text"]
extractor = f"model:{model}@{ROUND_PROMPT_VERSION}"
regex = para_values(text, codes, rule_kind=kind, rule_year=year)
pcts = _percentiles(text)
pcts = _percentiles(text, year)
plain = _plain_sentences(text, year)
quote = out.get("rationale_quote") or ""
societies = "; ".join(out.get("surveying_societies") or [])
if any(_recap(s, year) or _HYPOTHETICAL.search(s) for s in _sentences(quote)):
quote = ""
societies = "; ".join(
x for x in out.get("surveying_societies") or [] if _in_plain(x, plain)
)
rows: list[RoundRow] = []
for d in out.get("codes") or []:
code = d.get("code")
@@ -874,11 +1015,15 @@ def rows_from_output(
}
row = replace(
_blank(family, code, para, extractor),
ruc_work_rvu=d.get("ruc_work_rvu"),
ruc_work_rvu=_stated(code, d.get("ruc_work_rvu"), plain),
ruc_percentile=pcts.get(code, ""),
survey_intra_min=d.get("survey_intra_min"),
survey_total_min=d.get("survey_total_min"),
comparators="; ".join(c for c in d.get("comparators") or [] if c != code),
survey_intra_min=_stated(code, d.get("survey_intra_min"), plain),
survey_total_min=_stated(code, d.get("survey_total_min"), plain),
comparators="; ".join(
c
for c in d.get("comparators") or []
if c != code and _in_plain(c, plain)
),
surveying_societies=societies,
proposed_work_rvu=vals["proposed_work_rvu"],
final_work_rvu=vals["final_work_rvu"],
@@ -890,11 +1035,6 @@ def rows_from_output(
row = guard(row, text)
if row.final_work_rvu is not None:
row = replace(row, final_p_id=para["p_id"])
if not has_value(row) or row.ruc_work_rvu is None:
# a bare or unsupported label ("accept" with no RUC value to
# accept) is dropped; the numbers decide later
row = replace(row, decision="")
row = _numbers_decide(row)
if (
row.ruc_work_rvu is not None
or row.ruc_percentile
@@ -902,7 +1042,19 @@ def rows_from_output(
or row.survey_total_min is not None
):
row = replace(row, ruc_p_id=para["p_id"])
rows.append(row)
row = _stamp(row, para["p_id"])
unstated = row.decision in _STATED_LABELS and not any(
_covers(s, code) and re.search(rf"\b{row.decision}", s, re.I) for s in plain
)
if (
row.ruc_work_rvu is None
or getattr(row, _acted_field(row)) is None
or unstated
):
# no RUC value to judge, no value CMS acted on, or an
# unstated "interim"/"reject": the label goes, numbers decide
row = _put(row, None, decision="")
rows.append(_numbers_decide(row))
return rows
@@ -931,21 +1083,24 @@ _CONFLICT_FIELDS = (
_KEYS = ("family", "code", "item_key")
def _empty(v: Any) -> bool:
return v is None or v == "" or (type(v) is int and v == 0)
def _differs(a: Any, b: Any) -> bool:
if isinstance(a, float) and isinstance(b, float):
return abs(a - b) > 1e-9
return a != b
def _cite_list(cite: Any) -> list[Any]:
if cite is None:
return []
return list(cite) if isinstance(cite, list) else [cite]
def merge_rounds(*sources: Sequence[RoundRow]) -> list[RoundRow]:
"""One row per ``(code, item_key)``. Per field the first non-empty
value in source order wins (``0``/``""``/``None`` are empty, so a
paragraph can fill a table row's missing anchor); a later value that
disagrees on a valuation field is noted in ``pe_note``."""
paragraph can fill a table row's missing anchor) and keeps its source's
citation; a later value that disagrees on a valuation field is noted in
``pe_note``, which then cites every source its notes came from."""
merged: dict[tuple[str, str], RoundRow] = {}
for rows in sources:
for r in rows:
@@ -958,6 +1113,8 @@ def merge_rounds(*sources: Sequence[RoundRow]) -> list[RoundRow]:
continue
changes: dict[str, Any] = {}
notes = [cur.pe_note] if cur.pe_note else []
cites, theirs = field_cites(cur), field_cites(r)
note_cites = _cite_list(cites.get("pe_note"))
if cur.proposed_work_rvu is None and r.proposed_work_rvu is not None:
# the proposal's paragraph anchor travels with the proposal
# (a percentile-only row from another paragraph must not
@@ -966,7 +1123,7 @@ def merge_rounds(*sources: Sequence[RoundRow]) -> list[RoundRow]:
if cur.source != "fr-table":
changes["page"] = r.page
for f in fields(RoundRow):
if f.name in _KEYS or f.name in ("pe_note", "extractor"):
if f.name in _KEYS or f.name in ("pe_note", "extractor", "field_cites"):
continue
old, new = getattr(cur, f.name), getattr(r, f.name)
if f.name in changes or _empty(new):
@@ -983,14 +1140,24 @@ def merge_rounds(*sources: Sequence[RoundRow]) -> list[RoundRow]:
continue # a table's RUC value is cited by its page
if _empty(old):
changes[f.name] = new
cites.pop(f.name, None)
if f.name in theirs: # the cite travels with the value
cites[f.name] = theirs[f.name]
elif f.name in _CONFLICT_FIELDS and _differs(old, new):
notes.append(f"conflict: {f.name} {new} ({r.extractor})")
note_cites += _cite_list(theirs.get(f.name))
if r.pe_note:
notes.append(r.pe_note)
note_cites += _cite_list(theirs.get("pe_note"))
uniq = list(dict.fromkeys(note_cites))
if uniq:
cites["pe_note"] = uniq[0] if len(uniq) == 1 else uniq
if r.extractor not in cur.extractor.split("+"):
changes["extractor"] = f"{cur.extractor}+{r.extractor}"
merged[key] = replace(cur, pe_note="; ".join(notes), **changes)
return list(merged.values())
merged[key] = replace(
cur, pe_note="; ".join(notes), field_cites=_encode(cites), **changes
)
return [_prune(r) for r in merged.values()]
def apply_paragraph_rules(
@@ -1015,8 +1182,9 @@ def apply_paragraph_rules(
and r.final_work_rvu is None
and r.proposed_work_rvu is not None
):
out[i] = replace(
out[i] = _put(
r,
para["p_id"],
final_work_rvu=r.proposed_work_rvu,
final_p_id=para["p_id"],
rationale_p_id=para["p_id"],
@@ -1033,8 +1201,9 @@ def apply_paragraph_rules(
and r.proposed_work_rvu is not None
and r.code in present
):
out[i] = replace(
out[i] = _put(
r,
para["p_id"],
ruc_work_rvu=r.proposed_work_rvu,
ruc_p_id=r.ruc_p_id or para["p_id"],
)

View File

@@ -1344,7 +1344,7 @@ class TestRounds:
rows = [
self._row(),
self._row(rule_year=2008, rule_kind="final"),
self._row(code="99342", rule_year=2023),
self._row(code="99342", rule_year=2023, field_cites='{"decision": 1}'),
]
seen = {}
@@ -1406,7 +1406,9 @@ class TestRounds:
assert "CY2023 final: 2 rounds" in res.output
assert "done 2, skipped 1, failed 0, failed_pending 3" in res.output
assert "wrote 3 rounds" in res.output
assert len(read_rounds(con._real, "HOME-VISITS")) == 3
got = read_rounds(con._real, "HOME-VISITS")
assert len(got) == 3
assert {r.code: r.field_cites for r in got}["99342"] == '{"decision": 1}'
def test_rounds_without_write_writes_nothing(self, con, monkeypatch):
order = []

View File

@@ -13,6 +13,7 @@ from bib.item import Rule
from bib.store import Store
from pfs.codetables import RoundRow, ensure_tables, read_rounds, write_rounds
from pfs.rounds import (
CITE_FIELDS,
ROUND_PROMPT_VERSION,
ROUND_SCHEMA,
FrTable,
@@ -22,6 +23,7 @@ from pfs.rounds import (
decision_of,
family_as_proposed,
family_rounds,
field_cites,
find_tables,
fr_text_path,
guard,
@@ -1139,10 +1141,13 @@ def test_final_p_id_roundtrips_and_old_tables_gain_the_column():
" rationale_quote VARCHAR, pe_note VARCHAR, extractor VARCHAR)"
)
ensure_tables(con)
row = replace(_row(code="99341"), final_p_id=1685, ruc_p_id=877)
cites = '{"final_work_rvu": 1685, "ruc_work_rvu": "page:66361"}'
row = replace(_row(code="99341"), final_p_id=1685, ruc_p_id=877, field_cites=cites)
write_rounds(con, "F", [row])
got = read_rounds(con, "F")[0]
assert (got.final_p_id, got.ruc_p_id) == (1685, 877)
assert (got.final_p_id, got.ruc_p_id, got.field_cites) == (1685, 877, cites)
assert field_cites(got) == {"final_work_rvu": 1685, "ruc_work_rvu": "page:66361"}
assert got == row
# ── fix round 3 (N1, N2, R26) ────────────────────────────────────────
@@ -1346,13 +1351,401 @@ def test_hypothetical_sentences_yield_no_regex_values():
"proposed",
"Were we to propose a work RVU of 2.00 for CPT code 99348, it would be consistent.",
),
(
"proposed",
"Similarly, the RUC-recommended work RVU of 1.40 for CPT code 99348 would be the second highest.",
),
):
assert para_values(t, {"99348"}, rule_kind=k) == [], t
real = "We agree that it would be more accurate to finalize the RUC-recommended work RVU of 1.25 for CPT code 99348."
assert ("99348", "final_work_rvu", 1.25) in para_values(
real, {"99348"}, rule_kind="final"
)
# N3-c: "would be the/consistent/..." states the value (5ITGVDJV ¶429, JCWARTJD ¶833)
genuine = "Similarly, the RUC-recommended work RVU of 1.40 for CPT code 92979 would be the second highest work RVU for add-on codes."
for k in ("proposed", "final"):
assert para_values(genuine, {"92979"}, rule_kind=k) == [
("92979", "ruc_work_rvu", 1.4)
]
t = "We are proposing a work RVU of 1.50 for CPT code 99348, which would be consistent with the RUC survey."
assert para_values(t, {"99348"}, rule_kind="proposed") == [
("99348", "proposed_work_rvu", 1.5)
]
# ── fix round 4 (R27 field cites, R28 model gates, R29 minors) ───────
P878 = (
"We are proposing the RUC-recommended work RVU for all eight CPT codes in the "
"Home or Residence Services CPT code family. We are proposing a work RVU of "
f"3.88 for CPT code 99345 {DESC}, and a work RVU of 3.60 for CPT code 99350 {DESC}."
)
P880 = (
"This means that for CPT code 99345, assuming we finalize the RUC-recommended "
"total time of 126 minutes, prolonged services would be reportable once 141 or "
"more minutes are spent by a physician or NPP providing home or residence "
"services. Likewise, for CPT code 99350, assuming we finalize the "
"RUC-recommended total time of 97 minutes, prolonged services would be "
"reportable once 112 or more minutes are spent by a physician or NPP providing "
"home or residence services."
)
def _p23(p_id, text):
return para(
p_id, text, item="MZ24MX5S", kind="proposed", title=TITLE23P, date="2022-07-29"
)
def _cited_fields(r):
return {f for f in CITE_FIELDS if getattr(r, f) not in (None, "")}
def test_field_cites_cite_each_value_to_the_paragraph_stating_it():
"""R26b/R27: CY2023 proposed 99350 — percentile ¶877, proposal and RUC
value ¶878, total time ¶880 (only ¶880 states the minutes)."""
codes = {"99345", "99350"}
paras = [_p23(877, P877), _p23(878, P878), _p23(880, P880)]
regex = [r for p in paras for r in regex_rounds(p, codes, family="F")]
out = _reply(
_d("99345", ruc_work_rvu=3.88, proposed_work_rvu=3.88, survey_total_min=126),
_d(
"99350",
ruc_work_rvu=3.6,
proposed_work_rvu=3.6,
survey_total_min=97,
decision="accept",
),
)
model = rows_from_output(out, paras[2], codes, family="F", model="m")
rows = {
r.code: r for r in apply_paragraph_rules(merge_rounds([], regex, model), paras)
}
r = rows["99350"]
assert (
r.ruc_work_rvu,
r.proposed_work_rvu,
r.survey_total_min,
r.ruc_percentile,
r.decision,
) == (3.6, 3.6, 97, "median", "accept")
assert field_cites(r) == {
"ruc_percentile": 877,
"ruc_work_rvu": 878,
"proposed_work_rvu": 878,
"survey_total_min": 880,
"decision": 878,
}
assert (r.p_id, r.ruc_p_id) == (878, 877) # row-level anchors unchanged
assert field_cites(rows["99345"])["survey_total_min"] == 880
for row in rows.values():
assert set(field_cites(row)) == _cited_fields(row)
def test_field_cites_final_rule_family_as_proposed():
"""CY2023 final 99341: the final, the decision and the rationale cite ¶1685."""
r = _applied()["99341"]
assert field_cites(r) == {
"ruc_percentile": 1676,
"proposed_work_rvu": 1676,
"ruc_work_rvu": 1676,
"final_work_rvu": 1685,
"decision": 1685,
"rationale_quote": 1685,
}
def test_field_cites_for_table_cells_and_model_rows():
rows = {r.code: r for r in _rounds(codes={"99343", "92557"})}
page = "page:66361"
assert field_cites(rows["99343"]) == {
f: page
for f in (
"ruc_work_rvu",
"requested_work_rvu",
"proposed_work_rvu",
"final_work_rvu",
"decision",
"rationale_quote",
)
}
assert field_cites(rows["92557"])["pe_note"] == page
out = _reply(
_d(
"99341",
proposed_work_rvu=1.0,
comparators=["99350"],
pe_note="removed SK062",
),
quote="We proposed the RUC-recommended work RVU for all eight CPT codes in the Home or Residence Services CPT code family.",
societies=["American Academy of Family Physicians"],
)
text = P1676 + " The American Academy of Family Physicians surveyed."
[r] = rows_from_output(out, para(1676, text), {"99341"}, family="F", model="m")
assert field_cites(r) == {
f: 1676
for f in (
"ruc_percentile",
"proposed_work_rvu",
"comparators",
"surveying_societies",
"rationale_quote",
"pe_note",
)
}
def test_merge_carries_the_winning_sources_cite_and_lists_pe_note_sources():
[table] = _rounds(codes={"99343"})
table = replace(table, item_key="K", proposed_work_rvu=None)
model = rounds_mod._stamp(
replace(
table,
source="fr-para",
extractor="model:m@rounds-v1",
p_id=77,
proposed_work_rvu=2.27,
final_work_rvu=2.6,
ruc_percentile="25th",
survey_intra_min=40.0,
rationale_quote="",
pe_note="pe says",
decision="",
field_cites="",
),
77,
)
later = rounds_mod._stamp(
replace(model, p_id=78, pe_note="more pe", final_work_rvu=2.7), 78
)
[m] = merge_rounds([table], [], [model, later])
c = field_cites(m)
assert c["final_work_rvu"] == c["ruc_work_rvu"] == c["decision"] == "page:66361"
assert c["proposed_work_rvu"] == c["ruc_percentile"] == c["survey_intra_min"] == 77
assert c["rationale_quote"] == "page:66361"
# pe_note joins three notes: the model's two and the conflicts they raised
assert m.pe_note.startswith("conflict: final_work_rvu 2.6")
assert c["pe_note"] == [77, 78]
assert set(c) == _cited_fields(m)
# one source -> a single cite; rows without cites merge without any
[one] = merge_rounds([model])
assert field_cites(one)["pe_note"] == 77
bare = replace(model, field_cites="")
[m] = merge_rounds([bare], [replace(bare, pe_note="x")])
assert field_cites(m) == {}
def test_model_values_from_another_years_rule_are_dropped_in_both_kinds():
"""R28 (R26c): a recap sentence yields no model value and no label."""
t = (
"In the CY 2020 PFS final rule (84 FR 62796), we finalized the "
"RUC-recommended work RVU of 0.48 for CPT code 99281."
)
out = _reply(
_d(
"99281",
ruc_work_rvu=0.48,
final_work_rvu=0.48,
decision="accept",
comparators=["99281", "99282"],
survey_total_min=0.48,
),
quote=t,
)
for kind, title, date in (
("proposed", "CY 2027 PFS Proposed Rule", "2026-07-16"),
("final", "CY 2027 PFS Final Rule", "2026-11-09"),
):
# the only other sentence names the code but states no number
p = para(
1, t + " CPT code 99281 is related.", title=title, date=date, kind=kind
)
[r] = rows_from_output(out, p, {"99281"}, family="F", model="m")
assert not has_content(r), kind
assert (r.ruc_work_rvu, r.decision, r.comparators, r.rationale_quote) == (
None,
"",
"",
"",
)
assert not any(
has_content(x) for x in apply_paragraph_rules(merge_rounds([r]), [p])
)
def test_model_numbers_need_a_plain_sentence_covering_the_code():
t = (
"The RUC recommended a work RVU of 2.00 for CPT code 99348. "
"If we were to accept a survey intra-service time of 30 minutes for CPT code "
"99348, the value would rise. CPT code 99349 is similar. "
"The RUC recommended a total time of 60 minutes for all eight CPT codes in the family. "
"For all codes except CPT code 99350 the RUC recommended 45 minutes of post-service time."
)
out = _reply(
_d("99348", ruc_work_rvu=2.0, survey_intra_min=30, survey_total_min=60),
_d("99349", ruc_work_rvu=2.0, survey_intra_min=45),
)
rows = {
r.code: r
for r in rows_from_output(out, pp(t), {"99348", "99349"}, family="F", model="m")
}
assert (rows["99348"].ruc_work_rvu, rows["99348"].survey_intra_min) == (2.0, None)
assert rows["99348"].survey_total_min == 60 # a collective sentence covers it
# 2.00 is 99348's; 45 sits in a sentence naming another code
assert (rows["99349"].ruc_work_rvu, rows["99349"].survey_intra_min) == (None, None)
# a hypothetical rationale quote is dropped too
q = "If we were to accept a survey intra-service time of 30 minutes for CPT code 99348, the value would rise."
[r] = rows_from_output(
_reply(_d("99348", ruc_work_rvu=2.0), quote=q),
pp(t),
{"99348"},
family="F",
model="m",
)
assert (r.ruc_work_rvu, r.rationale_quote, r.rationale_p_id) == (2.0, "", 0)
def test_model_label_needs_ruc_and_the_acted_value():
"""R28: a final rule's label needs a final; a proposed rule's a proposal."""
t = "We proposed the RUC-recommended work RVU of 2.00 for CPT code 99348."
out = _reply(
_d("99348", ruc_work_rvu=2.0, proposed_work_rvu=2.0, decision="refine")
)
[r] = rows_from_output(out, para(1, t), {"99348"}, family="F", model="m")
assert (r.ruc_work_rvu, r.proposed_work_rvu, r.final_work_rvu) == (2.0, 2.0, None)
assert r.decision == "" and "decision" not in field_cites(r)
t = "We are proposing the RUC-recommended work RVU of 2.00 for CPT code 99348."
[r] = rows_from_output(out, pp(t), {"99348"}, family="F", model="m")
assert r.decision == "accept" and field_cites(r)["decision"] == 1
def test_stated_interim_or_reject_labels_are_not_overridden():
"""N3-d: numbers do not override "interim"/"reject" the sentence says."""
t = "On an interim basis, we are finalizing the RUC-recommended work RVU of 2.00 for CPT code 99348."
out = _reply(_d("99348", ruc_work_rvu=2.0, final_work_rvu=2.0, decision="interim"))
[r] = rows_from_output(out, para(1, t), {"99348"}, family="F", model="m")
assert r.decision == "interim"
assert apply_paragraph_rules([r], [])[0].decision == "interim"
t = (
"We reject the RUC-recommended work RVU of 2.00 for CPT code 99348 and are "
"finalizing a work RVU of 1.80 for CPT code 99348."
)
out = _reply(_d("99348", ruc_work_rvu=2.0, final_work_rvu=1.8, decision="reject"))
[r] = rows_from_output(out, para(1, t), {"99348"}, family="F", model="m")
assert r.decision == "reject"
# the word is not in the sentence: the numbers decide
t = "We are finalizing the RUC-recommended work RVU of 2.00 for CPT code 99348."
for label in ("interim", "reject"):
out = _reply(_d("99348", ruc_work_rvu=2.0, final_work_rvu=2.0, decision=label))
[r] = rows_from_output(out, para(1, t), {"99348"}, family="F", model="m")
assert r.decision == "accept", label
def test_maintain_only_as_cms_own_verb():
"""N3-a: NV3Q43B7 ¶1598/¶1602 are recommendations, not CMS finals."""
for t in (
"The AMA RUC referred CPT code 24160 to the CPT Editorial Panel for revision of "
"the descriptor and recommended that we maintain the current (CY 2012) work RVU "
"of 8.00 for CPT code 24160 for CY 2013.",
"Commenters recommended we maintain the 2009 valued AMA RUC work RVU of 0.60 for CPT code 24160.",
"Commenters recommended that we will maintain a work RVU of 0.60 for CPT code 24160.",
"Commenters recommend we are maintaining a work RVU of 0.60 for CPT code 24160.",
):
assert para_values(t, {"24160"}, rule_kind="final") == [], t
# bare "we maintain that" is an argument: no proposal or final (the RUC's
# own figure is still the RUC's)
t = "We maintain that the RUC-recommended work RVU of 2.00 for CPT code 99348 overstates the work."
assert para_values(t, {"99348"}, rule_kind="final") == [
("99348", "ruc_work_rvu", 2.0)
]
t = "We will maintain a work RVU of 1.56 for CPT code 99348."
assert para_values(t, {"99348"}, rule_kind="final") == [
("99348", "final_work_rvu", 1.56)
]
assert para_values(t, {"99348"}, rule_kind="proposed") == [
("99348", "proposed_work_rvu", 1.56)
]
t = "Therefore we are maintaining 2.8 work RVUs for CPT code 43219."
assert para_values(t, {"43219"}, rule_kind="final") == [
("43219", "final_work_rvu", 2.8)
]
def test_recap_lead_ins_and_prefixes():
"""N3-b: "Response:"/"Comment:" lead-ins and "CY 20XX final rule" without "PFS"."""
for t in (
"Response: In the CY 2023 PFS final rule, we finalized a work RVU of 1.00 for CPT code 99341.",
"Comment: In the CY 2023 final rule, we finalized a work RVU of 1.00 for CPT code 99341.",
"In the CY 2023 final rule, we finalized a work RVU of 1.00 for CPT code 99341.",
"In the CY 2023 proposed rule, we proposed a work RVU of 1.00 for CPT code 99341.",
):
for kind in ("proposed", "final"):
assert para_values(t, {"99341"}, rule_kind=kind, rule_year=2027) == [], t
# mid-sentence references and "Comment on the ..." headings still state values
kept = (
(
"The AMA RUC recommended 3.61 work RVUs for CPT code 17106, which we "
"accepted in the CY 2009 PFS final rule with comment.",
2010,
("17106", "ruc_work_rvu", 3.61),
),
(
"Comment on the CY 2016 PFS final rule with comment period: Commenters "
"urged CMS to accept the RUC-recommended work RVU of 9.00 for CPT code 43210.",
2017,
("43210", "ruc_work_rvu", 9.0),
),
(
"Similarly, we are finalizing a work RVU of 8.94 for CPT code 43276 based "
"upon the formula described in the CY 2014 final rule with comment period.",
2015,
("43276", "final_work_rvu", 8.94),
),
)
for t, year, triple in kept:
assert para_values(t, {triple[0]}, rule_kind="final", rule_year=year) == [
triple
], t
def test_percentiles_skip_recaps_and_hypotheticals():
recap = "In the CY 2020 PFS final rule, the RUC recommended the 25th percentile for CPT code 99341."
assert _percentiles(recap, rule_year=2027) == {}
assert _percentiles(recap) == {"99341": "25th"} # no rule year, no recap test
hypo = "If the RUC recommended the median for CPT code 99341, the value would rise."
assert _percentiles(hypo) == {}
[r] = regex_rounds(
para(
1,
recap + " We proposed a work RVU of 1.00 for CPT code 99341.",
title="CY 2027 PFS Final Rule",
date="2026-11-09",
),
{"99341"},
family="F",
)
assert (r.ruc_percentile, r.proposed_work_rvu) == ("", 1.0)
assert guard(replace(r, ruc_percentile="25th"), recap).ruc_percentile == ""
def test_family_rounds_cite_every_value(tmp_path):
s, _ = _store(tmp_path)
ask = FakeAsk(_good_reply())
rows, _ = family_rounds(None, s, FAM, ask=ask, log=RunLog(tmp_path / "l.jsonl"))
assert rows
for r in rows:
assert set(field_cites(r)) == _cited_fields(r), r.code
def test_rule_items_keep_an_item_without_an_fr_document_row(tmp_path):
s = Store(":memory:", storage_dir=tmp_path / "st")
key = s.create(Rule(title=TITLE23, date_published="2022-11-18"))
s._con().execute(
"INSERT INTO fr_anchors (item_key, p_id, page, ordinal, text) VALUES (?,?,?,?,?)",
(key, 1, 1, 1, P1685),
)
assert [i["item_key"] for i in rule_items(s, [])] == [key]
def test_percentiles_code_mentions_without_a_survey_value():
t = (
"The RUC recommended the 25th percentile work RVU after CPT code 99342 was "
"surveyed, with CPT code 99341 at the median intra-service time."
)
assert _percentiles(t) == {}