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:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user