Files
stack/tests/pfs/test_curated.py
kert f9cfdef0b2 fix(pfs): grounded guideline paraphrases (guideline-v2), CPT Changes-dated deletions, invented test fixtures, stance-v6 text rules, MOD1/MOD2 aliases, multi-docket golden eval
The grounding guard checks each paraphrase against its numbered sentence
(content-word coverage, unseen words, code ranges per action clause, NFKC copy
guard). Deletions cite CPT Changes 2023 pages; the Panel row is built from the
entry's code lists. Pair phrase: drop the bare 'home and residence'. stance-v6
is not shipped (golden recall 11/27 vs v5 14/27); v5 stays the default.
2026-11-12 17:53:14 -05:00

242 lines
8.4 KiB
Python

"""pfs.curated — the hand-curated, cited family registry."""
from __future__ import annotations
import pytest
from pfs.codetables import FamilyRow
from pfs.curated import apply_curated, curated_rows, load_curated
def _write(tmp_path, body):
p = tmp_path / "c.yaml"
p.write_text(body)
return p
def test_packaged_home_visits_loads():
fam = load_curated()["HOME-VISITS"]
assert len(fam.codes) == 20 and "99343" in fam.codes and "G2211" not in fam.codes
assert {(link.kind, link.target) for link in fam.links} >= {
("add-on", "G2211"),
("modifier", "B1"),
("modifier", "B2"),
("prolonged-add-on", "G0318"),
}
m = next(m for m in fam.members if m.code == "99343")
assert (m.role, m.until, m.item_key, m.p_id) == (
"predecessor",
2022,
"PETM225D",
1672,
)
g = next(link for link in fam.links if link.target == "G0318")
assert (g.item_key, g.p_id, g.since) == ("PETM225D", 1694, 2023)
old = next(m for m in fam.members if m.code == "99339")
assert (old.item_key, old.p_id, old.rvu_year) == ("", 0, 2022)
def test_page_cite_and_key_normalised(tmp_path):
p = _write(
tmp_path,
"fam-x:\n name: X\n synonyms: [' Foo Bar ']\n members:\n"
" - {code: g0318, role: member, cite: {item_key: K, page: 7}}\n",
)
fam = load_curated(p)["FAM-X"]
assert fam.synonyms == ("foo bar",)
m = fam.members[0]
assert (m.code, m.page, m.p_id, m.since, m.until) == ("G0318", 7, 0, None, None)
assert fam.links == ()
@pytest.mark.parametrize(
"member, needle",
[
("{code: '99341', role: base}", "missing cite"),
("{code: '99341', role: base, cite: {item_key: K}}", "ambiguous cite"),
(
"{code: '99341', role: base, cite: {item_key: K, p_id: 1, page: 2}}",
"ambiguous",
),
("{code: '99341', role: bogus, cite: {rvu_last_year: 2022}}", "unknown role"),
("{code: 'ABC', role: base, cite: {rvu_last_year: 2022}}", "not a CPT"),
],
)
def test_bad_member_rejected(tmp_path, member, needle):
p = _write(tmp_path, f"F:\n name: F\n members:\n - {member}\n")
with pytest.raises(ValueError, match=needle) as e:
load_curated(p)
assert "F member" in str(e.value)
def test_bad_link_rejected(tmp_path):
p = _write(
tmp_path,
"F:\n name: F\n links:\n - {kind: nope, target: B1, cite: {rvu_last_year: 1}}\n",
)
with pytest.raises(ValueError, match="F link .B1.: unknown kind"):
load_curated(p)
def test_empty_file_is_empty(tmp_path):
assert load_curated(_write(tmp_path, "")) == {}
def test_curated_rows_carry_cite():
rows = curated_rows(load_curated())
r = next(r for r in rows if r.code == "99343")
assert (r.key, r.role, r.until, r.item_key, r.p_id, r.note) == (
"HOME-VISITS",
"predecessor",
2022,
"PETM225D",
1672,
"",
)
assert len(rows) == 20
def test_apply_curated_claims_codes():
derived = [
FamilyRow(
"NEW-PATIENT",
"New Patient",
"99341",
"base",
2015,
None,
"",
0,
"E/M > Home",
),
FamilyRow(
"NEW-PATIENT", "New Patient", "99202", "base", 2015, None, "", 0, "E/M"
),
]
rows = apply_curated(derived, load_curated())
assert [r.key for r in rows if r.code == "99341"] == ["HOME-VISITS"]
assert [r.key for r in rows if r.code == "99202"] == ["NEW-PATIENT"]
assert rows == sorted(rows, key=lambda r: (r.key, r.code))
_OK = "cite: {item_key: K, p_id: 1}"
@pytest.mark.parametrize(
"body, needle",
[
("F:\n members: []\n", "F: missing name"),
("F: [1]\n", "F: family must be a mapping"),
("F:\n name: F\n members: 3\n", "F: members must be a list"),
("F:\n name: F\n members: [oops]\n", "F members entry 'oops'"),
("F:\n name: F\n links: [oops]\n", "F links entry 'oops'"),
(
"F:\n name: F\n members:\n - {code: '99341', role: base, cite: {item_key: K, p_id: x}}\n",
"F member .99341.: p_id must be an integer",
),
(
"F:\n name: F\n members:\n - {code: '99341', role: base, cite: {item_key: K, page: x}}\n",
"page must be an integer",
),
(
"F:\n name: F\n members:\n - {code: '99341', role: base, cite: {rvu_last_year: x}}\n",
"rvu_last_year must be an integer",
),
(
f"F:\n name: F\n members:\n - {{code: '99341', role: base, since: abc, {_OK}}}\n",
"F member .99341.: since must be an integer",
),
(
f"F:\n name: F\n members:\n - {{code: '99341', role: base, until: true, {_OK}}}\n",
"until must be an integer",
),
(
f"F:\n name: F\n members:\n - {{code: '99341', role: base, {_OK}}}\n - {{code: '99341', role: base, {_OK}}}\n",
"F member .99341.: duplicate member code",
),
(
"F:\n name: F\n members:\n - {code: '99341', role: base, cite: oops}\n",
"F member .99341.: cite must be a mapping",
),
(
f"F:\n name: F\n links:\n - {{kind: add-on, target: G1, since: x, {_OK}}}\n",
"F link .G1.: since must be an integer",
),
(
f"A:\n name: A\n members:\n - {{code: '99341', role: base, {_OK}}}\n"
f"B:\n name: B\n members:\n - {{code: '99341', role: base, {_OK}}}\n",
"B member 99341: already a member of A",
),
],
)
def test_malformed_entries_named(tmp_path, body, needle):
with pytest.raises(ValueError, match=needle):
load_curated(_write(tmp_path, body))
def test_pair_phrases_absent_present_and_normalised(tmp_path):
absent = load_curated(_write(tmp_path, "F:\n name: F\n"))["F"]
assert absent.pair_phrases == ()
p = _write(tmp_path, "F:\n name: F\n pair_phrases: [' Home Visit ', '']\n")
assert load_curated(p)["F"].pair_phrases == ("home visit",)
@pytest.mark.parametrize("value", ["home visit", "[1, 2]", "{a: b}"])
def test_pair_phrases_wrong_type_rejected(tmp_path, value):
p = _write(tmp_path, f"F:\n name: F\n pair_phrases: {value}\n")
with pytest.raises(ValueError, match="F: pair_phrases must be a list of strings"):
load_curated(p)
def test_home_visits_pair_phrases_are_visit_naming():
fam = load_curated()["HOME-VISITS"]
assert (
"home visits" not in fam.pair_phrases and "domiciliary" not in fam.pair_phrases
)
assert "house call" in fam.pair_phrases
def test_link_aliases_and_their_cite():
fam = load_curated()["HOME-VISITS"]
g = next(link for link in fam.links if link.target == "G0318")
assert (g.aliases, g.alias_item_key, g.alias_p_id) == (("GXXX3",), "MZ24MX5S", 880)
b = next(link for link in fam.links if link.target == "B1")
assert (b.aliases, b.alias_item_key, b.alias_p_id) == (("MOD1",), "5ITGVDJV", 521)
b2 = next(link for link in fam.links if link.target == "B2")
assert (b2.aliases, b2.alias_item_key, b2.alias_p_id) == (
("MOD2",),
"5ITGVDJV",
537,
)
# the bare phrase admitted nursing-facility G2211 letters (R75); e/m stays
assert "home and residence e/m" in fam.pair_phrases
assert "home and residence" not in fam.pair_phrases
def test_alias_parsing_and_errors(tmp_path):
head = "F:\n name: F\n links:\n - {kind: add-on, target: G1, since: 2023, "
ok = (
head
+ "aliases: [' gxxx3 ', ''], alias_cite: {item_key: K, p_id: 5}, cite: {item_key: K, p_id: 1}}\n"
)
link = load_curated(_write(tmp_path, ok))["F"].links[0]
assert link.aliases == ("GXXX3",) and link.alias_p_id == 5
for bad, needle in [
(
head + "aliases: GXXX3, cite: {item_key: K, p_id: 1}}\n",
"aliases must be a list",
),
(
head + "aliases: [1], cite: {item_key: K, p_id: 1}}\n",
"aliases must be a list",
),
(head + "aliases: [X], cite: {item_key: K, p_id: 1}}\n", "alias_cite"),
(
head
+ "aliases: [X], alias_cite: {item_key: K, page: 1}, cite: {item_key: K, p_id: 1}}\n",
"alias_cite",
),
]:
with pytest.raises(ValueError, match=needle):
load_curated(_write(tmp_path, bad))