fix(pfs): since = earliest evidence; derived families get no synonyms (refs F3 F5)
`since` used to take cpt_since unconditionally, so a code first ingested
in the 2019 CPT edition read `since=2019` even when the RVU/FR record
shows it created years earlier (99490, CY2015). Now `since =
min(cpt_since, appeared-event year)` when an appeared event exists —
cpt_since is only the earliest *ingested* edition, not proof the code
didn't exist before that.
Derived (non-hand) families used to get `(key.lower(), name.lower())`
as synonyms, turning ~1,334 one/two-word CPT heading names ("Repair",
"Excision") into chat triggers and costing detect_codes 0.50s/call at
8.5k families. Derived families now get an empty synonym tuple; hand
families are unaffected. Matching a derived family by name is #699's
job (a precompiled alternation).
This commit is contained in:
@@ -377,7 +377,16 @@ def _group_rows(
|
|||||||
else:
|
else:
|
||||||
role = "base"
|
role = "base"
|
||||||
if c in cpt_since:
|
if c in cpt_since:
|
||||||
since = cpt_since[c]
|
appeared = next((ev.year for ev in evs if ev.kind == "appeared"), None)
|
||||||
|
# C13: cpt_since is only the earliest edition *on hand* — it
|
||||||
|
# is not evidence the code didn't exist earlier. When an
|
||||||
|
# RVU/FR "appeared" event is older, that's the real origin
|
||||||
|
# (e.g. 99490 created CY2015, first ingested CPT edition
|
||||||
|
# 2019); when no such event exists, cpt_since is the only
|
||||||
|
# evidence there is.
|
||||||
|
since = (
|
||||||
|
min(cpt_since[c], appeared) if appeared is not None else cpt_since[c]
|
||||||
|
)
|
||||||
until = (
|
until = (
|
||||||
None
|
None
|
||||||
if newest_cpt_year is not None and cpt_last_year[c] >= newest_cpt_year
|
if newest_cpt_year is not None and cpt_last_year[c] >= newest_cpt_year
|
||||||
@@ -752,11 +761,14 @@ def load_families(con: Any) -> dict[str, Family]:
|
|||||||
for r in rows:
|
for r in rows:
|
||||||
fam = out.get(r.key)
|
fam = out.get(r.key)
|
||||||
codes = (*(fam.codes if fam else ()), r.code)
|
codes = (*(fam.codes if fam else ()), r.code)
|
||||||
syn = (
|
# Ruling F3: a derived (non-hand) family gets no synonyms at all
|
||||||
HAND_FAMILIES[r.key].synonyms
|
# — one/two-word CPT heading names ("Repair", "Excision") would
|
||||||
if r.key in HAND_FAMILIES
|
# otherwise become chat triggers across ~1,334 derived families,
|
||||||
else (r.key.lower(), r.name.lower())
|
# and scanning every family's synonyms against every chunk costs
|
||||||
)
|
# 0.50s/call at ~8.5k families. Matching a derived family by name
|
||||||
|
# is #699's job (a precompiled alternation), not this one. Hand
|
||||||
|
# families keep their curated synonyms.
|
||||||
|
syn = HAND_FAMILIES[r.key].synonyms if r.key in HAND_FAMILIES else ()
|
||||||
out[r.key] = Family(r.key, r.name, tuple(sorted(set(codes))), syn)
|
out[r.key] = Family(r.key, r.name, tuple(sorted(set(codes))), syn)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -1025,6 +1025,63 @@ class TestCptPresenceC12:
|
|||||||
assert by_code["90001"].until is None
|
assert by_code["90001"].until is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSinceEarliestEvidenceC13:
|
||||||
|
"""F5 / Ruling C13: ``since`` = min(cpt_since, appeared-event year)
|
||||||
|
when an ``appeared`` event exists — cpt_since is only the earliest
|
||||||
|
*ingested* edition, not evidence the code didn't exist earlier."""
|
||||||
|
|
||||||
|
def test_since_is_min_of_cpt_presence_and_appeared_event(self):
|
||||||
|
# 99490 was created CY2015 but the oldest CPT edition on hand is
|
||||||
|
# 2019 — since must read 2015, not 2019.
|
||||||
|
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
|
||||||
|
cpt_codes = [_cpt_code("99490", "sec_a", year=2024)]
|
||||||
|
cpt_presence = {"99490": (2019, 2021, 2022, 2024)}
|
||||||
|
events = {"99490": [_ev("99490", 2015, "appeared")]}
|
||||||
|
rows = derive_families(
|
||||||
|
{},
|
||||||
|
events,
|
||||||
|
{},
|
||||||
|
cpt_codes=cpt_codes,
|
||||||
|
cpt_sections=sections,
|
||||||
|
cpt_presence=cpt_presence,
|
||||||
|
)
|
||||||
|
by_code = {r.code: r for r in rows}
|
||||||
|
assert by_code["99490"].since == 2015
|
||||||
|
|
||||||
|
def test_since_falls_back_to_cpt_presence_when_no_appeared_event(self):
|
||||||
|
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
|
||||||
|
cpt_codes = [_cpt_code("90001", "sec_a", year=2024)]
|
||||||
|
cpt_presence = {"90001": (2019, 2021, 2022, 2024)}
|
||||||
|
rows = derive_families(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
cpt_codes=cpt_codes,
|
||||||
|
cpt_sections=sections,
|
||||||
|
cpt_presence=cpt_presence,
|
||||||
|
)
|
||||||
|
by_code = {r.code: r for r in rows}
|
||||||
|
assert by_code["90001"].since == 2019
|
||||||
|
|
||||||
|
def test_since_keeps_cpt_presence_when_appeared_event_is_later(self):
|
||||||
|
# An appeared event newer than the CPT evidence must not win —
|
||||||
|
# min(), not "prefer the event".
|
||||||
|
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
|
||||||
|
cpt_codes = [_cpt_code("90001", "sec_a", year=2024)]
|
||||||
|
cpt_presence = {"90001": (2019, 2021, 2022, 2024)}
|
||||||
|
events = {"90001": [_ev("90001", 2021, "appeared")]}
|
||||||
|
rows = derive_families(
|
||||||
|
{},
|
||||||
|
events,
|
||||||
|
{},
|
||||||
|
cpt_codes=cpt_codes,
|
||||||
|
cpt_sections=sections,
|
||||||
|
cpt_presence=cpt_presence,
|
||||||
|
)
|
||||||
|
by_code = {r.code: r for r in rows}
|
||||||
|
assert by_code["90001"].since == 2019
|
||||||
|
|
||||||
|
|
||||||
class TestLoadAndRefresh:
|
class TestLoadAndRefresh:
|
||||||
def test_load_and_refresh_in_place(self, restore_families):
|
def test_load_and_refresh_in_place(self, restore_families):
|
||||||
mod = restore_families
|
mod = restore_families
|
||||||
@@ -1099,6 +1156,59 @@ class TestLoadAndRefresh:
|
|||||||
finally:
|
finally:
|
||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
|
def test_derived_family_gets_empty_synonyms(self, restore_families):
|
||||||
|
# F3: a derived (non-hand) family must not become a chat trigger
|
||||||
|
# off its own one/two-word name — matching by name is #699's job.
|
||||||
|
mod = restore_families
|
||||||
|
con = duckdb.connect(":memory:")
|
||||||
|
try:
|
||||||
|
ensure_tables(con)
|
||||||
|
write_families(
|
||||||
|
con,
|
||||||
|
[
|
||||||
|
FamilyRow(
|
||||||
|
"AORTA-REPAIR",
|
||||||
|
"Repair",
|
||||||
|
"99999",
|
||||||
|
"base",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
refresh_from(con)
|
||||||
|
assert mod.FAMILIES["AORTA-REPAIR"].synonyms == ()
|
||||||
|
assert detect_codes("repair of the aorta").families == ()
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def test_hand_family_keeps_its_synonyms_after_refresh(self, restore_families):
|
||||||
|
mod = restore_families
|
||||||
|
con = duckdb.connect(":memory:")
|
||||||
|
try:
|
||||||
|
ensure_tables(con)
|
||||||
|
write_families(
|
||||||
|
con,
|
||||||
|
[
|
||||||
|
FamilyRow(
|
||||||
|
"CCM",
|
||||||
|
"Chronic Care Management",
|
||||||
|
"99490",
|
||||||
|
"base",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
refresh_from(con)
|
||||||
|
assert mod.FAMILIES["CCM"].synonyms == HAND_FAMILIES["CCM"].synonyms
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
def test_schema_present_table_absent_returns_empty(self):
|
def test_schema_present_table_absent_returns_empty(self):
|
||||||
# pfs schema created (e.g. by an earlier ensure_tables call for a
|
# pfs schema created (e.g. by an earlier ensure_tables call for a
|
||||||
# sibling table) but pfs.code_family itself never materialized —
|
# sibling table) but pfs.code_family itself never materialized —
|
||||||
|
|||||||
Reference in New Issue
Block a user