fix(pfs): CPT family edges never bridge two headed families; since/until from all ingested editions (refs #687)

Ruling C11: a use-with/parent edge, and the slice-1 addon-of relation
element, now union two codes only when at least one endpoint has no
qualifying CPT heading (cpt_groups) — when both already resolve to
different headings, the edge crosses families by design and is
skipped. Live-corpus review found 12 keys blending >= 2 headings this
way (PLA 600 = PLA 378 + Tier 1 Molecular Pathology 212 + Tier 2 10,
via 0133U's use-with; a Category III blob mixing a pacemaker heading
and a device-evaluation heading via 33224 -> 0695T; among others). The
guard lives in a new _cpt_edges() helper so it's testable in isolation
from the rest of derive_families' union-find wiring.

Ruling C12: derive_families gains cpt_presence (code -> sorted edition
years present, across every ingested edition, not just the newest);
when given, since/until come from it instead of being clamped to
whichever single edition cpt_codes carries. The CLI builds it from one
extra SELECT code, edition_year FROM pfs.cpt_code pass. Grouping/naming
stay newest-edition-scoped (cpt_codes unchanged).

Verified on the replica: no non-hand key has >= 2 distinct `note`
values; 99490 now reads since 2019 (its earliest ingested edition)
instead of 2024.
This commit is contained in:
kert
2026-09-09 19:53:34 -04:00
parent d1d4187f95
commit e9c3a5251f
4 changed files with 369 additions and 29 deletions

View File

@@ -212,23 +212,37 @@ def lineage_cmd(
con.close() con.close()
def _cpt_inputs(con: Any) -> tuple[list[Any], list[Any], list[Any]]: def _cpt_inputs(
"""The newest CPT edition's rows for ``derive_families`` — ``()`` con: Any,
empty on any replica shape that doesn't have them yet (I4: a plain ) -> tuple[list[Any], list[Any], list[Any], dict[str, tuple[int, ...]]]:
read of an old replica, pre pfs.cpt_*, is not a bug).""" """The newest CPT edition's rows for ``derive_families`` (grouping/
naming stays newest-edition-scoped, Ruling C12), plus ``cpt_presence``
— every ingested edition's ``code -> years present`` map, one extra
full-table pass, used only for ``since``/``until``: a code an earlier
edition ingested but the newest one dropped still gets its true
``since`` (not clamped to the newest edition) and a real ``until``.
All empty on any replica shape that doesn't have them yet (I4: a
plain read of an old replica, pre pfs.cpt_*, is not a bug)."""
try: try:
years = cpt_years(con) years = cpt_years(con)
except Exception as exc: except Exception as exc:
if _missing_table(exc): if _missing_table(exc):
return [], [], [] return [], [], [], {}
raise raise
if not years: if not years:
return [], [], [] return [], [], [], {}
newest = max(years) newest = max(years)
presence: dict[str, list[int]] = {}
for code, year in con.execute(
"SELECT code, edition_year FROM pfs.cpt_code"
).fetchall():
presence.setdefault(code, []).append(year)
cpt_presence = {code: tuple(sorted(set(yrs))) for code, yrs in presence.items()}
return ( return (
read_cpt_codes(con, newest), read_cpt_codes(con, newest),
read_cpt_sections(con, newest), read_cpt_sections(con, newest),
read_cpt_instructions(con, newest), read_cpt_instructions(con, newest),
cpt_presence,
) )
@@ -252,7 +266,7 @@ def _derive_rows(con: Any) -> list[Any]:
"SELECT hcpcs, arg_max(description, year) FROM pfs.rvu WHERE mod IS NULL OR mod = '' GROUP BY hcpcs" "SELECT hcpcs, arg_max(description, year) FROM pfs.rvu WHERE mod IS NULL OR mod = '' GROUP BY hcpcs"
).fetchall() ).fetchall()
} }
cpt_codes, cpt_sections, cpt_instructions = _cpt_inputs(con) cpt_codes, cpt_sections, cpt_instructions, cpt_presence = _cpt_inputs(con)
return derive_families( return derive_families(
elements_by_code, elements_by_code,
events_by_code, events_by_code,
@@ -260,6 +274,7 @@ def _derive_rows(con: Any) -> list[Any]:
cpt_codes=cpt_codes, cpt_codes=cpt_codes,
cpt_sections=cpt_sections, cpt_sections=cpt_sections,
cpt_instructions=cpt_instructions, cpt_instructions=cpt_instructions,
cpt_presence=cpt_presence,
) )

