fix(pfs): name-phrase family detection only for CPT-named families (refs #699)

This commit is contained in:
kert
2026-09-10 09:33:43 -04:00
parent 5f31d7b645
commit e80b179f8c
2 changed files with 90 additions and 8 deletions

View File

@@ -11,7 +11,7 @@ from __future__ import annotations
import logging
import re
import threading
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import Any, Mapping, Sequence
_logger = logging.getLogger(__name__)
@@ -64,6 +64,15 @@ class Family:
name: str
codes: tuple[str, ...]
synonyms: tuple[str, ...] # lower-case; matched on word boundaries
#: True when this (derived) family is named for a real CPT manual
#: heading (``load_families`` sets it from a non-empty ``FamilyRow.note``
#: — the heading path) rather than a stem-derived group's raw RVU short
#: description. Ruling B1: only a CPT-named family's ``name`` is ever a
#: name-phrase chat trigger (``rebuild_index``) — a stem group's name is
#: often abbreviated billing shorthand, not a real phrase. Hand families
#: are matched by their curated ``synonyms`` instead and leave this
#: ``False``.
cpt: bool = False
HAND_FAMILIES: dict[str, Family] = {
@@ -172,14 +181,19 @@ def _build_registry_index(
``rebuild_index``/``refresh_from`` swap in together.
A hand family is matched by any of its curated ``synonyms``; a
derived (non-hand) family is matched by its own ``name`` too, but
only when the name is a real phrase (``_phrase_qualifies``) — a
one-word CPT heading title must never become a chat trigger.
derived, CPT-named family (``fam.cpt``) is matched by its own
``name`` too, but only when the name is a real phrase
(``_phrase_qualifies``) — a one-word CPT heading title must never
become a chat trigger. Ruling B1: a derived family that is *not*
CPT-named (a stem-derived group, ``fam.cpt`` false) never matches by
name at all, even when its name happens to be long/multi-word — its
"name" is a raw RVU short description (billing shorthand), not a
real phrase.
"""
phrase_families: dict[str, set[str]] = {}
for key, fam in families.items():
phrases = list(fam.synonyms)
if key not in HAND_FAMILIES and _phrase_qualifies(fam.name):
if key not in HAND_FAMILIES and fam.cpt and _phrase_qualifies(fam.name):
phrases.append(fam.name.lower())
for p in phrases:
phrase_families.setdefault(p, set()).add(key)
@@ -894,6 +908,7 @@ def load_families(con: Any) -> dict[str, Family]:
return {}
raise
out: dict[str, Family] = {}
cpt_key: set[str] = set()
for r in rows:
fam = out.get(r.key)
codes = (*(fam.codes if fam else ()), r.code)
@@ -905,7 +920,16 @@ def load_families(con: Any) -> dict[str, Family]:
# 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 ()
# Ruling B1: a non-empty note is the code's own CPT heading path
# (_group_rows) — any member row carrying one marks the whole
# family as CPT-named, which is the only kind of derived family
# whose `name` is a real phrase rather than a stem group's raw
# RVU short description.
if r.note:
cpt_key.add(r.key)
out[r.key] = Family(r.key, r.name, tuple(sorted(set(codes))), syn)
for key in cpt_key:
out[key] = replace(out[key], cpt=True)
return out

View File

@@ -140,19 +140,36 @@ class TestDetectDerivedFamilies:
assert d.codes == ("15850", "15851")
def test_derived_family_name_phrase_requires_two_words(self, restore_families):
restore_families.FAMILIES["GENE"] = Family("GENE", "GENE", ("81400",), ())
# Both are CPT-named (cpt=True) — the single-word heading title
# must still never fire regardless of the B1 CPT-name gate.
restore_families.FAMILIES["GENE"] = Family(
"GENE", "GENE", ("81400",), (), cpt=True
)
restore_families.FAMILIES["WOUND-CARE"] = Family(
"WOUND-CARE", "Complex Wound Debridement Services", ("11042",), ()
"WOUND-CARE", "Complex Wound Debridement Services", ("11042",), (), cpt=True
)
rebuild_index()
# The single-word heading name never fires, even though the text
# contains it verbatim.
assert "GENE" not in detect_codes("the GENE panel result").families
# A qualifying (>= 12 chars, >= 2 words) derived name does.
# A qualifying (>= 12 chars, >= 2 words) CPT-named derived name does.
d = detect_codes("How are Complex Wound Debridement Services valued?")
assert "WOUND-CARE" in d.families
assert "11042" in d.codes
def test_derived_family_name_requires_cpt_flag(self, restore_families):
# Ruling B1: a stem-derived family's "name" is a raw RVU short
# description, not a real phrase — even a long, multi-word one
# must never become a name trigger unless the family is
# CPT-named (fam.cpt). Member-code detection is unaffected.
restore_families.FAMILIES["STEM-GROUP"] = Family(
"STEM-GROUP", "Complex Remote Monitoring Setup Review", ("99999",), ()
)
rebuild_index()
d = detect_codes("How is Complex Remote Monitoring Setup Review valued?")
assert "STEM-GROUP" not in d.families
assert detect_codes("value of 99999").families == ("STEM-GROUP",)
def test_family_of_is_indexed(self, restore_families):
synthetic = {
f"F{i:04d}": Family(
@@ -1311,6 +1328,47 @@ class TestLoadAndRefresh:
finally:
con.close()
def test_load_families_sets_cpt_flag_from_note(self):
# Ruling B1: a non-empty `note` (the code's own CPT heading path)
# on any member row marks the whole derived family CPT-named;
# a family with no member carrying a note stays cpt=False.
con = duckdb.connect(":memory:")
try:
ensure_tables(con)
write_families(
con,
[
FamilyRow(
"CHRONIC-CARE-MANAGEMENT-SERVICES",
"Chronic Care Management Services",
"99490",
"base",
None,
None,
"",
0,
"Evaluation and Management > Care Management "
"Services > Chronic Care Management Services",
),
FamilyRow(
"STEM-GROUP",
"Complex Remote Monitoring Setup Review",
"99999",
"base",
None,
None,
"",
0,
"",
),
],
)
fams = load_families(con)
assert fams["CHRONIC-CARE-MANAGEMENT-SERVICES"].cpt is True
assert fams["STEM-GROUP"].cpt is False
finally:
con.close()
def test_schema_present_table_absent_returns_empty(self):
# pfs schema created (e.g. by an earlier ensure_tables call for a
# sibling table) but pfs.code_family itself never materialized —