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.
1752 lines
66 KiB
Python
1752 lines
66 KiB
Python
"""pfs.rounds — valuation rounds from FR RUC/CMS tables (dossier task 4)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import duckdb
|
|
|
|
import pfs.rounds as rounds_mod
|
|
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,
|
|
_percentiles,
|
|
apply_paragraph_rules,
|
|
candidate_paragraphs,
|
|
decision_of,
|
|
family_as_proposed,
|
|
family_rounds,
|
|
field_cites,
|
|
find_tables,
|
|
fr_text_path,
|
|
guard,
|
|
has_content,
|
|
merge_rounds,
|
|
model_rounds,
|
|
para_values,
|
|
regex_rounds,
|
|
rounds_inventory,
|
|
rows_from_output,
|
|
ruc_as_proposed,
|
|
rule_items,
|
|
split_pages,
|
|
table_rounds,
|
|
)
|
|
from pfs.runlog import RunLog
|
|
|
|
FIXTURE = (Path(__file__).parent / "fixtures" / "fr_table_cy2008_t14.txt").read_text()
|
|
CODES = {"99326", "99343", "99350", "93325"}
|
|
|
|
|
|
def _t14() -> FrTable:
|
|
[t] = [t for t in find_tables(FIXTURE) if t.title.startswith("Table 14")]
|
|
return t
|
|
|
|
|
|
def _rounds(**kw):
|
|
args = dict(
|
|
family="HOME-VISITS",
|
|
item_key="54GTKR5G",
|
|
rule_year=2008,
|
|
rule_kind="final",
|
|
codes={"99326", "99343", "99350"},
|
|
)
|
|
args.update(kw)
|
|
return table_rounds(_t14(), **args)
|
|
|
|
|
|
def test_table_rounds_cy2008_home_visits():
|
|
rows = {r.code: r for r in _rounds()}
|
|
r = rows["99343"]
|
|
assert (
|
|
r.proposed_work_rvu,
|
|
r.requested_work_rvu,
|
|
r.ruc_work_rvu,
|
|
r.final_work_rvu,
|
|
) == (2.27, 2.65, 2.27, 2.53)
|
|
assert (r.decision, r.page, r.source) == ("refine", 66361, "fr-table")
|
|
assert rows["99326"].final_work_rvu == 2.63
|
|
assert rows["99350"].final_work_rvu == 3.28
|
|
assert "92557" not in rows
|
|
assert r.table_title.startswith("Table 14")
|
|
assert r.rationale_quote == "Refinement." and r.pe_note == ""
|
|
|
|
|
|
def test_refinement_basis_with_ruc_equal_final_keeps_accept_and_notes_it():
|
|
rows = {r.code: r for r in _rounds(codes={"92557"})}
|
|
r = rows["92557"]
|
|
assert (r.ruc_work_rvu, r.final_work_rvu) == (0.6, 0.6)
|
|
assert r.decision == "accept"
|
|
assert r.rationale_quote == "Refinement."
|
|
assert r.pe_note == "basis says Refinement.; work RVU equals RUC"
|
|
|
|
|
|
def test_continuation_lines_do_not_create_rows():
|
|
t = _t14()
|
|
assert [row[0] for row in t.rows] == ["92557", "99326", "99343", "99350", "93325"]
|
|
assert t.rows[1][2] == "Domicil/r-home visit new pat."
|
|
assert t.header[3].lower().startswith("proposed")
|
|
|
|
|
|
def test_non_numeric_ruc_is_none():
|
|
[r] = _rounds(codes={"93325"})
|
|
assert r.ruc_work_rvu is None and r.final_work_rvu == 0.07
|
|
assert r.decision == ""
|
|
|
|
|
|
def test_agree_maps_to_accept():
|
|
t = FrTable(
|
|
"Table 9.--x",
|
|
10,
|
|
("code", "RUC recommendation", "2009 work RVU", "Basis for decision"),
|
|
(("99211", "2.35", "2.35", "Agree."),),
|
|
)
|
|
[r] = table_rounds(
|
|
t, family="F", item_key="K", rule_year=2009, rule_kind="final", codes={"99211"}
|
|
)
|
|
assert r.decision == "accept" and r.final_work_rvu == 2.35
|
|
assert r.proposed_work_rvu is None
|
|
|
|
|
|
def test_header_roles_proposed_vs_final():
|
|
t = FrTable(
|
|
"Table 1.--x",
|
|
1,
|
|
("code", "proposed work RVU", "2008 work RVU", "CMS"),
|
|
(("99211", "1.00", "1.50", "x"),),
|
|
)
|
|
[r] = table_rounds(
|
|
t, family="F", item_key="K", rule_year=2008, rule_kind="final", codes={"99211"}
|
|
)
|
|
assert (r.proposed_work_rvu, r.final_work_rvu) == (1.0, 1.5)
|
|
|
|
|
|
def test_table_without_roles_skipped_and_short_rows_ignored():
|
|
t = FrTable("Table 2.--y", 1, ("code", "descriptor"), (("99211", "x"),))
|
|
assert (
|
|
table_rounds(
|
|
t, family="F", item_key="K", rule_year=1, rule_kind="final", codes={"99211"}
|
|
)
|
|
== []
|
|
)
|
|
t2 = FrTable("Table 3.--z", 1, ("code", "RUC rec", "final"), (("99211",),))
|
|
assert (
|
|
table_rounds(
|
|
t2,
|
|
family="F",
|
|
item_key="K",
|
|
rule_year=1,
|
|
rule_kind="final",
|
|
codes={"99211"},
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
def test_decision_of():
|
|
assert decision_of(1.0, 2.0, "interim values") == "interim"
|
|
assert decision_of(1.0, 2.0, "Agree.") == "accept"
|
|
assert decision_of(2.0, 2.0, "") == "accept"
|
|
assert decision_of(1.0, 2.0, "Refinement.") == "refine"
|
|
assert decision_of(None, 2.0, "Disagree") == "refine"
|
|
assert decision_of(1.0, 2.0, "") == "refine"
|
|
assert decision_of(None, 2.0, "We reject the RUC") == "reject"
|
|
assert decision_of(None, None, "Comments.") == ""
|
|
|
|
|
|
def test_interim_from_title():
|
|
t = FrTable(
|
|
"Table 5.--Interim work RVUs",
|
|
1,
|
|
("code", "RUC rec", "final"),
|
|
(("99211", "1.0", "1.2"),),
|
|
)
|
|
[r] = table_rounds(
|
|
t, family="F", item_key="K", rule_year=1, rule_kind="final", codes={"99211"}
|
|
)
|
|
assert r.decision == "interim"
|
|
|
|
|
|
def test_split_pages_numbers_text():
|
|
pages = split_pages("front\n[[Page 7]]\nabc\n[[Page 8]]\ndef")
|
|
assert [p for p, _ in pages] == [0, 7, 8]
|
|
assert "abc" in pages[1][1] and "def" in pages[2][1]
|
|
assert split_pages("[[Page 3]]\nx")[0][0] == 3
|
|
|
|
|
|
def test_find_tables_spanning_pages_and_no_table():
|
|
assert find_tables("nothing here") == []
|
|
text = (
|
|
"[[Page 5]]\nTable 1.--Spans\n" + "-" * 30 + "\n"
|
|
" Code Value\n" + "-" * 30 + "\n"
|
|
"11111.... 1.00\n[[Page 6]]\n22222.... 2.00\n" + "-" * 30 + "\n"
|
|
)
|
|
[t] = find_tables(text)
|
|
assert t.page == 5 and [r[0] for r in t.rows] == ["11111", "22222"]
|
|
assert t.row_pages == (5, 6)
|
|
# an unterminated / headerless title is dropped
|
|
assert find_tables("Table 2.--x\nno rules\n") == []
|
|
assert find_tables("Table 2.--x\n" + "-" * 20 + "\nHdr Val\n") == []
|
|
rule = "-" * 20
|
|
assert find_tables(f"Table 2.--x\n{rule}\nHdr Val\n{rule}\n11111 1.0\n") == []
|
|
# a stray indented line before any row is ignored
|
|
[t] = find_tables(
|
|
f"Table 2.--x\n{rule}\nHdr Val\n{rule}\n stray\n11111 1.0\n{rule}\n"
|
|
)
|
|
assert t.rows == (("11111", "1.0"),)
|
|
# a continuation under a short (two-cell) row appends to its last cell
|
|
[t] = find_tables(
|
|
f"Table 2.--x\n{rule}\nHdr Val\n{rule}\n11111 1.0\n more\n{rule}\n"
|
|
)
|
|
assert t.rows == (("11111", "1.0 more"),)
|
|
|
|
|
|
def test_row_pages_flow_to_rounds():
|
|
t = FrTable(
|
|
"Table 1.--x",
|
|
5,
|
|
("code", "RUC rec", "final"),
|
|
(("11111", "1.0", "1.0"), ("22222", "2.0", "2.5")),
|
|
(5, 6),
|
|
)
|
|
got = table_rounds(
|
|
t,
|
|
family="F",
|
|
item_key="K",
|
|
rule_year=1,
|
|
rule_kind="final",
|
|
codes={"11111", "22222"},
|
|
)
|
|
assert [r.page for r in got] == [5, 6]
|
|
|
|
|
|
def test_mid_table_rule_between_row_groups_keeps_all_rows():
|
|
rule = "-" * 20
|
|
text = (
|
|
f"Table 3.--Groups\n{rule}\nCode Val\n{rule}\n"
|
|
f"11111... 1.0\n{rule}\n\n[[Page 9]]\n22222... 2.0\n"
|
|
f"{rule}\n\\1\\ footnote text.\n"
|
|
)
|
|
[t] = find_tables(text)
|
|
assert [r[0] for r in t.rows] == ["11111", "22222"]
|
|
assert t.row_pages == (0, 9)
|
|
|
|
|
|
def test_closing_rule_before_text_or_eof_still_ends_table():
|
|
rule = "-" * 20
|
|
base = f"Table 3.--G\n{rule}\nCode Val\n{rule}\n11111... 1.0\n{rule}\n"
|
|
for tail in ("", " indented\n", "Plain prose sentence here.\n", rule + "\n"):
|
|
[t] = find_tables(base + tail)
|
|
assert [r[0] for r in t.rows] == ["11111"]
|
|
|
|
|
|
def test_fr_text_path_prefers_attachment(tmp_path, monkeypatch):
|
|
s = Store(":memory:", storage_dir=tmp_path / "st")
|
|
key = s.create(Rule(title="CY 2008 PFS", document_number="07-0001"))
|
|
assert fr_text_path(s, key) is None
|
|
assert fr_text_path(s, "NOSUCH") is None
|
|
dl = tmp_path / "dl"
|
|
dl.mkdir()
|
|
(dl / "07-0001.txt").write_text("fallback")
|
|
monkeypatch.setattr("pfs.rounds.ROOT", tmp_path)
|
|
(tmp_path / "data").mkdir()
|
|
(tmp_path / "data" / "fr_downloads").symlink_to(dl)
|
|
s._con().execute(
|
|
"INSERT INTO fr_anchor_docs (item_key, document_number, html_url, start_page,"
|
|
" end_page, fr_volume) VALUES (?,'07-0001','u',1,2,72)",
|
|
(key,),
|
|
)
|
|
assert fr_text_path(s, key) == tmp_path / "data/fr_downloads/07-0001.txt"
|
|
pdf = tmp_path / "x.pdf"
|
|
pdf.write_bytes(b"%PDF")
|
|
s.attach_file(key, pdf) # a non-txt attachment is skipped
|
|
assert fr_text_path(s, key) == tmp_path / "data/fr_downloads/07-0001.txt"
|
|
(dl / "07-0001.txt").unlink() # anchor row but no file on disk
|
|
assert fr_text_path(s, key) is None
|
|
(dl / "07-0001.txt").write_text("fallback")
|
|
att = tmp_path / "att.txt"
|
|
att.write_text("attached")
|
|
s.attach_file(key, att)
|
|
p = fr_text_path(s, key)
|
|
assert p is not None and p.read_text() == "attached"
|
|
|
|
|
|
def test_rounds_roundtrip():
|
|
con = duckdb.connect(":memory:")
|
|
ensure_tables(con)
|
|
rows = _rounds()
|
|
assert write_rounds(con, "HOME-VISITS", rows) == 3
|
|
assert write_rounds(con, "HOME-VISITS", rows) == 3 # idempotent
|
|
got = read_rounds(con, "HOME-VISITS")
|
|
assert got == sorted(
|
|
rows, key=lambda r: (r.code, r.rule_year, r.rule_kind, r.source)
|
|
)
|
|
assert read_rounds(con, "OTHER") == []
|
|
assert isinstance(got[0], RoundRow)
|
|
|
|
|
|
def test_cy2008_nprm_table_10_parses_with_the_data_row_columns():
|
|
"""4PYQ2WWF (07-3274) Table 10: stacked header cells split finer than
|
|
the data, so the first data row fixes the columns."""
|
|
[t] = find_tables(
|
|
(
|
|
Path(__file__).parent / "fixtures" / "fr_table_cy2008_nprm_t10.txt"
|
|
).read_text()
|
|
)
|
|
assert t.header[3:] == (
|
|
"2007 work RVU",
|
|
"Requested work RVU",
|
|
"RUC REC",
|
|
"CMS proposal (agree/ disagree)",
|
|
"2008 Proposed work RVU \\2\\",
|
|
)
|
|
rows = {
|
|
r.code: r
|
|
for r in table_rounds(
|
|
t,
|
|
family="F",
|
|
item_key="4PYQ2WWF",
|
|
rule_year=2008,
|
|
rule_kind="proposed",
|
|
codes={"99343", "99344"},
|
|
)
|
|
}
|
|
r = rows["99343"]
|
|
assert (r.proposed_work_rvu, r.requested_work_rvu, r.ruc_work_rvu) == (
|
|
2.27,
|
|
2.65,
|
|
2.27,
|
|
)
|
|
assert (r.final_work_rvu, r.decision, r.rationale_quote, r.page) == (
|
|
None,
|
|
"accept",
|
|
"Agree",
|
|
38147,
|
|
)
|
|
assert rows["99344"].proposed_work_rvu == 3.03
|
|
|
|
|
|
# ── paragraph source ─────────────────────────────────────────────────
|
|
|
|
DESC = "( Home or residence visit for the evaluation and management of a patient)"
|
|
P1676 = (
|
|
"The RUC survey time includes pre-service time 3 days before the date of "
|
|
"encounter. These eight CPT codes were reviewed at the October 2021 RUC "
|
|
"meeting. The RUC recommended the survey 25th percentile value for all CPT "
|
|
"codes in the Home or Residence Services code family, except for CPT code "
|
|
"99350, for which the RUC recommended the median value. We proposed the "
|
|
"RUC-recommended work RVU for all eight CPT codes in the Home or Residence "
|
|
"Services CPT code family. "
|
|
f"We proposed a work RVU of 1.00 for CPT code 99341 {DESC}, "
|
|
f"a work RVU of 1.65 for CPT code 99342 {DESC}, "
|
|
f"and a work RVU of 3.60 for CPT code 99350 {DESC}."
|
|
)
|
|
P877 = (
|
|
"In February 2021, the CPT Editorial Panel deleted the nine CPT codes in "
|
|
"the Domiciliary, Rest Home family (CPT codes 99324-99328, and "
|
|
"99334-99337), and one CPT code in the Home Services family (CPT code "
|
|
"99343), to merge these services with the eight remaining home visit "
|
|
"services. The eight remaining home services CPT codes (99341, 99342, "
|
|
"99344, 99345, and 99347-99350) were revised. These eight CPT codes were "
|
|
"reviewed at the October 2021 RUC meeting. The RUC recommended the survey "
|
|
"25th percentile value for all CPT codes in the Home or Residence "
|
|
"Services code family, except for CPT code 99350, for which the RUC "
|
|
"recommended the median value."
|
|
)
|
|
P1685 = (
|
|
"After consideration of the public comments, we are finalizing the work RVU "
|
|
"values for the Home or Residence Services code family as proposed. We are "
|
|
"also finalizing the direct PE inputs for these codes as proposed."
|
|
)
|
|
TITLE23 = "Medicare Program; CY 2023 PFS Final Rule"
|
|
TITLE23P = "Medicare Program; CY 2023 PFS Proposed Rule"
|
|
|
|
|
|
def para(
|
|
p_id, text, *, item="PETM225D", title=TITLE23, date="2022-11-18", kind="final"
|
|
):
|
|
return {
|
|
"item_key": item,
|
|
"p_id": p_id,
|
|
"page": 69608,
|
|
"text": text,
|
|
"title": title,
|
|
"date_published": date,
|
|
"rule_kind": kind,
|
|
"context": ["earlier one", "earlier two"],
|
|
}
|
|
|
|
|
|
def pp(text, kind="proposed"):
|
|
return para(
|
|
1, text, title="CY 2027 PFS Proposed Rule", date="2026-07-16", kind=kind
|
|
)
|
|
|
|
|
|
CODES23 = {"99341", "99342", "99350"}
|
|
|
|
|
|
def test_final_rule_we_proposed_is_proposed():
|
|
vals = dict(
|
|
((c, f), v)
|
|
for c, f, v in para_values(P1676, {"99341", "99350"}, rule_kind="final")
|
|
)
|
|
assert vals[("99341", "proposed_work_rvu")] == 1.00
|
|
assert vals[("99350", "proposed_work_rvu")] == 3.60
|
|
assert not any(f == "final_work_rvu" for (_, f) in vals)
|
|
|
|
|
|
def test_para_values_verbs_patterns_and_filters():
|
|
t = (
|
|
"The RUC-recommended work RVU of 2.27 for CPT code 99343 was reviewed. "
|
|
"We are finalizing a work RVU of 2.53 for CPT code 99343 and "
|
|
"assigning 1.10 work RVUs to HCPCS code G0001, "
|
|
"a work RVU of 9.99 for CPT code 11111 (not ours)."
|
|
)
|
|
assert para_values(t, {"99343", "G0001"}, rule_kind="final") == [
|
|
("99343", "ruc_work_rvu", 2.27),
|
|
("99343", "final_work_rvu", 2.53),
|
|
("G0001", "final_work_rvu", 1.10),
|
|
]
|
|
t2 = "We are assigning 1.10 work RVUs to HCPCS code G0001."
|
|
assert para_values(t2, {"G0001"}, rule_kind="final") == [
|
|
("G0001", "final_work_rvu", 1.10)
|
|
]
|
|
assert (
|
|
para_values("a work RVU of 1.00 for CPT code 99341", {"99341"}, rule_kind="x")
|
|
== []
|
|
)
|
|
twice = "We propose a work RVU of 1.00 for CPT code 99341. " * 2
|
|
assert len(para_values(twice, {"99341"}, rule_kind="proposed")) == 1
|
|
|
|
|
|
def test_we_proposed_the_ruc_recommended_work_rvu_is_proposed_and_ruc():
|
|
t = "We proposed the RUC-recommended work RVU of 1.00 for CPT code 99341."
|
|
got = para_values(t, {"99341"}, rule_kind="final")
|
|
assert sorted(got) == [
|
|
("99341", "proposed_work_rvu", 1.0),
|
|
("99341", "ruc_work_rvu", 1.0),
|
|
]
|
|
t = "We are finalizing the RUC-recommended work RVU of 1.10 for CPT code 99341."
|
|
assert sorted(para_values(t, {"99341"}, rule_kind="final")) == [
|
|
("99341", "final_work_rvu", 1.1),
|
|
("99341", "ruc_work_rvu", 1.1),
|
|
]
|
|
# N1: in a proposed rule the whole sentence is a recap: no value at all
|
|
assert para_values(t, {"99341"}, rule_kind="proposed") == []
|
|
rounds = regex_rounds(pp(t), {"99341"}, family="F")
|
|
assert rounds == []
|
|
# a model "final" equal to that proposal is dropped
|
|
out = {
|
|
"codes": [_d("99341", final_work_rvu=1.0, proposed_work_rvu=1.0)],
|
|
"surveying_societies": [],
|
|
"rationale_quote": "",
|
|
}
|
|
t = "We proposed the RUC-recommended work RVU of 1.00 for CPT code 99341."
|
|
[r] = rows_from_output(out, para(3, t), {"99341"}, family="F", model="m")
|
|
assert (r.proposed_work_rvu, r.final_work_rvu) == (1.0, None)
|
|
|
|
|
|
def test_proposed_rules_make_no_finals_and_negation_gives_none():
|
|
t = "In the CY 2023 PFS final rule, we finalized a work RVU of 1.00 for CPT code 99341."
|
|
assert para_values(t, {"99341"}, rule_kind="proposed") == []
|
|
assert [x.final_work_rvu for x in regex_rounds(pp(t), {"99341"}, family="F")] == []
|
|
for neg in (
|
|
"We are not finalizing the proposed work RVU of 1.00 for CPT code 99341.",
|
|
"We are not adopting a work RVU of 1.00 for CPT code 99341.",
|
|
"We do not finalize a work RVU of 1.00 for CPT code 99341.",
|
|
):
|
|
assert para_values(neg, {"99341"}, rule_kind="final") == [], neg
|
|
t = "We disagree with the RUC recommendation and are proposing a work RVU of 1.50 for CPT code 99341."
|
|
assert para_values(t, {"99341"}, rule_kind="proposed") == [
|
|
("99341", "proposed_work_rvu", 1.5)
|
|
]
|
|
|
|
|
|
def test_paragraph_rules_only_in_final_rules():
|
|
prop = regex_rounds(
|
|
pp("We are proposing a work RVU of 1.10 for CPT code 99341."),
|
|
{"99341"},
|
|
family="F",
|
|
)
|
|
hist = pp(
|
|
"In the CY 2023 PFS final rule, we finalized the work RVU values for the Home or "
|
|
"Residence Services code family as proposed (87 FR 69608).",
|
|
kind="proposed",
|
|
)
|
|
[r] = apply_paragraph_rules(prop, [hist])
|
|
assert (r.proposed_work_rvu, r.final_work_rvu, r.decision) == (1.1, None, "")
|
|
# R22a: a proposal verb sets the RUC value in a proposed rule too ...
|
|
ruc = pp(
|
|
"We are proposing the RUC-recommended work RVU for all eight codes: 99341, 99342.",
|
|
kind="proposed",
|
|
)
|
|
[r] = apply_paragraph_rules(prop, [ruc])
|
|
assert (r.ruc_work_rvu, r.proposed_work_rvu, r.decision) == (1.1, 1.1, "accept")
|
|
# ... and the decision compares ruc with the proposal when there is no final
|
|
diff = replace(prop[0], ruc_work_rvu=1.0)
|
|
[r] = apply_paragraph_rules([diff], [])
|
|
assert (r.final_work_rvu, r.decision) == (None, "refine")
|
|
# ... but a "finalized the RUC-recommended ..." recap in a proposed rule is nothing
|
|
recap = pp(
|
|
"In the CY 2023 final rule we finalized the RUC-recommended work RVU for all eight codes: 99341.",
|
|
kind="proposed",
|
|
)
|
|
[r] = apply_paragraph_rules(prop, [recap])
|
|
assert (r.ruc_work_rvu, r.decision) == (None, "")
|
|
assert ruc_as_proposed(recap["text"], rule_kind="final")
|
|
assert not ruc_as_proposed(recap["text"], rule_kind="proposed")
|
|
assert ruc_as_proposed(ruc["text"], rule_kind="proposed")
|
|
assert not ruc_as_proposed(P1685)
|
|
|
|
|
|
def test_model_accept_without_a_ruc_value_is_dropped():
|
|
t = "We are proposing a work RVU of 1.10 for CPT code 99341."
|
|
out = _reply(_d("99341", proposed_work_rvu=1.1, decision="accept"))
|
|
p = pp(t)
|
|
[r] = rows_from_output(out, p, {"99341"}, family="F", model="m")
|
|
assert (r.proposed_work_rvu, r.ruc_work_rvu, r.decision) == (1.1, None, "")
|
|
|
|
|
|
def test_paragraph_predicates():
|
|
assert family_as_proposed(P1685) and not family_as_proposed(P1676)
|
|
assert ruc_as_proposed(P1676) and not ruc_as_proposed(P1685)
|
|
|
|
|
|
def _regex_1676():
|
|
return regex_rounds(para(1676, P1676), CODES23, family="HOME-VISITS")
|
|
|
|
|
|
def test_regex_rounds_carry_anchor_year_and_percentile():
|
|
rows = {r.code: r for r in _regex_1676()}
|
|
r = rows["99341"]
|
|
assert (r.rule_year, r.rule_kind, r.p_id, r.page) == (2023, "final", 1676, 69608)
|
|
assert (r.source, r.extractor, r.proposed_work_rvu) == ("fr-para", "regex", 1.00)
|
|
assert (r.final_work_rvu, r.ruc_percentile, r.final_p_id) == (None, "25th", 0)
|
|
assert rows["99350"].ruc_percentile == "median"
|
|
fin = regex_rounds(
|
|
para(7, "We are finalizing a work RVU of 2.0 for CPT code 99341."),
|
|
{"99341"},
|
|
family="F",
|
|
)
|
|
assert (fin[0].final_work_rvu, fin[0].final_p_id) == (2.0, 7)
|
|
|
|
|
|
def _applied():
|
|
paras = [para(1676, P1676), para(1685, P1685)]
|
|
return {
|
|
x.code: x for x in apply_paragraph_rules(merge_rounds(_regex_1676()), paras)
|
|
}
|
|
|
|
|
|
def test_family_finalize_as_proposed_propagates_and_cites_the_finalizing_paragraph():
|
|
r = _applied()
|
|
assert (r["99341"].final_work_rvu, r["99350"].final_work_rvu) == (1.00, 3.60)
|
|
assert (r["99341"].final_p_id, r["99341"].rationale_p_id) == (1685, 1685)
|
|
assert r["99341"].rationale_quote.startswith("After consideration")
|
|
assert r["99341"].p_id == 1676 # the proposal stays cited to its paragraph
|
|
|
|
|
|
def test_ruc_recommended_proposal_sets_ruc():
|
|
r = _applied()["99341"]
|
|
assert (r.ruc_work_rvu, r.final_work_rvu, r.decision) == (1.00, 1.00, "accept")
|
|
assert r.proposed_work_rvu == 1.00 and r.ruc_percentile == "25th"
|
|
|
|
|
|
def test_paragraph_rules_leave_other_rules_and_existing_values_alone():
|
|
rows = _regex_1676()
|
|
other = replace(rows[0], item_key="OTHER", code="99341", final_work_rvu=None)
|
|
kept = replace(rows[1], final_work_rvu=7.0, ruc_work_rvu=9.0)
|
|
bare = replace(rows[2], proposed_work_rvu=None)
|
|
out = apply_paragraph_rules(
|
|
[other, kept, bare], [para(1676, P1676), para(1685, P1685)]
|
|
)
|
|
assert out[0].final_work_rvu is None and out[0].ruc_work_rvu is None
|
|
assert (out[1].final_work_rvu, out[1].ruc_work_rvu) == (7.0, 9.0)
|
|
assert out[2].final_work_rvu is None and out[2].decision == ""
|
|
assert out[1].decision == "refine"
|
|
assert apply_paragraph_rules([], [para(1, P1685)]) == []
|
|
|
|
|
|
# ── percentiles (R21) ────────────────────────────────────────────────
|
|
|
|
|
|
def test_percentiles_real_1676_text():
|
|
p = _percentiles(P1676)
|
|
assert p["99350"] == "median"
|
|
assert p == {c: "25th" for c in ("99341", "99342")} | {"99350": "median"}
|
|
|
|
|
|
def test_percentiles_real_877_deleted_codes_get_none():
|
|
p = _percentiles(P877)
|
|
for dead in (
|
|
"99324",
|
|
"99325",
|
|
"99326",
|
|
"99327",
|
|
"99328",
|
|
"99334",
|
|
"99335",
|
|
"99336",
|
|
"99337",
|
|
"99343",
|
|
):
|
|
assert dead not in p, dead
|
|
assert {
|
|
c: p[c] for c in ("99341", "99342", "99344", "99345", "99347", "99348", "99349")
|
|
} == {
|
|
c: "25th"
|
|
for c in ("99341", "99342", "99344", "99345", "99347", "99348", "99349")
|
|
}
|
|
assert p["99350"] == "median"
|
|
|
|
|
|
def test_percentiles_probe_sentences():
|
|
both = "The RUC recommended the 25th percentile for CPT code 99341 and the median for CPT code 99342."
|
|
assert _percentiles(both) == {"99341": "25th", "99342": "median"}
|
|
# a bare exception names no value for 99350
|
|
assert "99350" not in _percentiles(
|
|
"The RUC recommended the 25th percentile for all codes except CPT code 99350."
|
|
)
|
|
# a stray "all" is not collective; 99341 gets nothing
|
|
stray = "After reviewing all survey data, the RUC recommended the median work RVU for CPT code 99350."
|
|
assert _percentiles(stray) == {"99350": "median"}
|
|
# median as a time statistic
|
|
assert (
|
|
_percentiles(
|
|
"The survey median intra-service time for CPT code 99343 was 45 minutes."
|
|
)
|
|
== {}
|
|
)
|
|
# P2: "<code> at the <term>"
|
|
assert _percentiles(
|
|
"The RUC set CPT code 99341 at the 75th percentile, and CPT code 99342 at the median."
|
|
) == {"99341": "75th", "99342": "median"}
|
|
# contradictory statements give nothing
|
|
assert (
|
|
_percentiles(
|
|
"The 25th percentile for CPT code 99341. The median for CPT code 99341."
|
|
)
|
|
== {}
|
|
)
|
|
assert _percentiles("No survey statistic here for CPT code 99341.") == {}
|
|
# a list of codes
|
|
assert _percentiles(
|
|
"The 75th percentile for CPT codes 99341, 99342 and 99344."
|
|
) == {"99341": "75th", "99342": "75th", "99344": "75th"}
|
|
|
|
|
|
# ── guard ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _row(**kw):
|
|
base = replace(
|
|
regex_rounds(para(1676, P1676), CODES23, family="F")[0], extractor="m"
|
|
)
|
|
return replace(base, **kw)
|
|
|
|
|
|
def test_guard_drops_numbers_not_in_text():
|
|
r = guard(_row(code="99341", ruc_work_rvu=1.75, proposed_work_rvu=1.0), P1676)
|
|
assert r.ruc_work_rvu is None and r.proposed_work_rvu == 1.0
|
|
|
|
|
|
def test_guard_keeps_median_only_when_stated():
|
|
assert (
|
|
guard(_row(code="99350", ruc_percentile="median"), P1676).ruc_percentile
|
|
== "median"
|
|
)
|
|
assert guard(_row(code="99350", ruc_percentile="25th"), P1676).ruc_percentile == ""
|
|
assert (
|
|
guard(_row(code="99341", ruc_percentile="25th"), P1676).ruc_percentile == "25th"
|
|
)
|
|
for bad in ("median", "75th"):
|
|
assert guard(_row(code="99341", ruc_percentile=bad), P1676).ruc_percentile == ""
|
|
assert (
|
|
guard(_row(code="99341", ruc_percentile="25th"), "no stats").ruc_percentile
|
|
== ""
|
|
)
|
|
|
|
|
|
def test_guard_quote_societies_comparators():
|
|
quote = "These eight CPT codes were reviewed at the October 2021 RUC meeting."
|
|
r = guard(
|
|
_row(
|
|
rationale_quote=quote,
|
|
rationale_p_id=1676,
|
|
surveying_societies="American Academy of Family Physicians; Made Up Society",
|
|
comparators="99350; 99999",
|
|
),
|
|
P1676 + " The American Academy of Family Physicians surveyed.",
|
|
)
|
|
assert r.rationale_quote == quote and r.rationale_p_id == 1676
|
|
assert r.surveying_societies == "American Academy of Family Physicians"
|
|
assert r.comparators == "99350"
|
|
bad = guard(_row(rationale_quote="invented", rationale_p_id=1676), P1676)
|
|
assert bad.rationale_quote == "" and bad.rationale_p_id == 0
|
|
|
|
|
|
# ── model rows ───────────────────────────────────────────────────────
|
|
|
|
|
|
class FakeAsk:
|
|
model = "qwen-test"
|
|
|
|
def __init__(self, out):
|
|
self.out, self.calls = out, []
|
|
|
|
def __call__(self, system, user, schema):
|
|
self.calls.append((system, user, schema))
|
|
return self.out
|
|
|
|
|
|
def _d(code, **kw):
|
|
base = {
|
|
"code": code,
|
|
"ruc_work_rvu": None,
|
|
"ruc_percentile": "none",
|
|
"survey_intra_min": None,
|
|
"survey_total_min": None,
|
|
"comparators": [],
|
|
"proposed_work_rvu": None,
|
|
"final_work_rvu": None,
|
|
"decision": "none",
|
|
"pe_note": "",
|
|
}
|
|
return {**base, **kw}
|
|
|
|
|
|
def _reply(*codes, quote="", societies=()):
|
|
return {
|
|
"codes": list(codes),
|
|
"surveying_societies": list(societies),
|
|
"rationale_quote": quote,
|
|
}
|
|
|
|
|
|
def test_schema_accepts_a_good_reply():
|
|
from llm.structured import validate
|
|
|
|
assert validate(_reply(_d("99341")), ROUND_SCHEMA) == []
|
|
assert ROUND_PROMPT_VERSION == "rounds-v1"
|
|
|
|
|
|
def test_model_rounds_guards_and_maps_enums():
|
|
out = _reply(
|
|
_d(
|
|
"99341",
|
|
ruc_work_rvu=1.75,
|
|
ruc_percentile="75th",
|
|
proposed_work_rvu=1.0,
|
|
decision="accept",
|
|
comparators=["99350"],
|
|
pe_note="removed SK062",
|
|
),
|
|
_d(
|
|
"99350",
|
|
ruc_percentile="25th",
|
|
final_work_rvu=3.6,
|
|
proposed_work_rvu=3.6,
|
|
decision="none",
|
|
),
|
|
_d("99342", ruc_percentile="other", proposed_work_rvu=1.65),
|
|
_d("99999", ruc_work_rvu=1.0),
|
|
quote="We proposed the RUC-recommended work RVU for all eight CPT codes in the Home or Residence Services CPT code family.",
|
|
societies=["AAFP"],
|
|
)
|
|
ask = FakeAsk(out)
|
|
rows = {
|
|
r.code: r for r in model_rounds(para(1676, P1676), CODES23, ask, family="HV")
|
|
}
|
|
assert set(rows) == CODES23 # 99999 is not a family code
|
|
r = rows["99341"]
|
|
assert r.ruc_work_rvu is None # 1.75 is not in the text
|
|
# the percentile is the paragraph's (25th), not the model's 75th
|
|
assert (r.ruc_percentile, r.proposed_work_rvu, r.decision) == (
|
|
"25th",
|
|
1.0,
|
|
"",
|
|
) # no RUC value: the model's "accept" is dropped
|
|
assert r.comparators == "99350" and r.surveying_societies == ""
|
|
assert r.extractor == "model:qwen-test@rounds-v1"
|
|
assert (r.p_id, r.rationale_p_id, r.source) == (1676, 1676, "fr-para")
|
|
assert rows["99350"].ruc_percentile == "median" and rows["99350"].decision == ""
|
|
assert rows["99350"].final_work_rvu is None # "we proposed" 3.60 is the proposal
|
|
assert rows["99342"].ruc_percentile == "25th"
|
|
assert "99341, 99342, 99350" in ask.calls[0][1]
|
|
|
|
|
|
def test_model_only_values_need_a_verb_in_the_codes_sentence():
|
|
"""R20: the CY2020 99348 "would need to increase from 1.56 to 2.00" case."""
|
|
t = (
|
|
"To maintain the same proportional value, the work RVU for CPT code 99348 "
|
|
"would need to increase from 1.56 to 2.00. We are proposing a work "
|
|
"RVU of 1.25 for CPT code 99349 and we propose 4.5 for CPT code 99350. "
|
|
"In CY2020 we finalized 3.0 for CPT code 99342. We are not finalizing 2.5 for CPT code 99344."
|
|
)
|
|
out = _reply(
|
|
_d("99348", proposed_work_rvu=2.0, final_work_rvu=1.56, decision="accept"),
|
|
_d("99349", proposed_work_rvu=1.25),
|
|
_d("99350", proposed_work_rvu=4.5),
|
|
_d("99342", final_work_rvu=3.0),
|
|
_d("99344", final_work_rvu=2.5, comparators=["G2211"], pe_note="x"),
|
|
)
|
|
codes = {"99348", "99349", "99350", "99342", "99344"}
|
|
p = para(
|
|
9, t, title="CY 2020 PFS Proposed Rule", date="2019-08-14", kind="proposed"
|
|
)
|
|
rows = {r.code: r for r in rows_from_output(out, p, codes, family="F", model="m")}
|
|
assert (
|
|
rows["99348"].proposed_work_rvu is None and rows["99348"].final_work_rvu is None
|
|
)
|
|
assert rows["99348"].decision == "" # a bare label with no value is no round
|
|
assert rows["99349"].proposed_work_rvu == 1.25
|
|
assert rows["99350"].proposed_work_rvu == 4.5 # its sentence says "we propose"
|
|
assert rows["99342"].final_work_rvu is None # a proposed rule's recap
|
|
assert not any(
|
|
has_content(r) for k, r in rows.items() if k in ("99348", "99342", "99344")
|
|
)
|
|
# the same paragraph in a final rule: finals need the finalize verb
|
|
pf = {**p, "rule_kind": "final"}
|
|
rows = {r.code: r for r in rows_from_output(out, pf, codes, family="F", model="m")}
|
|
assert rows["99342"].final_work_rvu == 3.0 and rows["99342"].final_p_id == 9
|
|
assert rows["99344"].final_work_rvu is None # negated
|
|
assert rows["99348"].final_work_rvu is None
|
|
|
|
|
|
def test_has_content_ignores_comparators_and_pe_notes():
|
|
r = _row(
|
|
code="99341",
|
|
proposed_work_rvu=None,
|
|
ruc_work_rvu=None,
|
|
final_work_rvu=None,
|
|
comparators="G2211",
|
|
pe_note="removed SK062",
|
|
decision="",
|
|
)
|
|
assert not has_content(r)
|
|
assert has_content(replace(r, decision="accept"))
|
|
assert has_content(replace(r, survey_intra_min=30.0))
|
|
assert not rounds_mod.has_value(replace(r, decision="accept"))
|
|
|
|
|
|
def test_model_failed_and_empty_replies():
|
|
assert model_rounds(para(1, P1676), CODES23, FakeAsk(None), family="HV") == []
|
|
assert rows_from_output({}, para(1, P1676), CODES23, family="HV", model="m") == []
|
|
[r] = rows_from_output(
|
|
{"codes": [{"code": "99341"}]}, para(1, P1676), CODES23, family="HV", model="m"
|
|
)
|
|
assert r.decision == "" and r.comparators == "" and r.rationale_p_id == 0
|
|
assert r.ruc_percentile == "25th"
|
|
|
|
|
|
def test_model_final_with_both_numbers_gets_a_numeric_decision():
|
|
text = (
|
|
"The RUC recommended a work RVU of 2.27 for CPT code 99326; we are "
|
|
"finalizing a work RVU of 2.63 for CPT code 99326 after refinement."
|
|
)
|
|
out = _reply(
|
|
_d(
|
|
"99326",
|
|
ruc_work_rvu=2.27,
|
|
final_work_rvu=2.63,
|
|
decision="accept",
|
|
comparators=["99326", "99350"],
|
|
pe_note="none (no PE inputs)",
|
|
)
|
|
)
|
|
[r] = rows_from_output(out, para(9, text), {"99326"}, family="HV", model="m")
|
|
assert (r.decision, r.pe_note, r.comparators) == ("refine", "", "")
|
|
out["codes"][0]["pe_note"] = "removed SK062"
|
|
[r] = rows_from_output(out, para(9, text), {"99326"}, family="HV", model="m")
|
|
assert r.pe_note == "removed SK062"
|
|
|
|
|
|
def test_model_prompt_expands_code_ranges():
|
|
ask = FakeAsk(_reply())
|
|
model_rounds(para(877, P877), {"99349", "99350", "99341"}, ask, family="HV")
|
|
user = ask.calls[0][1]
|
|
assert "99347-99350 = 99347 99348 99349 99350" in user and "Code ranges:" in user
|
|
assert "99341, 99349, 99350" in user # range members are named codes
|
|
assert "Code ranges" not in rounds_mod.model_user(para(1, "no ranges"), [])
|
|
|
|
|
|
# ── merge ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _tbl(**kw):
|
|
return replace(
|
|
_row(code="99341", item_key="K"),
|
|
source="fr-table",
|
|
extractor="fr-table",
|
|
p_id=0,
|
|
rationale_p_id=0,
|
|
ruc_p_id=0,
|
|
final_p_id=0,
|
|
page=66361,
|
|
**kw,
|
|
)
|
|
|
|
|
|
def test_merge_prefers_table_over_model():
|
|
table = _tbl(
|
|
final_work_rvu=2.53,
|
|
decision="refine",
|
|
ruc_work_rvu=2.27,
|
|
proposed_work_rvu=None,
|
|
ruc_percentile="",
|
|
)
|
|
model = replace(
|
|
_tbl(
|
|
final_work_rvu=2.60,
|
|
ruc_work_rvu=2.27,
|
|
decision="accept",
|
|
proposed_work_rvu=2.27,
|
|
ruc_percentile="25th",
|
|
pe_note="pe says",
|
|
),
|
|
source="fr-para",
|
|
extractor="model:m@rounds-v1",
|
|
p_id=77,
|
|
page=1,
|
|
rationale_p_id=77,
|
|
rationale_quote="because",
|
|
final_p_id=77,
|
|
)
|
|
[m] = merge_rounds([table], [], [model])
|
|
assert m.final_work_rvu == 2.53 and m.decision == "refine"
|
|
assert m.proposed_work_rvu == 2.27 and m.ruc_percentile == "25th"
|
|
assert (m.p_id, m.rationale_p_id, m.rationale_quote, m.final_p_id) == (
|
|
77,
|
|
77,
|
|
"because",
|
|
77,
|
|
)
|
|
assert m.page == 66361 and m.source == "fr-table"
|
|
assert "conflict: final_work_rvu 2.6 (model:m@rounds-v1)" in m.pe_note
|
|
assert "conflict: decision accept (model:m@rounds-v1)" in m.pe_note
|
|
assert "pe says" in m.pe_note and "conflict: ruc_work_rvu" not in m.pe_note
|
|
assert m.extractor == "fr-table+model:m@rounds-v1"
|
|
|
|
|
|
def test_merge_proposal_keeps_its_own_paragraph_anchor():
|
|
"""A percentile-only row from ¶877 must not become the proposal's citation."""
|
|
pct = replace(
|
|
_row(code="99341", item_key="K"), proposed_work_rvu=None, p_id=877, page=100
|
|
)
|
|
prop = replace(
|
|
_row(code="99341", item_key="K"), proposed_work_rvu=1.0, p_id=889, page=101
|
|
)
|
|
[m] = merge_rounds([pct], [prop])
|
|
assert (m.p_id, m.page, m.proposed_work_rvu) == (889, 101, 1.0)
|
|
# a table row keeps its table page; the anchor still moves to the paragraph
|
|
tbl = _tbl(proposed_work_rvu=None)
|
|
[m] = merge_rounds([tbl], [prop])
|
|
assert (m.p_id, m.page, m.source) == (889, 66361, "fr-table")
|
|
|
|
|
|
def test_merge_keys_and_same_extractor():
|
|
a, b = _tbl(), replace(_tbl(), code="99342")
|
|
out = merge_rounds([a, b], [replace(a, pe_note="x")], [replace(a, item_key="Z")])
|
|
assert [(r.code, r.item_key) for r in out] == [
|
|
("99341", "K"),
|
|
("99342", "K"),
|
|
("99341", "Z"),
|
|
]
|
|
assert out[0].extractor == "fr-table" and out[0].pe_note == "x"
|
|
assert merge_rounds() == []
|
|
|
|
|
|
# ── store-backed: candidates, family run ─────────────────────────────
|
|
|
|
|
|
def _store(tmp_path, with_text=True, dup=False):
|
|
s = Store(":memory:", storage_dir=tmp_path / "st")
|
|
key = s.create(
|
|
Rule(title=TITLE23, document_number="22-0001", date_published="2022-11-18")
|
|
)
|
|
con = s._con()
|
|
anchors = [
|
|
(key, 1, 10, 1, "Unrelated opening paragraph."),
|
|
(key, 2, 10, 2, "Context two."),
|
|
(key, 1676, 69608, 3, P1676),
|
|
(key, 1685, 69609, 4, P1685),
|
|
(key, 1690, 69609, 5, "Code 99341 is named in passing here."),
|
|
]
|
|
keys = [key]
|
|
if dup: # a second bib item for the same FR document
|
|
k2 = s.create(
|
|
Rule(title=TITLE23, document_number="22-0001", date_published="2022-11-19")
|
|
)
|
|
keys.append(k2)
|
|
anchors += [(k2, 1676, 69608, 3, P1676)]
|
|
# a correction notice quoting the rule
|
|
kc = s.create(
|
|
Rule(
|
|
title="CY 2023 PFS Final Rule; Correction",
|
|
document_number="22-9",
|
|
date_published="2023-01-01",
|
|
)
|
|
)
|
|
anchors.append((kc, 5, 1, 1, P1676))
|
|
con.executemany(
|
|
"INSERT INTO fr_anchors (item_key, p_id, page, ordinal, text) VALUES (?,?,?,?,?)",
|
|
anchors,
|
|
)
|
|
for k in keys:
|
|
con.execute(
|
|
"INSERT INTO fr_anchor_docs (item_key, document_number, html_url, start_page, end_page, fr_volume)"
|
|
" VALUES (?,'22-0001','u',1,2,87)",
|
|
(k,),
|
|
)
|
|
con.commit()
|
|
if with_text:
|
|
txt = tmp_path / "rule.txt"
|
|
txt.write_text(FIXTURE.replace("99343", "99341").replace("2008", "2023"))
|
|
s.attach_file(key, txt)
|
|
return s, key
|
|
|
|
|
|
FAM = SimpleNamespace(
|
|
key="HOME-VISITS",
|
|
codes=("99341", "99342", "99350"),
|
|
synonyms=("Home or Residence Services",),
|
|
)
|
|
|
|
|
|
def test_rule_items_dedupe_documents_and_drop_corrections(tmp_path):
|
|
s, key = _store(tmp_path, dup=True)
|
|
items = rule_items(s, [])
|
|
assert [i["item_key"] for i in items] == [key] # dup and correction gone
|
|
assert items[0]["kind"] == "final"
|
|
# PFS-titled items with anchors are included even when no anchor names a code
|
|
assert [i["item_key"] for i in rule_items(s, [])] == [key]
|
|
|
|
|
|
def test_candidate_paragraphs_filter_and_context(tmp_path):
|
|
s, key = _store(tmp_path, dup=True)
|
|
assert candidate_paragraphs(s, (), ()) == []
|
|
got = candidate_paragraphs(s, FAM.codes, FAM.synonyms)
|
|
assert [(p["item_key"], p["p_id"]) for p in got] == [(key, 1676), (key, 1685)]
|
|
assert got[0]["context"] == ["Unrelated opening paragraph.", "Context two."]
|
|
assert got[0]["rule_kind"] == "final" and got[0]["title"] == TITLE23
|
|
assert [p["p_id"] for p in candidate_paragraphs(s, (), FAM.synonyms)] == [
|
|
1676,
|
|
1685,
|
|
]
|
|
assert [p["p_id"] for p in candidate_paragraphs(s, FAM.codes, ())] == [1676]
|
|
|
|
|
|
def _good_reply():
|
|
return _reply(
|
|
_d("99341", ruc_percentile="25th", proposed_work_rvu=1.0),
|
|
_d("99350", ruc_percentile="median", proposed_work_rvu=3.6),
|
|
)
|
|
|
|
|
|
def test_family_rounds_end_to_end_and_resume(tmp_path):
|
|
s, key = _store(tmp_path, with_text=False)
|
|
ask = FakeAsk(_good_reply())
|
|
log = RunLog(tmp_path / "ledger.jsonl")
|
|
rows, counts = family_rounds(None, s, FAM, ask=ask, log=log)
|
|
assert counts == {"done": 1, "skipped": 0, "failed": 0, "failed_pending": 0}
|
|
by = {r.code: r for r in rows}
|
|
r = by["99341"]
|
|
assert (r.proposed_work_rvu, r.final_work_rvu, r.ruc_work_rvu) == (1.0, 1.0, 1.0)
|
|
assert (r.ruc_percentile, r.decision, r.rule_year) == ("25th", "accept", 2023)
|
|
assert (r.p_id, r.final_p_id) == (1676, 1685)
|
|
assert by["99350"].ruc_percentile == "median"
|
|
assert len(ask.calls) == 1
|
|
again, counts = family_rounds(None, s, FAM, ask=ask, log=log)
|
|
assert counts["skipped"] == 1 and counts["done"] == 0 and len(ask.calls) == 1
|
|
assert again == rows
|
|
bad = FakeAsk(None)
|
|
log2 = RunLog(tmp_path / "l2.jsonl")
|
|
rows2, c2 = family_rounds(None, s, FAM, ask=bad, log=log2)
|
|
assert c2["failed"] == 1 and {x.extractor for x in rows2} == {"regex"}
|
|
_, c3 = family_rounds(None, s, FAM, ask=bad, log=log2)
|
|
assert c3["failed_pending"] == 1 and len(bad.calls) == 1
|
|
_, c4 = family_rounds(None, s, FAM, ask=bad, log=log2, retry_failed=True)
|
|
assert c4["failed"] == 1 and len(bad.calls) == 2
|
|
|
|
|
|
def test_no_llm_and_tables_only_replay_the_finished_ledger(tmp_path):
|
|
s, key = _store(tmp_path, with_text=False)
|
|
ask = FakeAsk(_good_reply())
|
|
log = RunLog(tmp_path / "ledger.jsonl")
|
|
full, _ = family_rounds(None, s, FAM, ask=ask, log=log)
|
|
assert any("model:" in r.extractor for r in full)
|
|
for kw in ({}, {"tables_only": True}):
|
|
replay, counts = family_rounds(
|
|
None, s, FAM, ask=None, log=log, replay_model="qwen-test", **kw
|
|
)
|
|
assert replay == full and counts["done"] == 0
|
|
assert len(ask.calls) == 1
|
|
# a different model has no recorded outputs: regex rows only, nothing deleted that regex made
|
|
other, _ = family_rounds(None, s, FAM, ask=None, log=log, replay_model="other")
|
|
assert {r.extractor for r in other} == {"regex"} or all(
|
|
"model:" not in r.extractor for r in other
|
|
)
|
|
# no model name at all
|
|
plain, _ = family_rounds(None, s, FAM, ask=None, log=None)
|
|
assert all("model:" not in r.extractor for r in plain)
|
|
|
|
|
|
def test_family_rounds_default_ledger(tmp_path, monkeypatch):
|
|
s, _ = _store(tmp_path, with_text=False)
|
|
monkeypatch.setattr(
|
|
rounds_mod, "ledger_path", lambda t, f: tmp_path / f"{t}-{f}.jsonl"
|
|
)
|
|
ask = FakeAsk(_good_reply())
|
|
family_rounds(None, s, FAM, ask=ask, log=None)
|
|
assert (tmp_path / "rounds-HOME-VISITS.jsonl").exists()
|
|
|
|
|
|
def test_family_rounds_reads_the_fr_table_and_merges_it_first(tmp_path):
|
|
s, key = _store(tmp_path)
|
|
rows, counts = family_rounds(None, s, FAM, ask=None, log=None)
|
|
[r] = [r for r in rows if r.code == "99341"]
|
|
assert r.source == "fr-table" and (r.rule_year, r.rule_kind) == (2023, "final")
|
|
assert r.final_work_rvu == 2.53 and r.p_id == 1676 # table + paragraph anchor
|
|
s2, _ = _store(tmp_path / "x", with_text=False)
|
|
assert all(
|
|
r.source != "fr-table"
|
|
for r in family_rounds(None, s2, FAM, ask=None, log=None)[0]
|
|
)
|
|
|
|
|
|
def test_rounds_inventory(tmp_path):
|
|
s, _ = _store(tmp_path)
|
|
paras, table = rounds_inventory(s, FAM)
|
|
assert [p["p_id"] for p in paras] == [1676]
|
|
assert any(r.code == "99341" for r in table)
|
|
|
|
|
|
def test_final_p_id_roundtrips_and_old_tables_gain_the_column():
|
|
con = duckdb.connect(":memory:")
|
|
con.execute("CREATE SCHEMA pfs")
|
|
con.execute(
|
|
"CREATE TABLE pfs.valuation_round (family VARCHAR, code VARCHAR, rule_year INTEGER, item_key VARCHAR,"
|
|
" rule_kind VARCHAR, p_id INTEGER, page INTEGER, source VARCHAR, table_title VARCHAR,"
|
|
" ruc_work_rvu DOUBLE, ruc_percentile VARCHAR, survey_intra_min DOUBLE, 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)"
|
|
)
|
|
ensure_tables(con)
|
|
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, 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) ────────────────────────────────────────
|
|
|
|
|
|
def test_numbers_decide_the_label_in_proposed_and_final_rules():
|
|
"""N2: ruc 2.27, proposed 2.50, model "accept" -> refine."""
|
|
t = "The RUC recommended a work RVU of 2.27 for CPT code 99343. We are proposing a work RVU of 2.50 for CPT code 99343."
|
|
out = _reply(
|
|
_d("99343", ruc_work_rvu=2.27, proposed_work_rvu=2.5, decision="accept")
|
|
)
|
|
[m] = rows_from_output(out, pp(t), {"99343"}, family="F", model="m")
|
|
assert (m.ruc_work_rvu, m.proposed_work_rvu, m.decision) == (2.27, 2.5, "refine")
|
|
[r] = apply_paragraph_rules([replace(m, decision="accept")], [])
|
|
assert r.decision == "refine"
|
|
# a final rule: the final arrives from family_as_proposed in another paragraph
|
|
row = replace(
|
|
regex_rounds(
|
|
para(
|
|
1,
|
|
"We proposed the RUC-recommended work RVU of 2.27 for CPT code 99343.",
|
|
),
|
|
{"99343"},
|
|
family="F",
|
|
)[0],
|
|
decision="refine",
|
|
)
|
|
assert row.ruc_work_rvu == 2.27 and row.final_work_rvu is None
|
|
[r] = apply_paragraph_rules([row], [para(2, P1685)])
|
|
assert (r.final_work_rvu, r.decision) == (2.27, "accept")
|
|
# a table's own label stands; a table row without one is derived
|
|
tb = _tbl(ruc_work_rvu=2.27, final_work_rvu=2.53, decision="accept")
|
|
assert apply_paragraph_rules([tb], [])[0].decision == "accept"
|
|
assert apply_paragraph_rules([replace(tb, decision="")], [])[0].decision == "refine"
|
|
|
|
|
|
def test_rationale_comes_from_a_paragraph_with_a_value_or_decision():
|
|
"""R26a: ¶294-style misvaluation boilerplate is not the rationale."""
|
|
boiler = replace(
|
|
_row(
|
|
code="99341",
|
|
item_key="K",
|
|
proposed_work_rvu=None,
|
|
ruc_work_rvu=None,
|
|
final_work_rvu=None,
|
|
ruc_percentile="",
|
|
decision="",
|
|
),
|
|
p_id=294,
|
|
rationale_p_id=294,
|
|
rationale_quote="misvalued codes boilerplate",
|
|
)
|
|
valued = replace(
|
|
_row(code="99341", item_key="K", proposed_work_rvu=1.0, decision=""),
|
|
p_id=878,
|
|
rationale_p_id=0,
|
|
rationale_quote="",
|
|
)
|
|
[m] = merge_rounds([boiler], [valued])
|
|
assert (m.rationale_p_id, m.rationale_quote) == (0, "")
|
|
[m] = merge_rounds([valued], [boiler]) # boilerplate arriving later is skipped too
|
|
assert (m.rationale_p_id, m.rationale_quote) == (0, "")
|
|
assert m.p_id == 878
|
|
quoted = replace(
|
|
valued, rationale_p_id=878, rationale_quote="we propose it", extractor="x"
|
|
)
|
|
[m] = merge_rounds([boiler], [quoted])
|
|
assert (m.rationale_p_id, m.rationale_quote) == (878, "we propose it")
|
|
|
|
|
|
def test_ruc_p_id_follows_the_ruc_statement():
|
|
"""R26b: percentile in ¶877, proposal in ¶878 -> p_id 878, ruc_p_id 877."""
|
|
pct = regex_rounds(
|
|
para(877, P877, item="K", kind="proposed", title=TITLE23P, date="2022-07-29"),
|
|
{"99341"},
|
|
family="F",
|
|
)
|
|
prop = regex_rounds(
|
|
para(
|
|
878,
|
|
"We are proposing the RUC-recommended work RVU of 1.00 for CPT code 99341.",
|
|
item="K",
|
|
kind="proposed",
|
|
title=TITLE23P,
|
|
date="2022-07-29",
|
|
),
|
|
{"99341"},
|
|
family="F",
|
|
)
|
|
assert pct[0].ruc_p_id == 877 and prop[0].ruc_p_id == 878
|
|
[m] = merge_rounds(pct, [replace(prop[0], ruc_work_rvu=None, ruc_p_id=0)])
|
|
assert (m.p_id, m.ruc_p_id, m.ruc_percentile, m.proposed_work_rvu) == (
|
|
878,
|
|
877,
|
|
"25th",
|
|
1.0,
|
|
)
|
|
# ruc_as_proposed anchors the RUC value to its paragraph when nothing else does
|
|
bare = replace(prop[0], ruc_work_rvu=None, ruc_p_id=0)
|
|
ruc_para = para(
|
|
900,
|
|
"We are proposing the RUC-recommended work RVU for all eight codes: 99341.",
|
|
item="K",
|
|
kind="proposed",
|
|
title=TITLE23P,
|
|
date="2022-07-29",
|
|
)
|
|
[r] = apply_paragraph_rules([bare], [ruc_para])
|
|
assert (r.ruc_work_rvu, r.ruc_p_id) == (1.0, 900)
|
|
# a table's RUC value is cited by its page: a paragraph does not take over ruc_p_id
|
|
tbl = _tbl(ruc_work_rvu=2.27)
|
|
[m] = merge_rounds([tbl], [replace(pct[0], item_key="K", code="99341")])
|
|
assert m.ruc_p_id == 0 and m.ruc_percentile == "25th"
|
|
# model rows carry it for survey data / percentile
|
|
out = _reply(_d("99341", survey_intra_min=30.0, proposed_work_rvu=1.0))
|
|
t = "The RUC survey intra-service time was 30 minutes for CPT code 99341. We proposed a work RVU of 1.0 for CPT code 99341."
|
|
[r] = rows_from_output(out, para(5, t), {"99341"}, family="F", model="m")
|
|
assert r.ruc_p_id == 5
|
|
out = _reply(_d("99341", proposed_work_rvu=1.0))
|
|
t = "We proposed a work RVU of 1.0 for CPT code 99341."
|
|
[r] = rows_from_output(out, para(5, t), {"99341"}, family="F", model="m")
|
|
assert r.ruc_p_id == 0
|
|
|
|
|
|
def test_recaps_of_another_years_rule_yield_nothing_in_both_kinds():
|
|
for kind in ("proposed", "final"):
|
|
for t in (
|
|
"In the CY 2023 PFS final rule, we finalized a work RVU of 1.00 for CPT code 99341.",
|
|
"In the CY 2023 PFS proposed rule, we proposed a work RVU of 1.00 for CPT code 99341.",
|
|
"(CY 2023 PFS final rule) we proposed a work RVU of 1.00 for CPT code 99341.",
|
|
):
|
|
assert para_values(t, {"99341"}, rule_kind=kind, rule_year=2027) == [], (
|
|
kind,
|
|
t,
|
|
)
|
|
# the rule's own year is not a recap
|
|
t = "In the CY 2027 PFS proposed rule, we proposed a work RVU of 1.00 for CPT code 99341."
|
|
assert para_values(t, {"99341"}, rule_kind="proposed", rule_year=2027) == [
|
|
("99341", "proposed_work_rvu", 1.0)
|
|
]
|
|
# the model path too
|
|
out = _reply(_d("99341", proposed_work_rvu=1.0, ruc_work_rvu=1.0))
|
|
t = "In the CY 2023 PFS proposed rule, we proposed a work RVU of 1.00 for CPT code 99341."
|
|
[r] = rows_from_output(out, pp(t), {"99341"}, family="F", model="m")
|
|
assert r.proposed_work_rvu is None
|
|
|
|
|
|
def test_disagree_with_ruc_values_and_maintain_goes_to_the_main_verb():
|
|
t = (
|
|
"We disagree with the AMA RUC-recommended values for these services and are "
|
|
"maintaining the current work RVUs for the related predecessor codes of 0.17 RVUs for CPT "
|
|
"code 90460 and 0.15 work RVUs for CPT code 90461."
|
|
)
|
|
assert para_values(t, {"90461", "90460"}, rule_kind="final") == [
|
|
("90461", "final_work_rvu", 0.15)
|
|
]
|
|
assert para_values(t, {"90461"}, rule_kind="proposed") == [
|
|
("90461", "proposed_work_rvu", 0.15)
|
|
]
|
|
t2 = "We disagree with the RUC-recommended values and are finalizing 0.15 work RVUs for CPT code 90461."
|
|
assert para_values(t2, {"90461"}, rule_kind="final") == [
|
|
("90461", "final_work_rvu", 0.15)
|
|
]
|
|
assert para_values(
|
|
t2.replace("finalizing", "proposing"), {"90461"}, rule_kind="final"
|
|
) == [("90461", "proposed_work_rvu", 0.15)]
|
|
|
|
|
|
def test_printed_page_markers_are_not_codes():
|
|
p = _percentiles(P877 + " ( printed page 46000)")
|
|
assert "46000" not in p
|
|
assert rounds_mod._named(
|
|
"see ( printed page 46000) 99341", rounds_mod.code_pattern(["99341"])
|
|
) == ("99341",)
|
|
sent = rounds_mod._sentences(
|
|
"We proposed a ( printed page 123) work RVU of 1.00 for CPT code 99341."
|
|
)
|
|
assert "printed" not in sent[0]
|
|
|
|
|
|
def test_apply_rules_with_no_rows_and_a_ruc_paragraph():
|
|
ruc = para(
|
|
900,
|
|
"We are proposing the RUC-recommended work RVU for all eight codes: 99341.",
|
|
kind="proposed",
|
|
)
|
|
assert apply_paragraph_rules([], [ruc]) == []
|
|
|
|
|
|
def test_hypothetical_sentences_yield_no_regex_values():
|
|
for k, t in (
|
|
(
|
|
"final",
|
|
"If we were to finalize a work RVU of 2.00 for CPT code 99348, payment would rise.",
|
|
),
|
|
(
|
|
"proposed",
|
|
"We are proposing that, if the RUC recommends it, we would assign a work RVU of 2.00 for CPT code 99348.",
|
|
),
|
|
(
|
|
"proposed",
|
|
"Were we to propose a work RVU of 2.00 for CPT code 99348, it would be consistent.",
|
|
),
|
|
):
|
|
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) == {}
|