View File

@@ -188,7 +188,9 @@ def stem_tokens(description: str | None) -> frozenset[str]:
# ``crosswalk`` events are dropped entirely — a rule's "codes ... are # ``crosswalk`` events are dropped entirely — a rule's "codes ... are
# crosswalked to X for valuation" sentence routinely lists a whole # crosswalked to X for valuation" sentence routinely lists a whole
# family's worth of codes together and is evidence, not a merge signal. # family's worth of codes together and is evidence, not a merge signal.
_LINK_RELATIONS = {"addon-of", "defined-by-reference-to"} # ``addon-of`` moved out to ``_cpt_edges`` (Ruling C11: it needs the same
# cross-heading guard as ``use-with``/``parent``).
_LINK_RELATIONS = {"defined-by-reference-to"}
#: ``replaces``/``replaced_by`` events still count, but only when the #: ``replaces``/``replaced_by`` events still count, but only when the
#: relevant field names exactly one code — a multi-code list (a blanket #: relevant field names exactly one code — a multi-code list (a blanket
#: "these codes are replaced by ..." sentence spanning several families, #: "these codes are replaced by ..." sentence spanning several families,
@@ -392,6 +394,49 @@ def _group_rows(
return out return out
def _cpt_edges(
union: Any,
groups_by_code: Mapping[str, tuple[str, str, str, tuple[str, ...]]],
cpt_instructions: Sequence[Any],
cpt_code_rows: Mapping[str, Any],
elements: Mapping[str, Sequence[Any]],
) -> None:
"""Wire the three CPT-sourced intra-family edges — a single-target
``use-with`` instruction, ``pfs.cpt_code.parent`` (the semicolon
rule), and an ``addon-of`` relation element — each subject to
Ruling C11: when *both* endpoints already resolve to a CPT heading
(``cpt_groups``) and those headings differ, the edge crosses
families by design and must not be unioned (#687 live-corpus finds:
``0133U``'s ``use-with`` note reaching into Tier 1 Molecular
Pathology blended it with Proprietary Laboratory Analyses; a
``33224 -> 0695T`` addon-of edge blended a pacemaker heading with a
Category III one). An edge where at least one endpoint has no
heading at all (a HCPCS code, or a CPT code alone at every ancestor)
still joins as before — only a *headed-to-differently-headed* edge
is skipped.
"""
def bridges_distinct_headings(a: str, b: str) -> bool:
ia = groups_by_code.get(a)
ib = groups_by_code.get(b)
return ia is not None and ib is not None and ia[2] != ib[2]
def guarded_union(a: str, b: str) -> None:
if not bridges_distinct_headings(a, b):
union(a, b)
for instr in cpt_instructions:
if instr.kind == "use-with" and len(instr.targets) == 1:
guarded_union(instr.code, instr.targets[0].upper())
for code, row in cpt_code_rows.items():
if row.parent:
guarded_union(code, row.parent.upper())
for code, els in elements.items():
for e in els:
if e.type == "relation" and e.value == "addon-of" and e.detail:
guarded_union(code, e.detail.upper())
def _connected(nodes: Sequence[str], adj: Mapping[str, set[str]]) -> list[list[str]]: def _connected(nodes: Sequence[str], adj: Mapping[str, set[str]]) -> list[list[str]]:
"""Connected components of *nodes* using only edges between members of """Connected components of *nodes* using only edges between members of
*nodes* itself (an edge to a node outside the set doesn't count).""" *nodes* itself (an edge to a node outside the set doesn't count)."""
@@ -423,6 +468,7 @@ def derive_families(
cpt_codes: Sequence[Any] = (), cpt_codes: Sequence[Any] = (),
cpt_sections: Sequence[Any] = (), cpt_sections: Sequence[Any] = (),
cpt_instructions: Sequence[Any] = (), cpt_instructions: Sequence[Any] = (),
cpt_presence: Mapping[str, tuple[int, ...]] | None = None,
) -> list[Any]: ) -> list[Any]:
"""Connected components over codes linked by the CPT manual's own """Connected components over codes linked by the CPT manual's own
hierarchy (``cpt_codes``/``cpt_sections`` — the primary organizing hierarchy (``cpt_codes``/``cpt_sections`` — the primary organizing
@@ -447,14 +493,33 @@ def derive_families(
HCPCS codes (not in ``cpt_codes``) keep the slice-1 derivation HCPCS codes (not in ``cpt_codes``) keep the slice-1 derivation
unchanged — they only join a CPT-headed family through one of the unchanged — they only join a CPT-headed family through one of the
slice-1 edges (e.g. a ``replaced_by`` event into a CPT code). slice-1 edges (e.g. a ``replaced_by`` event into a CPT code).
``cpt_codes``/``cpt_sections``/``cpt_instructions`` stay scoped to
one (the newest) edition — grouping and naming are newest-edition
concepts. ``cpt_presence`` (Ruling C12) is separate: a ``code ->
sorted edition years present`` map spanning *every* ingested
edition, used only for ``since``/``until`` — a code the newest
edition dropped still gets its true earliest year and a real
``until``, rather than being clamped to the one edition
``cpt_codes`` carries. When omitted, ``since``/``until`` fall back
to ``cpt_codes`` alone (single-edition — the pre-C12 behavior).
""" """
cpt_code_rows: dict[str, Any] = {c.code: c for c in cpt_codes} cpt_code_rows: dict[str, Any] = {c.code: c for c in cpt_codes}
groups_by_code = cpt_groups(cpt_codes, cpt_sections) groups_by_code = cpt_groups(cpt_codes, cpt_sections)
cpt_since: dict[str, int] = {} cpt_since: dict[str, int] = {}
cpt_last_year: dict[str, int] = {} cpt_last_year: dict[str, int] = {}
if cpt_presence:
for code, years in cpt_presence.items():
if not years:
continue
cpt_since[code] = min(years)
cpt_last_year[code] = max(years)
else:
for c in cpt_codes: for c in cpt_codes:
cpt_since[c.code] = min(cpt_since.get(c.code, c.edition_year), c.edition_year) cpt_since[c.code] = min(
cpt_since.get(c.code, c.edition_year), c.edition_year
)
cpt_last_year[c.code] = max( cpt_last_year[c.code] = max(
cpt_last_year.get(c.code, c.edition_year), c.edition_year cpt_last_year.get(c.code, c.edition_year), c.edition_year
) )
@@ -462,15 +527,19 @@ def derive_families(
# Reject members that aren't code-shaped before deriving anything — # Reject members that aren't code-shaped before deriving anything —
# a corpus parsing artifact (e.g. a bare '\x1a') must never become a # a corpus parsing artifact (e.g. a bare '\x1a') must never become a
# family of its own. `cpt_code_rows` is exempt from the CODE_RE # family of its own. `cpt_code_rows`/`cpt_presence` are exempt from
# shape check: those codes come from the structured `pfs.cpt_code` # the CODE_RE shape check: those codes come from the structured
# table, not a free-text scan, and CODE_RE (5 digits or a letter + # `pfs.cpt_code` table, not a free-text scan, and CODE_RE (5 digits
# 4 digits) doesn't cover Category II/III/PLA's 4-digits-then-letter # or a letter + 4 digits) doesn't cover Category II/III/PLA's
# shape (0001U, 0001T, 0001F) — excluding them would silently drop # 4-digits-then-letter shape (0001U, 0001T, 0001F) — excluding them
# ~1,400 real 2024-edition codes from every family (#687-style regression # would silently drop ~1,400 real 2024-edition codes from every
# found live: those categories vanished from the derivation entirely). # family (#687-style regression found live: those categories
# vanished from the derivation entirely). `cpt_presence` also brings
# in a code the newest edition dropped (Ruling C12) so it still gets
# a family row with its true since/until instead of vanishing.
codes = sorted( codes = sorted(
set(cpt_code_rows) set(cpt_code_rows)
| set(cpt_presence or {})
| { | {
c c
for c in (set(elements) | set(events) | set(descriptions)) for c in (set(elements) | set(events) | set(descriptions))
@@ -514,13 +583,12 @@ def derive_families(
# bridges unrelated families and is evidence, not a merge signal # bridges unrelated families and is evidence, not a merge signal
# (live corpus: 637 use-with instructions, 325 name >1 target, one # (live corpus: 637 use-with instructions, 325 name >1 target, one
# names 228 — narrowing this is what keeps the CPT heading grouping # names 228 — narrowing this is what keeps the CPT heading grouping
# from cascading into a single book-spanning component). # from cascading into a single book-spanning component). Ruling C11
for instr in cpt_instructions: # adds a second guard on top, in _cpt_edges: an edge (use-with,
if instr.kind == "use-with" and len(instr.targets) == 1: # parent, or addon-of) between two codes that each already resolve
union(instr.code, instr.targets[0].upper()) # to a *different* CPT heading crosses families by design and is
for code, row in cpt_code_rows.items(): # skipped, even when single-target/unambiguous.
if row.parent: _cpt_edges(union, groups_by_code, cpt_instructions, cpt_code_rows, elements)
union(code, row.parent.upper())
for code, els in elements.items(): for code, els in elements.items():
for e in els: for e in els:

View File

@@ -273,7 +273,7 @@ class TestFamilies:
res.output res.output
) )
def _ccm_cpt_edition(self): def _ccm_cpt_edition(self, year=2024):
section = CptSection( section = CptSection(
sec_id="sec_ccm", sec_id="sec_ccm",
level=3, level=3,
@@ -328,7 +328,7 @@ class TestFamilies:
), ),
) )
return CptEdition( return CptEdition(
year=2024, year=year,
sections=(section,), sections=(section,),
codes=codes, codes=codes,
instructions=(), instructions=(),
@@ -351,6 +351,20 @@ class TestFamilies:
assert "families: total" in res.output assert "families: total" in res.output
assert "cpt-named" in res.output assert "cpt-named" in res.output
def test_since_spans_all_ingested_editions_not_just_the_newest(self, con):
# Ruling C12: _cpt_inputs' cpt_presence comes from a full
# SELECT code, edition_year FROM pfs.cpt_code (every edition
# write_cpt_edition has ever written), so `since` is the
# earliest edition, not clamped to whichever one is newest.
write_cpt_edition(con, self._ccm_cpt_edition(year=2019), "ITEM_2019")
write_cpt_edition(con, self._ccm_cpt_edition(year=2024), "ITEM_2024")
res = runner.invoke(app, ["pfs", "families", "--write"])
assert res.exit_code == 0, res.output
fams = read_families(con)
ccm = {r.code: r for r in fams if r.key == "CCM"}
assert ccm["99490"].since == 2019
assert ccm["99490"].until is None
def test_write_tolerates_null_rvu_description(self, con): def test_write_tolerates_null_rvu_description(self, con):
# #687 regression: a real pfs.rvu row can carry a NULL description # #687 regression: a real pfs.rvu row can carry a NULL description
# (a corpus parsing artifact) at the current max year with a # (a corpus parsing artifact) at the current max year with a

View File

@@ -22,6 +22,7 @@ from pfs.families import (
FAMILIES, FAMILIES,
HAND_FAMILIES, HAND_FAMILIES,
Detection, Detection,
_cpt_edges,
cpt_groups, cpt_groups,
derive_families, derive_families,
detect_codes, detect_codes,
@@ -782,6 +783,248 @@ class TestDeriveCpt:
assert by_code["G2058"].since == 2020 and by_code["G2058"].until == 2021 assert by_code["G2058"].since == 2020 and by_code["G2058"].until == 2021
class TestCptEdgesHelper:
def test_skips_only_edges_between_distinct_headings(self):
# #687 controller review (Ruling C11), tested against the helper
# in isolation: an edge is skipped only when BOTH endpoints
# already resolve to a heading and those headings differ.
groups_by_code = {
"70001": (
"HEADING-A",
"Heading A",
"Chapter > Heading A",
("70001", "70002"),
),
"70002": (
"HEADING-A",
"Heading A",
"Chapter > Heading A",
("70001", "70002"),
),
"70003": (
"HEADING-B",
"Heading B",
"Chapter > Heading B",
("70003", "70004"),
),
}
unioned = []
def fake_union(a, b):
unioned.append((a, b))
instructions = [
CptInstructionRow(
edition_year=2024,
item_key="I",
code="70001",
kind="use-with",
text="",
targets=["70003"], # both headed, distinct -> skipped
),
CptInstructionRow(
edition_year=2024,
item_key="I",
code="70001",
kind="use-with",
text="",
targets=["70005"], # 70005 unheaded -> unioned
),
]
_cpt_edges(fake_union, groups_by_code, instructions, {}, {})
assert unioned == [("70001", "70005")]
class TestCptEdgeGuardC11:
def _two_headings(self):
sections = [
_cpt_section("sec_a", ("Chapter", "Heading A")),
_cpt_section("sec_b", ("Chapter", "Heading B")),
]
cpt_codes = [
_cpt_code("70001", "sec_a"),
_cpt_code("70002", "sec_a"),
_cpt_code("70003", "sec_b"),
_cpt_code("70004", "sec_b"),
]
return sections, cpt_codes
def test_use_with_between_two_headed_codes_does_not_bridge(self):
sections, cpt_codes = self._two_headings()
instructions = [
CptInstructionRow(
edition_year=2024,
item_key="ITEM0001",
code="70001",
kind="use-with",
text="(Use 70001 in conjunction with 70003)",
targets=["70003"],
)
]
rows = derive_families(
{},
{},
{},
cpt_codes=cpt_codes,
cpt_sections=sections,
cpt_instructions=instructions,
)
keys = {r.code: r.key for r in rows}
assert keys["70001"] == keys["70002"]
assert keys["70003"] == keys["70004"]
assert keys["70001"] != keys["70003"]
def test_use_with_from_unheaded_code_still_joins_the_headed_family(self):
sections, cpt_codes = self._two_headings()
# 70005 sits alone under its own chapter — no sibling anywhere it
# rolls up to, so it never qualifies for a heading of its own.
sections = sections + [_cpt_section("sec_x", ("Solo Chapter", "Heading X"))]
cpt_codes = cpt_codes + [_cpt_code("70005", "sec_x")]
instructions = [
CptInstructionRow(
edition_year=2024,
item_key="ITEM0001",
code="70005",
kind="use-with",
text="(Use 70005 in conjunction with 70001)",
targets=["70001"],
)
]
rows = derive_families(
{},
{},
{},
cpt_codes=cpt_codes,
cpt_sections=sections,
cpt_instructions=instructions,
)
keys = {r.code: r.key for r in rows}
assert keys["70005"] == keys["70001"] == keys["70002"]
def test_parent_between_two_headed_codes_does_not_bridge(self):
sections, cpt_codes = self._two_headings()
# 70003's `parent` names a code in a DIFFERENT heading — the
# guard must hold even though a real semicolon-rule parent is
# always within the same heading in practice.
cpt_codes = [
c if c.code != "70003" else _cpt_code("70003", "sec_b", parent="70001")
for c in cpt_codes
]
rows = derive_families({}, {}, {}, cpt_codes=cpt_codes, cpt_sections=sections)
keys = {r.code: r.key for r in rows}
assert keys["70003"] == keys["70004"]
assert keys["70003"] != keys["70001"]
def test_addon_of_between_two_headed_codes_does_not_bridge(self):
sections, cpt_codes = self._two_headings()
elements = {"70003": [_el("70003", "relation", "addon-of", "70001")]}
rows = derive_families(
elements, {}, {}, cpt_codes=cpt_codes, cpt_sections=sections
)
keys = {r.code: r.key for r in rows}
assert keys["70003"] == keys["70004"]
assert keys["70003"] != keys["70001"]
def test_addon_of_from_unheaded_code_still_joins(self):
sections, cpt_codes = self._two_headings()
elements = {"77000": [_el("77000", "relation", "addon-of", "70001")]}
rows = derive_families(
elements, {}, {}, cpt_codes=cpt_codes, cpt_sections=sections
)
keys = {r.code: r.key for r in rows}
assert keys["77000"] == keys["70001"]
def test_note_value_is_unaffected_by_the_guard(self):
# The guard only skips a union edge; it must not touch a code's
# own note (still its own heading's path_key).
sections, cpt_codes = self._two_headings()
instructions = [
CptInstructionRow(
edition_year=2024,
item_key="ITEM0001",
code="70001",
kind="use-with",
text="(Use 70001 in conjunction with 70003)",
targets=["70003"],
)
]
rows = derive_families(
{},
{},
{},
cpt_codes=cpt_codes,
cpt_sections=sections,
cpt_instructions=instructions,
)
by_code = {r.code: r for r in rows}
assert by_code["70001"].note == "Chapter > Heading A"
assert by_code["70003"].note == "Chapter > Heading B"
class TestCptPresenceC12:
def test_code_absent_from_newest_edition_gets_since_and_until_from_all_editions(
self,
):
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
cpt_codes = [
_cpt_code("90001", "sec_a", year=2024),
_cpt_code("90002", "sec_a", year=2024),
]
cpt_presence = {
"90001": (2019, 2021, 2022, 2024),
"90002": (2019, 2021, 2022, 2024),
"80001": (2019, 2020, 2021, 2022), # gone by 2024, not in cpt_codes
}
rows = derive_families(
{},
{},
{},
cpt_codes=cpt_codes,
cpt_sections=sections,
cpt_presence=cpt_presence,
)
by_code = {r.code: r for r in rows}
assert "80001" in by_code
assert by_code["80001"].since == 2019
assert by_code["80001"].until == 2023
def test_code_new_in_newest_edition_has_no_until(self):
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
cpt_codes = [
_cpt_code("90001", "sec_a", year=2024),
_cpt_code("90003", "sec_a", year=2024),
]
cpt_presence = {
"90001": (2019, 2021, 2022, 2024),
"90003": (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
assert by_code["90003"].since == 2024
assert by_code["90003"].until is None
def test_without_cpt_presence_falls_back_to_cpt_codes_alone(self):
# Pre-C12 behavior preserved when the caller doesn't have a
# multi-edition presence map to give.
sections = [_cpt_section("sec_a", ("Chapter", "Heading A"))]
cpt_codes = [
_cpt_code("90001", "sec_a", year=2024),
_cpt_code("90002", "sec_a", year=2024),
]
rows = derive_families({}, {}, {}, cpt_codes=cpt_codes, cpt_sections=sections)
by_code = {r.code: r for r in rows}
assert by_code["90001"].since == 2024
assert by_code["90001"].until is None
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