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:
kert
2026-09-09 20:56:51 -04:00
parent 78a56f593e
commit 589b8338ef
2 changed files with 128 additions and 6 deletions

View File

@@ -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

View File

@@ -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 —